스프링  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계층 다이어그램

 

호출스택은 다음과 같습니다.

MappingJackson2HttpMessageConverterAbstractJackson2HttpMessageConverter의 writeInternal에 의해서 호출됩니다.

 

다음 사진은 AbstractJackson2HttpMessageConverterwriteInternal 메서드 입니다.  여기서 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