Spring @PathVariable에 대한 에러 처리는 어떻게 할까

@RequestMapping(method = RequestMethod.GET, value = "/shuttle/{number}")
public ResponseEntity shuttle(@PathVariable int number) {
    //
}

뭐 이런 API를 만든다고 했을 때 스프링으로 number에 해당하는 부분을 손쉽게 타입 변환까지 거쳐서 int로 받아 사용할 수 있다. 그런데, 만약 입력한 값이 int 타입이 아니라면 무슨 일이 생길까. 스프링은 무슨 값이 오던 int 타입으로 타입 변환을 시도할 것이고 에러가 발생한다. 그리고 응답은 400으로 나가며 본문의 메시지는 비어있다. 깔끔한 API라고 보긴 어렵다. 뭔가 에러 상황에서는 왜 그런 에러가 발생했는지 클라이언트가 알기 쉽도록 안내해 주는게 좋은 API일 것이다. 그렇다면 무슨 에러가 발생하며 어떻게 처리할 수 있나 알아보자.

@Test
public void testMethodArgumentTypeMismatchException() throws Exception {
    mockMvc.perform(get("/shuttle/aaa"))
            .andDo(print())
            .andExpect(status().isBadRequest());
}

테스트 코드다. 실행하면 콘솔에서 이런 내용을 확인할 수 있다.

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /shuttle/aaa
       Parameters = {}
          Headers = {}

Handler:
             Type = me.whiteship.web.ShuttleController
           Method = public org.springframework.http.ResponseEntity me.whiteship.web.ShuttleController.shuttle(int)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = org.springframework.web.method.annotation.MethodArgumentTypeMismatchException

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 400
    Error message = null
          Headers = {}
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

뭐 이미 답이 다 이 안에 있다. 우선 응답이 썩 사용자에 친화적이지 않다는 것을 알 수 있고, 어떤 에러가 발생했는지도 알 수 있다. 그걸 잡아서 처리하면 되겠다.

@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
    return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
}

이런식으로 MATME를 잡아서 처리하는 핸들러 코드를 추가하면 원하는 메시지를 응답으로 담을 수 있다. 그냥 getMessage()를 꺼내서 보내는 것보다는 좀 더 손을 대야겠지만 그건 뭐 취향으로.. 그리고 이걸 해보며 드는 생각은 이렇다.

  • 이 예외 처리기는 공용으로 사용하기엔 뭔가 너무 일반화되어 있다는 느낌이 든다. 아무래도 컨트롤러 안에 두는게 좋겠다.
  • 저 예외 클래스가 제공하는 메서드중 getName()과 getParameter()를 잘 사용한다면 어쩌면 @ControllerAdvice에 둘 수도 있을지 모르겠다.
  • 타입이 String인 변수에 @PathVariable을 사용했다면 이 에러를 잡을 일이 없을 것이다.