ResponseEntity는 왜 사용하는 것이며 @RestControllerAdvice는 무엇일까.
스프링 3.2부터 @ControllerAdvice를 이용해서 편리하게 전역으로 exception handling을 할 수 있습니다.
저도 개인적으로 전역으로 예외를 처리 설정할 때 유용하게 쓰고 있었습니다.
그런데 우연치 않게 @RestControllerAdvice라는 어노테이션을 발견해서 해당 어노테이션을 확인해 보았습니다.
확인해본 결과 @ResponseBody + @ControllerAdvice가 합쳐진 어노테이션이었습니다.
참고로 @ReponseBody는 HttpMessageConverter를 통해서 응답 값을 자동으로 json으로 직렬화 한 뒤 응답해주는 역할을 합니다.
(대표적으로 많이 사용되는 @RestController는 @Controller + @ReponseBody입니다)
순간 아차 싶었습니다.
분명 @ControllerAdvice만 사용해도 Json으로 응답을 잘 하고있었는데,,
그럼 어떻게 @ControllerAdvice는 자동으로 json으로 응답하고 있었지?
우선 확인을 위해 바로 테스트를 만들어서 실험을 진행해봤습니다.
응답 확인이 주 목적이니 다른 로직들은 제외하고 최소한의 속성 응답으로 실행을 진행하였습니다.
우선 일반적으로 사용하는 것처럼 @ControllerAdivce + ReponseEntity를 이용하여 HttpStatusCode와 함께 응답하겠습니다.
1. @ControllerAdvice + ResponseEntity
@RestController // @Controller여도 상관 없습니다.
public class HelloController {
@GetMapping("/error")
public void test() {
throw new IllegalArgumentException("hello");
}
}
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
protected ResponseEntity<?> handleIllegalArgumentException(IllegalArgumentException e) {
return new ResponseEntity<>(new ErrorResponse(LocalDateTime.now()), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@WebMvcTest
public class HelloControllerTest {
@Autowired
MockMvc mockMvc;
@Test
void exception_handle() throws Exception {
final ResultActions actions = mockMvc.perform(get("/error"));
actions
.andDo(print())
.andExpect(result -> assertThat(result.getResolvedException())
.isInstanceOf(IllegalArgumentException.class))
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(status().is5xxServerError());
}
}
테스트가 성공했습니다. 예상했던 Status 500, Response Content-Type이 Json으로 오는 걸 확인할 수 있습니다.
그럼 스프링에서는 어떻게 Json으로 응답할까요?
다른 컨버터를 등록하지 하지 않았으면 기본적으로 HttpMessageConvter의 구현체인
MappingJackson2HttpMessageConverter를 사용합니다.
호출스택은 다음과 같습니다.
MappingJackson2HttpMessageConverter는 AbstractJackson2HttpMessageConverter의 writeInternal에 의해서 호출됩니다.
다음 사진은 AbstractJackson2HttpMessageConverter의 writeInternal 메서드 입니다. 여기서 ObjectMappere등 Jackson라이브러리를 통해 Json으로 변환하는 것을 확인할 수 있습니다.
스프링 공식 문서 에서도 찾아보니 HttpEntity, ResponseEntity는 HttpMessageConverter로 컨버팅이 된다고 나옵니다.
(ResponseEntity는 HttpEntity를 상속 받았습니다(
자 그럼 이번에는 똑같이 @ControllerAdvice 진행하되, ReponseEntity로 파싱하지 않고 예외 응답 객체를 반환하겠습니다.
2. @ControllerAdvice + Object
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // (1)
protected ErrorResponse handleIllegalArgumentException(IllegalArgumentException e) {
return new ErrorResponse(LocalDateTime.now()); // ** ReponseEntity로 파싱하지 않고 응답합니다
}
}
(1) ReponseEntity로 데이터를 파싱하지 않을 경우 HTTP status codes는 기본적으로 200으로 응답하기 때문에,
이런식으로 응답 상태를 @ResponseStatus를 통해 지정해줘야 합니다.
테스트가 실패했습니다. 기대하던 500을 응답했지만, json으로 응답하지 않아서 테스트가 실패했습니다.
자 ResponseEntity를 뺏더니 json으로 응답하지 않았습니다.
그 뜻은 json으로 응답할 수 있었던 것은 @ControllerAdvice가 아닌 ReponseEntity 덕이었습니다.
이번엔 최종적으로 @ControllerAdvice에서 @RestControllerAdvice로 변경해 보겠습니다.
3. @RestControllerAdvice + Object
@Slf4j
@RestControllerAdvice // @ResponseBody + @ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
protected ErrorResponse handleIllegalArgumentException(IllegalArgumentException e) {
return new ErrorResponse(LocalDateTime.now());
}
}
정상적으로 테스트가 통과되는 걸 확인할 수 있습니다.
실험 결과를 정리하자면 예외처리 후 json으로 응답하는 방법은 다음과 같습니다.
1. @ControllerAdvice + ReponseEntity
2.@ReponseContrllerAdvice + 응답객체 + @ResponseStatus
2번 같은 경우 @ResponseStatus가 추가로 붙습니다. 이유는 위에서도 말했지만 @ResponseStatus를 사용하지 않으면 기본적으로 HttpStatusCode는200으로 응답하기 떄문입니다.
즉, ReponseEntity를 이용하면 자동으로 HttpMessageConverter를 통해서 Json으로 응답하고,응답으로 HttpStatusCode를 전달하기 때문에 @ResponseStatus를 사용하지 않아도 됩니다.
Reference
https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-return-types
'Spring' 카테고리의 다른 글
@Async는 어떤식으로 실행될까? (0) | 2021.08.23 |
---|---|
Spring AOP self-invocation이 발생하는 이유와 @Transaction 사용시 주의사항 (0) | 2021.08.11 |
EventLister를 활용한 느슨한 결합 및 이벤트 처리. (0) | 2021.07.21 |
@ConfigurationProperties를 immutable하게 설계하기 (0) | 2021.04.15 |
[Spring, OOP] 생성자 주입이 좋은 이유와 스프링을 이용한 다양한 DI (2) | 2021.02.10 |