Spring에서 응답값을 반환하는 방식을 개선해보자 ❕
보통 개인 프로젝트에서 컨트롤러 응답값을 반환할 때, 다음과 같이 작성해줬다.
@GetMapping("/summary")
public ResponseEntity<?> searchSummaryByDate(, ScheduleSearchReqDto reqDto) {
Message message = Message.builder()
.status(HttpStatus.OK)
.data(scheduleBusinessService.searchSummaryByDate(reqDto.getStartDate(), reqDto.getEndDate()))
.message("success")
.build();
return new ResponseEntity<>(message, message.getStatus());
}
즉, Service의 반환 데이터를 Controller로 반환하고, Controller에서 Message(커스텀한 dto)에 담고, 이를 또 ResponseEntity로 반환하는.. 다소 중복되고 비효율적인 코드이다.
그래서 이번 시간에는 위와 같이 Contoller에서 작성한 공통되는 코드를 개선해보려고 한다.
1. 해결 방법 탐색
1-1. Interceptor
처음에는 Interceptor의 postHandler()
를 활용해서 구현하려 했다. 하지만 인터셉터는 ModelAndView를 수정할 수는 있지만 HTTP 응답 body에는 직접 관여하지 못한다. 또한, @RestController를 사용해 반환된 DTO, 엔티티, String 등은 View형태가 아니므로 해당 반환된 객체에 접근하거나 수정할 수 없다. (이 시점에는 아직 반환된 데이터가 JSON이나 다른 형태로 변환하기 전이고, HttpServletReponse의 body에는 아직 데이터가 쓰여지지 않은 상태이다)
실제로 로그를 찍어보면, modelAndView는 null이고 내가 접근해야 할 body 객체에 접근할 수 없다.
1-2. Filter
필터는 인터셉터와 달리, 응답 본문을 가로채고 수정할 수 있다. 필터에서 제공하는 ServletResponse 객체를 ContentCachingResponseWrapper로 변환하면, 이를 통해 응답 body 데이터를 byte[]로 읽고 수정할 수 있다.
1-3. ResponseBodyAdvice (선택)
하지만, 공통 응답 관련 자료를 찾아보니 ResponseBodyAdvice를 사용한 예시가 많았다. ResponseBodyAdvice는 Spring에서 제공하는 인터페이스로, 컨트롤러의 반환된 응답 객체를 가로채 가공할 수 있도록 도와준다. 주로 응답 본문을 일관되게 처리하거나, 추가 정보를 삽입할 때 공통화하는 데 사용된다. 내가 의도한 대로, @RestController에서 반환한 데이터에 간단히 추가 작업을 할 수 있을 것 같아 보여 이 방법을 선택했다.
2. ResponseBodyAdvice
Spring에서 컨트롤러에서 반환된 응답 객체를 가로채 가공할 수 있도록 도와주는 인터페이스이다.
특징
- Http Body 변환 전에 처리
- 컨트롤러에서 반환된 객체가 http body로 직렬화되기 전에 호출된다.
- 즉, HttpMessageConverter에 의해 변환되기 전에 데이터를 가공할 수 있다.
- 컨트롤러 레벨에서 동작
- @RestController 또는 @ResponseBody를 사용하는 메서드의 응답에만 적용된다.
- View 렌더링을 사용하는 경우에는 적용되지 X
- 특정 컨트롤러 또는 메서드에만 적용 가능
ResponseBodyAdvice는 두가지 메서드가 존재한다.
- supports
- 이 메서드에서 true를 반환해야 beforeBodyWrite가 실행된다.
- 특정 응답에 공통 처리를 적용할지 말지 결정할 수 있다.
- 반환 타입과 메시지 컨버터 타입을 매개변수로 받아, 사용자가 유연하게 설정할 수 있다.
- beforeBodyWrite
- 컨트롤러 반환값을 가공하거나 수정할 수 있는 메서드
- 이 반환 값이 클라이언트에게 전달된다.
ResponseBodyAdvice 인터페이스의 주석을 확인해보면, 이 인터페이스를 구현하기 위해 RequestMappingHandlerAdapter와 ExceptionHandlerExceptionResolver를 사용해 구현체를 등록해야 하지만, @ControllerAdvice를 사용한다면 구현체가 자동으로 스프링에 등록된다고 한다.
3. 실습
1.
public class Message {
private HttpStatus status;
private Object data;
private String message;
private String memo;
}
- 먼저 일관된 포맷의 반환 DTO 클래스를 만들었다.
- ResponseBodyAdvice를 implements해서 두 메서드를 오버라이딩한다.
supports()
는 true로 변경해 모든 작업에서 beforeBodyWrite가 실행되도록 했다.beforeBodyWrite()
에서는 공통 작업을 추가했다. 나는 여기서 Message의 data필드에 컨트롤러 반환값을 넣어주었다.- 마지막으로
@ControllerAdvice
를 설정해준다. (ControllerAdvice의 옵션을 통해 특정 패키지에만 적용하거나, 추가 클래스 등을 설정해 줄 수 있다)
2.
// V1
@GetMapping("/summary")
public ResponseEntity<?> searchSummaryByDate(, ScheduleSearchReqDto reqDto) {
Message message = Message.builder()
.status(HttpStatus.OK)
.data(scheduleBusinessService.searchSummaryByDate(reqDto.getStartDate(), reqDto.getEndDate()))
.message("success")
.build();
return new ResponseEntity<>(message, message.getStatus());
}
// V2
@GetMapping("/summary")
public Object searchSummaryByDate(ScheduleSearchReqDto reqDto) {
return scheduleBusinessService.searchSummaryByDate(reqDto.getStartDate(), reqDto.getEndDate());
}
이제 아래 V2처럼 컨트롤러에서 서비스 레이어에 응답 값을 간단하게 반환해주기만 하면 된다.
3.
결과를 확인해보면 기존과 동일하게, Message DTO 형태로 응답이 반환되는 것을 확인할 수 있다.
만약 CustomResponseBodyAdvice를 제거하고 실행해보면, Message DTO 형태가 아닌 Service 레이어에서 반환한 값 자체로 응답이 넘어오는 것을 확인할 수 있다.
4. 예외 처리
그럼 예외가 발생한다면 어떻게 반환될까? 해당 프로젝트에서는 전역 예외처리로 @RestControllerAdvice를 사용해 각 예외마다 해당 예외 응답값을 설정해두었다.
만약 다음과 같이 설정해둔 예외를 발생시킨다면, 결과는 Message가 이중으로 생성되어 반환되는 문제가 있다.
따라서 ResponseBodyAdvice를 적용하려는 컨트롤러에만 적용되도록 변경했다.
@ControllerAdvice(
basePackages = "com.scheduler.scheduler_api.domain"
)
public class CustomResponseBodyAdvice implements ResponseBodyAdvice<Object> {
...
}
5. 마무리
개인 프로젝트에서 기존의 코드처럼 공통된 반환처리를 매번 작성해주는 경우가 있었다. 이는 새로운 API를 추가할 때마다 동일한 반환 로직을 반복적으로 작성해야 했고, 공통 코드가 서비스 메서드를 호출하는 코드와 섞여 있어 가독성이 떨어지는 문제가 있었다. 이번 ResponseBodyAdvice를 적용하면서 중복 코드를 제거해 가독성이 올라갔고, 이제 새로운 API를 추가하더라도 비효율적인 작업이 필요없을 듯하다. 유지보수 시 크게 이점이 될 것 같다. 다른 프로젝트들도 얼른 바꿔야겠다!
참고자료 😃
https://whalesbob.tistory.com/18
https://velog.io/@kylekim2123/SpringBoot-ResponseBodyAdvice를-이용한-공통-응답-처리와-관련-트러블-슈팅
'Spring' 카테고리의 다른 글
[Spring] Spring WebFlux 개념 및 실습 (1) | 2024.12.12 |
---|---|
[Spring] Apache Poi 엑셀 다운로드 개선 및 성능 확인 (+ SXSSF) (1) | 2024.12.06 |
[Spring] Log4J, Logback, Log4J2 개념 (+Logback실습) (1) | 2024.11.21 |
[Spring] SpringBoot에서 Redis 적용하기 (+부하 테스트) (8) | 2024.11.08 |
[Spring] Spring에서 동시성 이슈를 해결하는 방법 (1) | 2024.10.02 |