사용자 도구

사이트 도구


springframework:springboot:json

SpringBoot and JSON

날짜 시간 처리 Date Time Format

  • Java의 기존 java.util.Date와 신규 Java 8 시간에 대한 처리
  • java.util.Date는 기본적으로 Unix timestamp(long 값)으로 표현된다.

기본 설정

  • SpringBoot 2.x 는 starter 추가 하면 Jackson ObjectMapper 자동 생성 설정
    api 'org.springframework.boot:spring-boot-starter-json'
  • 구버전 SpringBoot 에서는 의존성에 다음 추가해야 JSR310 날짜/시간 지원됨.
    compile('com.fasterxml.jackson.datatype:jackson-datatype-jsr310')
    compile('com.fasterxml.jackson.datatype:jackson-datatype-jdk8')
  • 예제 컨트롤러
    @GetMapping("/dateTest")
    public MyClock dateTest2() {
        return new MyClock();
    }
     
    @Getter
    public static class MyClock {
        private Date date;
        private LocalDateTime localDateTime;
        private LocalDate localDate;
        private LocalTime localTime;
        private OffsetDateTime offsetDateTime;
        private ZonedDateTime zonedDateTime;
     
        public MyClock() {
            Instant instant = Instant.now();
            date = Date.from(instant);
            localDateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
            localTime = localDateTime.toLocalTime();
            localDate = localDateTime.toLocalDate();
            offsetDateTime = OffsetDateTime.ofInstant(instant, ZoneId.systemDefault());
            zonedDateTime = ZonedDateTime.ofInstant(instant, ZoneId.systemDefault());
        }
    }

WRITE_DATES_AS_TIMESTAMPS=false 사용

  • 제일 좋은 것은 처음부터 WRITE_DATES_AS_TIMESTAMPS=false 상태로 운영하는 것이다. application.yml
    spring.jackson.serialization.write-dates-as-timestamps: false
  • SpringBoot 2.x 는 이값이 기본 false 이고 Java 8 date/time 에 대해 자동으로 ISO 포맷을 사용하게 한다. simpleDateFormat만 따로 ISO와 동일하게 설정해주면 된다.
    • simpleDateFormat(), dateFormat()을 설정하면 ObjectMapper가 non-thread-safe 하게 돼 버린다. 하지말고, java.util.Date도 사용하지 말 것.
  • 이렇게 하면 java.util.Date,java.time.* 모두 ISO 혹은 그 유사 포맷으로 직렬화된다.
    {  
       "date":"2018-07-18T06:16:33.647+0000", ## java.util.Date가 약간 다르게 출력됨.
       "localDateTime":"2018-07-18T15:16:33.647",
       "localDate":"2018-07-18",
       "localTime":"15:16:33.647",
       "offsetDateTime":"2018-07-18T15:16:33.647+09:00",
       "zonedDateTime":"2018-07-18T15:16:33.647+09:00"
    }
  • 그러나 WRITE_DATES_AS_TIMESTAMPS 옵션을 변경할 경우 기존에 작동하던 java.util.Date의 포맷이 변경되므로 프로젝트가 이미 운영중일 때는 java.util.Date를 받는 모든 부분에 대해 기존 unix timestamp 받던것을 올바로 처리하게 변경해야만 한다.
  • Formatter와 Jackson Serializers/Deserializers 에 대해서 테스트를 만들어 보증하는 것이 좋다. FormatterSerializerTestControllerTest.groovy

