직렬화 예외 처리
public record RequestCreateReservation(
@NotBlank(message = "이름은 필수 입력사항 입니다.")
String name,
@NotNull(message = "날짜는 필수 입력사항 입니다.")
@DateTimeFormat(pattern = "yyyy-MM-dd")
LocalDate date,
@NotNull(message = "시간은 필수 입력사항 입니다.")
@DateTimeFormat(pattern = "HH-mm")
LocalTime time
)
- 예약 정보 생성을 요청하는 과정에서 사용되는 DTO이다.
- DateTimeFormat 어노테이션을 통해 HTTP 요청 메시지 바디에 있는 날짜 데이터를 LocalDate와 LocalTime으로 직렬화를 한다.
- 만약, DateTimeFormat 어노테이션에 설정된 패턴대로 데이터가 들어오지 않으면 어떻게 될까?


- 다음과 같은 예외가 발생한다.
- Spring MVC Request Lifecycle을 보면, Handler Adapter가 클라이언트의 요청을 처리할 컨트롤러 메소드를 호출한다.
- 이 과정에서 Argument resolver를 호출한다.
- Argument resolver은 컨트롤러 메소드의 파라미터에 작성된 어노테이션을 보고, 적절한 데이터를 생성한다. 이때, HttpMessageConverter를 사용한다.
- 현재 발생한 예외는 HttpMessageConverter가 직렬화하는 과정에서, 날짜 데이터가 설정된 포맷과 다르기 때문에 예외가 발생한 것이다.
- 응답 메시지로 400을 받지만, 400인 이유를 명확하게 알려준다면 클라이언트 입장에서도 어떤 곳이 잘못됐는지 알 수 있다.
예외 핸들러
- 스프링에서는 발생한 예외를 핸들링하는 핸들러를 어노테이션을 통해 지정할 수 있다.
- ControllerAdvice, RestControllerAdvice : 예외를 핸들링하는 핸들러를 지정해주는 어노테이션이다.
- ExceptionHandler : 핸들러 메소드에서 어떤 예외를 핸들링할 것인지 지정해주는 어노테이션이다.
- 어노테이션을 통해 핸들러를 지정하고, 예외를 핸들링하는 이유는 무엇일까?
- try-catch를 통해 예외를 잡는 방법도 존재하지만, 예외가 발생할 수 있는 곳에 모두 try-catch를 작성한다면 코드가 더러워진다.
- 하나의 핸들러에서 예외를 핸들링을 하면 try-catch를 작성하지 않아도 되고, 메인 로직만 신경쓸 수 있다.
구현
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<Object> httpMessageNotReadableException(HttpMessageNotReadableException e) {
//
}
}
- 위에서 언급한 내용을 바탕으로 다음과 같은 클래스와 메소드를 생성했다.
- 현재 발생한 예외는
HttpMessageNotReadableException이므로, ExceptionHandler 어노테이션을 통해 해당 예외를 핸들링 한다.

- 메소드에 브레이크 포인트를 걸고, 디버깅 모드로 애플리케이션을 실행시킨다.
- 이후, 잘못된 데이터를 보내면 핸들러에서 예외고 잡아 브레이크 포인트에서 멈춘다.
- 사진을 통해 알 수 내용은 다음과 같다.
- e.cause._value을 통해 예외를 발생시킨 값을 확인할 수 있다.
- e.cause._path을 통해 예외를 발생시킨 값의 경로를 확인할 수 있다.
InvalidFormatException가HttpMessageNotReadableException에 감싸져서 온 것을 확인할 수 있다.
undefined
InvalidFormatException를 확인하면,MismatchedInputException를 상속받음을 알 수 있다.MismatchedInputException예외도 다른 클래스를 상속받고 있음을 알 수 있었다.- _path를 가져오기 위해서는 발생한 예외에서 InvalidFormatException를 추출해야 한다.
instanceof연산자를 통해 가져올 수 있다.- instanceof : 객체 타입을 확인하는 연산자이다.
e.getCause() instanceof 클래스를 통해 객체의 인스턴스가 주어진 클래스와 일치하면 반환한다고 한다.
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<Object> httpMessageNotReadableException(HttpMessageNotReadableException e) {
if (e.getCause() instanceof MismatchedInputException mismatchedInputException) {
final String errorMessage = mismatchedInputException.getPath().get(0).getFieldName() + " 필드의 값이 잘못되었습니다.";
return ResponseEntity.badRequest().body(errorMessage);
} else {
return ResponseEntity.badRequest().body("확인할 수 없는 데이터가 들어왔습니다.");
}
}
- 위에서 언급한 내용을 바탕으로 작성한 코드이다.
- e.getCause()를 instanceof 연산자를 통해 MismatchedInputException의 클래스임을 확인하는 이유는 다음과 같다.
- jackson-databind 라이브러리의 예외는 대부분 MismatchedInputException 클래스를 상속받는다.
- 하위 클래스에 해당하는 코드를 하나하나 작성하는 것보다, 부모 클래스 하나로 처리하는 것이 좋다고 판단되어 MismatchedInputException 클래스를 사용했다.