Jackson, LocalDateTime Serialization, Deserialization 이슈

1. 들어가며

스프링에서 자바 객체를 직렬화/역직렬화를 할때, 내부적으로 Jackson을 사용하는데, 자바8에 도입된 LocalDateTime 타입으로 직렬화, 역직렬화할때 이슈가 있어서 정리해 보았다.

1.1. 직렬화/역직렬화

먼저 직렬화, 역직렬화에 대해서 알아보자. 직렬화(Serialization), 역직렬화(Deserialization)으로 자바 객체를 JSON,XML,..다른 데이터 형식으로 변환하는 것을 직렬화 그 반대를 역직렬화라고 한다.

여기서 알아볼 예제에서는 스프링에서 응답(Response)로 내보낼때 (자바 객체 -> JSON) 이를 직렬화 라고 하고, Client에서 요청에 담긴 (JSON 데이터를 -> 자바객체)로 변환하는걸 역직렬화라고 한다.

2. 상황

다음과 같은 3가지 상황을 알아보자.

  • @RequestParam 을 이용한 쿼리 스트링으로 LocalDateTime을 넘기는 상황 (역직렬화)
  • @RequestBody로 객체안의 필드의 타입이 LocalDateTime 인 상황 (역직렬화)
  • @ResponseBody로 객체를 리턴할때 객체안의 필드의 타입이 LocalDateTime 인 상황 (직렬화)

3. 예제 코드

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>

Spring Boot 버전 2.5.3버전에, web 모듈만 추가했을때, 스타터 종속성에 의하여 jackson라이브러리가 같이 추가된다.

스크린샷 2021-07-24 오후 5 45 33

간단한 Event POJO를 만든다.

@Getter
@Setter
public class Event {
    private String name;
    private LocalDateTime eventDate;
}

위의 3가지 경우를 각각 만들어 보자! (어떤한 설정도 하지 않았다)

@GetMapping("/") //1번째
public ResponseEntity<String> getParam(@RequestParam LocalDateTime currentDate) {
    log.info("currentDate = {} ", currentDate);
    return ResponseEntity.ok("SUCCESS");
} 

@PostMapping("/event") //2번째
public ResponseEntity<String> createEvent(@RequestBody Event event) {
    log.info("event = {} ", event);
    return ResponseEntity.ok("SUCCESS");
}

@GetMapping("/event") //3번째
@ReponseBody
public Event getEvent() {
    return new Event("event", LocalDateTime.now());
} 

1번째는 @RequestParam 으로 LocalDateTime 타입을 받는다.

2번째는 @RequestBody에 Event객체를 매핑해서, 역직렬화한다.

그리고 3번재는 @ResponseBody를 통해서 Event객체를 Json형식으로 직렬화 하는 예제이다.

4. 실패하는 경우?

위의 3가지 경우에서, 첫번째만 케이스만 실패한다.

예외 메세지는 다음과 같다.

Resolved [org.springframework.web.method.annotation.MethodArgumentTypeMismatchException: Failed to convert value of type ‘java.lang.String’ to required type ‘java.time.LocalDateTime’; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [@org.springframework.web.bind.annotation.RequestParam java.time.LocalDateTime] for value ‘2021-07-24T00:00:00’; nested exception is java.lang.IllegalArgumentException: Parse attempt failed for value [2021-07-24T00:00:00]]

말인 즉슨, String 타입을 LocalDateTime 타입으로 변환할 수 없다고 한다.

5. 해결책

해당 예외에는 해결책에는 두가지 방법이 존재한다.

첫번째 방법은 @DateTimeFormat 어노테이션으로 해당 데이터 타입을 지정해주는 방법이고,

두번째 방법은 String 타입으로 받아서, Controller나 Service레이어에서 String to LocalDateTime으로 파싱해서 사용하는 방법이다.

여기서 우리는 첫번째 방법을 사용한다.

해당 내용은 Stack overflow 에 있다.

@GetMapping("/event")
public ResponseEntity<String> getParam(@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime currentDate) {
  log.info("currentDate = {} ", currentDate);
  return ResponseEntity.ok("SUCCESS");
}

@DateTimeFormat은 스프링에서 제공해주는 어노테이션이다. @JsonFormat 어노테이션도 자주 등장하는데 @JsonFormat은 Jackson에서 제공하는 어노테이션이다.

6. 진짜 문제는 objectMapper를 재정의 하는 순간 ‼️‼️‼️

다음과 같이 objectmapper를 보통 싱글턴 빈으로 만들어서 사용하는데, 아무런 설정없이 빈을 만드는 경우 위의 케이스를 다시 살펴 보자.

@Configuration
public class JacksonConfig {
    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper objectMapper = new ObjectMapper();
        return objectMapper;
    }
}

기본 빈으로 objectmapper를 빈으로 설정했다.

@GetMapping("/event") //2번째 
public Event getEvent() {
    return new Event("event", LocalDateTime.now());
} 

@PostMapping("/event") //3번째
public ResponseEntity<String> createEvent(@RequestBody Event event) {
    log.info("event = {} ", event);
    return ResponseEntity.ok("SUCCESS");
}

이번에는 2번재, 3번째모두 실패한다. 예외 메세지는 다음과 같다.

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Java 8 date/time type java.time.LocalDateTime not supported by default: add Module “com.fasterxml.jackson.datatype:jackson-datatype-jsr310” to enable handling…

default는 자바8의 LocalDateTime 타입을 지원하지 않는다. 그렇기 때문에 핸들링 할 수 있게 jsr-310 모듈을 추가하라고 한다.

jsr-310?

JSR(Java Specification Request)로 자바플랫폼의 자바 사양을 정의한 문서와 같다. 이중에서 310은 날짜와 시간에 관련된 API를 정의한다.

처음에는 jsr-310 dependency 라이브러리를 추가하는 줄 알았는데, spring-boot-starter-json 스타터 의존성에 의해서 jsr310 디펜던시가 이미 추가되어있었다. 그래서 다른 방법을 찾아보았다.

@Bean
public ObjectMapper objectMapper() {
  ObjectMapper objectMapper = new ObjectMapper();
  objectMapper.registerModule(new JavaTimeModule());
  objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
  return objectMapper;
}

JavaTimeModule을 추가해주고 , DATE를 TimeStamp로 찍는 직렬화 기능을 disable로 추가해준다. 그리고 다시 요청을 보내면 성공한다.

소스 코드는 여기에서 확인 가능

7. 정리

  • 스프링 부트 2.5.3 버젼에서는 ObjectMapper 빈을 설정하지 않는다면, 기본 LocalDateTime으로 직렬화/역직렬화 문제없다.
    • 스프링 부트가 기본으로 ObjectMapper에 어떤 항목들을 default값으로 셋팅하는지는 살펴봄직하다.
  • @ReuquestParam을 LocalDateTime 타입으로 역직렬화를 하고 싶다면 @DateTimeFormat 어노테이션을 사용하자
  • ObjectMapper를 빈으로 등록했다면 JavaTimeModule을 추가하는 설정을 따로 해줘야 한다.
Jackson, LocalDateTime Serialization, Deserialization 이슈
Older post

MySQL 인덱스 종류, 내부 동작

Newer post

리눅스 Tree 명령어로 폴더 구조 보기

Jackson, LocalDateTime Serialization, Deserialization 이슈