모든 타입에 대해 일관성 있는 커스텀 형식 지원

  • 만약, 모든 날짜 객체에 대해 일관성있는 포맷을 지정하고자 한다면(ISO 포맷) 다음과 같이 한다.
    @SpringBootApplication
    public class DemoApplication implements Jackson2ObjectMapperBuilderCustomizer {
     
        public static void main(String[] args) {
            SpringApplication.run(DemoApplication.class, args);
        }
     
        // TODO : 아래 예에서는 deserializer가 빠져있다!! deserializer도 추가해줘야 일관성있게 된다.
        // ISO 포맷을 사용할경우 SpringBoot 2 에서 기본적으로 ISO Format으로 세팅하기 때문에 굳이 복잡하게 설정할 필요없이
        // java.util.Date 에 대해서만 일관성있게 설정한다.
        @Override
        public void customize(Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder) {
            LocalDateTimeSerializer localDateTimeSerializer = new LocalDateTimeSerializer(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
    //        LocalDateSerializer localDateSerializer = new LocalDateSerializer(DateTimeFormatter.ISO_LOCAL_DATE);
    //        LocalTimeSerializer localTimeSerializer = new LocalTimeSerializer(DateTimeFormatter.ISO_LOCAL_TIME);
     
    //        OffsetDateTimeSerializer offsetDateTimeSerializer = new CustomOffsetDateTimeSerializer();
    //        ZonedDateTimeSerializer zonedDateTimeSerializer = new CustomZonedDateTimeSerializer();
     
            jacksonObjectMapperBuilder
                    .timeZone(TimeZone.getDefault()) // 올바른 타임존을 설정해야 offset/zoned datetime이 올바로 설정됨.
                    .locale(Locale.getDefault())        
                    .simpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
    //                .serializerByType(LocalDateTime.class, localDateTimeSerializer)
    //                .serializerByType(LocalDate.class, localDateSerializer)
    //                .serializerByType(LocalTime.class, localTimeSerializer)
    //                .serializerByType(OffsetDateTime.class, offsetDateTimeSerializer)
    //                .serializerByType(ZonedDateTime.class, zonedDateTimeSerializer);
        }
     
        // 불필요
        public static class CustomOffsetDateTimeSerializer extends OffsetDateTimeSerializer {
            protected CustomOffsetDateTimeSerializer() {
                super(OffsetDateTimeSerializer.INSTANCE, false, DateTimeFormatter.ISO_OFFSET_DATE_TIME);
            }
        }
        // 불필요
        public static class CustomZonedDateTimeSerializer extends ZonedDateTimeSerializer {
            public CustomZonedDateTimeSerializer() {
                // ISO_OFFSET_DATE_TIME 로 바꾸면 OffsetDateTime과 동일하게 출력됨.
                super(ZonedDateTimeSerializer.INSTANCE, false, DateTimeFormatter.ISO_DATE_TIME, true);
            }
        }
    }
  • 위의 경우 출력이 모두 일관성있게 나온다.
    {  
       "date":"2018-07-18T15:11:49.693+09:00",
       "localDateTime":"2018-07-18T15:11:49.693",
       "localDate":"2018-07-18",
       "localTime":"15:11:49.693",
       "offsetDateTime":"2018-07-18T15:11:49.693+09:00",
       "zonedDateTime":"2018-07-18T15:11:49.693+09:00"
    }
  • DateTimeForamtter.ISO_ZONED_DATE_TIME은 표준이 아니다. jackson은 ZonedDateTime에 대해 기본으로 ISO_OFF_SET_DATE_TIME을 사용한다.

Jackson2ObjectMapperBuilder 사용시 Module 추가

  • Jackson2ObjectMapperBuilder 사용시 모듈을 추가하려면 modules 메소드 혹은 modulesToInstall를 사용한다.
  • modules(…) : Spring과 Jackson default 모듈 탐색을 모두 취소하고 오직 이 메소드 인자로 전달된 모듈만 탑재
  • modulesToInstall(…) : Spring(JSR-310 or jodatime 등 자동추가)과 Jackson default 모듈 탑재를 수행하고 그 뒤에 명시된 모듈을 추가 탑재

@EnableWebMvc

@EnableWebMvcWebMvcConfigurerAdapter를 사용하는 순간 더이상 SpringBoot가 아니고 Spring 이기 때문에 위의 설정이 먹지 않게 된다. SpringBoot and Spring MVC 참조 Spring Boot Issue 2116

이때는 WebMvcConfigurerAdapter#configureMessageConverters를 override 해야한다.

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
       super.configureMessageConverters(converters);
       converters.add(new MappingJackson2HttpMessageConverter(jackson2ObjectMapperBuilder().build()));
    }
 
    @Bean
    public Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder() {
       return new Jackson2ObjectMapperBuilder()
               .failOnUnknownProperties(false) // SpringBoot default
               .featuresToDisable(MapperFeature.DEFAULT_VIEW_INCLUSION) // SpringBoot default
               .featuresToEnable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) // SpringBoot default
               .serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ISO_LOCAL_DATE_TIME))
               .serializerByType(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ISO_DATE));
    }
}

Controller Get Parameter 에 대한 포맷지원

이것은 Jackson JSON 직렬화와는 다른 문제이므로 포맷을 지정해야한다.

@GetMapping("/test")
public Result test(@RequestParam("datetime") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime datetime) {
    // ...
}
// 파라미터를 ?datetime=2018-07-11T20:22:55.123 형태로 호출

@JsonComponent

WebFlux Jackson 설정

@Configuration
public class Config implements WebFluxConfigurer, Jackson2ObjectMapperBuilderCustomizer {
    @Override
    public void customize(Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder) {
        jacksonObjectMapperBuilder
                .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
                .timeZone(TimeZone.getDefault())
                .locale(Locale.getDefault())
                .simpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
    }
}

기타 참조

springframework/springboot/json.txt · 마지막으로 수정됨: 2020/11/23 19:00 저자 kwon37xi