일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- 클린 코드
- 도메인 모델
- Java
- 클린코드
- 스프링부트
- string
- Lombok
- 쿼리 최적화
- clean code
- 객체지향의 사실과 오해
- cache
- 캡슐화
- 캐시
- REST API
- 협력
- 객체
- 객체지향
- 자바
- JIT
- 책임
- Refactor
- 리팩토링
- 추상화
- spring boot
- 인터프리터
- JPA
- 캐싱
- SRP
- 스프링
- 재사용성
- Today
- Total
GO SIWOO!
[리팩토링 일지] 팀 프로젝트, 나홀로 리팩토링 (5) - 응답 form & Global Exception 본문
[리팩토링 일지] 팀 프로젝트, 나홀로 리팩토링 (5) - 응답 form & Global Exception
gosiwoo 2023. 8. 17. 02:44
[리팩토링 일지] 팀 프로젝트, 나홀로 리팩토링 (4) - 회원기능 구현을 위한 스프링 Security와 JWT발
📌 기존의 회원기능 기존 프로젝트의 회원 기능은 kakao API를 통해서 진행했다. 그러나, 하드코딩 된 API 호출을 보고 적용하지 않고 프론트 팀에서 전달받은 인증 코드를 바탕으로 회원 정보를 Ha
gosiwoo.tistory.com
📌3달만의 포스팅
약 3달 만에 리팩토링 관련 포스팅을 진행하게 되었다. 팀프로젝트를 2개 정도 진행하고 입사원서 작성과 그룹스터디... 많은 일이 있었지만 조금씩이라도 짬짬이 손 가는 대로 코드를 작성하였다. 오늘 작성할 포스팅 외에도 추가로 구현한 기능들도 꽤 있고 이 포스팅의 내용은 약 2 달반 전에 작성한 코드였다.
이미 완성하고도 남을 개인 프로젝트 였지만 아직까지 질질 끌고 있는데 최대한 이번달 내로 완성시키는 것을 목표로 해야겠다.
📌응답 form의 변경
1. 기존 팀 프로젝트의 응답 form의 문제점
기존 프로젝트의 응답 form은 정해지지 않았다. 다음과 같이 collection DTO에 값을 바로 반환하는 방법으로 프론트엔드 단으로 전달해 주거나 삭제 요청에도 성공, 실패 여부도 없고 심지어 RESTful 하지 않은 엔드포인트를 갖고 있었다. Controller 클래스 부분의 어노테이션으로 @RestController를 붙였으므로 @ResponseBody로 DTO 객체를 반환하는 형태만 취한 것이다.
이같은 방법은 HTTP 상태 코드를 직접 제어할 수 없는 상태로 반환하기에 조금 더 세밀한 제어의 분기가 불가능할 것이다. 물론 @ResponseStatus 어노테이션을 통해 설정이 가능하지만 사용하지도 않았을뿐더러 여간 복잡한 일이 아닐 것이다.
2. 변경된 응답 form
응답의 규격화
우선 RESTful한 엔드포인트는 둘째 치더라도 응답 form의 규격화가 필요했다.
2가지중 선택을 해야 했다
- Response 클래스 사용
- ResponseEntity<T> 클래스 사용
결론은 ReponseEntity<T> 클래스를 사용하기로 했다.
클래스를 확장하여 커스텀을 해야 하는 Response 클래스에 비해 ResponseEntity<T>는 메서드를 통해 헤더 커스텀에 대해 더욱 쉬운 접근이 가능할뿐더러 메서드 체이닝을 통해서 더욱 뛰어난 가독성을 줄 수 있다고 생각해 선택하였다.
return ResponseEntity.ok(response);
위와 같은 방식으로 상태 코드를 사용하지 않고 반환할 수 있도록 리팩토링을 하였다.
하지만 여기서 추가적인 작업이 필요한데 전역 예외 처리 설정에 대해서 고려가 필요했다. 이것은 @RestControllerAdvice 어노테이션을 사용해 한 전역 예외 처리 설정에 대해 뒤쪽에서 설명하겠다.
요청 body는 어떻게?
그러면 요청 body는 어떻게 해야 할까?
Entity는 물론 그대로 Controller에서 반환할 수는 없는 노릇이었는데 Entity의 내부 구현 부분을 캡슐화하여 더욱 객체지향적인 설계를 가져가고 순환참조를 예방하고자 했다. 따라서 요청 body로 사용될 DTO의 형태에 대한 규격을 어느 정도 정해줄 필요성을 느꼈다.
패키지 구조는 크게 도메인으로 나누고 각 도메인마다 계층을 나누었다. dto패키지 아래로는 request와 reponse 패키지를 추가로 두어 요청과 응답에 대한 패키지를 분리하였다.
이렇게 하면 API의 엔드포인트가 많아질수록 응답과 요청에 대해서 수많은 DTO가 생성될 수 있다는 고민이 있었는데 인프런의 질의응답에 답이 있었다.
- 프로젝트가 커지면 대부분 검색을 통해 클래스를 찾게 되고 (또는 타고 타고 클래스를 찾게 되고)
- 한 파일에 여러 클래스가 있더라도 검색 기능에는 지장이 없으며
- 매~~우 간혹 패키지로 클래스를 찾아야 하는 경우, 패키지의 구조가 매우 이상하지만 않다면 특정 객체를 찾는데 크게 문제가 없다
위와 같이 강사님께서 답을 주셨는데 DTO가 많아지더라도 IDE의 편리한 기능들을 통해 클래스의 검색이 쉬워지며 이는 잘 정리된 패키지 구조에서는 걱정할 필요가 없다는 말을 해 주셨다.
QuizWordDto와 SearchedwordDto 같은 클래스는 QueryDSL을 통한 복잡한 쿼리를 반환하는 매핑 DTO이다.
따라서 다음과 같은 형태로 Response DTO로 정하였다.
모든 Response는 msg라는 응답 메시지와 data 또는 datum이라는 필드를 가지며 msg에는 요청에 대한 결과의 설명을 반환하며 data 또는 datum은 API 호출에 대한 기댓값을 담는 필드이다.
여기서 추가적으로 수정되어야 할 점은 msg의 값이 하드코딩 되어 있다는 점이다. 이를 하나의 클래스에서 나중에 관리하여 정리하고자 한다.
📌Global Exception의 추가
1. Global Exception의 필요성
API 호출에서 기존의 에러는 위와 같은 형태로 반환된다. 이와 같은 정보는 모든걸 담고 있지만 에러발생 위치를 상세하게 알려주지만 이는 API를 호출하는 쪽의 입장에서는 불필요한 정보이다.
이를 더욱 간결하게 하여 API를 호출하는 쪽에서 보기 편하게 할 필요가 있었다. 또한 전역에서 에러를 처리하여 AOP 방식으로 Application 전반에서 발생하는 모든 Controller의 예외를 한 곳에서 관리할 수 있게 한다는 목적이 있었다.
따라서 다음과 같은 방식으로 ErrorResponse라는 DTO로 모든 에러를 반환하기로 하였고 아래는 그 결과이다.
2. 실제 적용 코드
모든 예외는 ResponseEntity<ErrorResponse>의 형태로 에러의 정보를 반환한다.
응답 DTO
보시다시피 여느 ResponseDTO와 형태가 같다. 다만 정적 팩토리 메서드를 사용해 객체의 생성을 캡슐화하고 호출시마다 새로운 ErrorResponse 객체를 만들 이유가 없기에 사용하게 되었다.
다만 한가지 아쉬운 점은 occurred라는 메서드의 이름을 from과 of로 하는게 더 좋지 않았을까 하는 아쉬움이 있다. 이는 네이밍 컨벤션을 지키지 않고 Error가 발생했다는 점을 메서드를 통해 제공하기 위해서 occurred로 지었는데 이에 대해서 더 고민해봐야 할 점이라고 생각한다.
전역 예외 처리 클래스
Controller의 전역 예외 처리하는 클래스의 일부이다.
클래스 전체적으로 @RestControllerAdvice가 걸려있다. @ControllerAdvice와 달리 자바 객체를 Json 또는 XML 형태로 반환하기 위해서 선택하였다.
appExceptionHandler(AppException e) 메서드는 서비스 단에서 발생한 예외를 핸들링 하기 위해서 선언한 메서드이다.
커스텀 Exception
커스텀 Exception인 AppException 클래스이다. 이는 RuntimeException을 상속받았으며 서비스 단에서 조건에 따른 예외를 발생하기에 RuntimeException을 상속받도록 하였다.
Service 단에서의 AppException 호출
서비스 단에서 AppException을 호출한 경우의 예이다.
단어장에서 단어의 ID 값을 통해 삭제를 할 때에 단어가 없으면 예외를 던진다.
여기서 수정해야 할 점이 한가지 더 있는데 " 번호의 단어가 없습니다." 같은 텍스트가 코드 상에 그대로 노출이 되어 이를 상수로 다루는 클래스를 따로 두거나 Service 클래스 상단에 정적 변수로 두도록 수정할 예정이다.
앞서 설명한 AppException과 같은 비즈니스 로직 중에서의 예외뿐만 아니라 API에게 전달된 매개변수의 개수나 형식이 맞지 않는다 할지, 아니면 JWT 토큰이 만료, 인증불가 등등으로 서비스를 이용할 수 없을때 발생하는 예외도 @RestControllerAdvice 클래스에서 모두 처리를 하도록 수정하였다.
📌리팩토링 과정에서 참고 한 글들
ResponseEntity - Spring Boot에서 Response를 만들자 (techcourse.co.kr)
ResponseEntity는 왜 사용하는 것이며 @RestControllerAdvice는 무엇일까. (tistory.com)
DTO의 개수가 많아질 경우에는 어떻게 관리하는게 좋을까요? - 인프런 | 질문 & 답변 (inflearn.com)
[Java] Global Exception 이해하고 구성하기 : Controller Exception — Contributor9 (tistory.com)