Spring에서 Java 8 날짜/시간 API인 LocalDate, LocalDateTime을 처리할 때는 Jackson의 ObjectMapper를 사용하여 JSON 직렬화/역직렬화를 수행한다.
하지만 기본적으로 JavaTimeModule이 등록되지 않으면 LocalDate와 같은 객체를 처리할 수 없으므로 별도의 설정이 필요하다.
아무런 설정을 하지 않고 objectMapper.convertValue에 LocalDate 데이터를 넣었더니 이런식으로 오류가 발생한다.
LocalDate, LocalDateTime 등을 처리하기 위한 몇 가지 방법이 있다.
변수에 직접 @JsonFormat과 pattern을 사용해 적용하는 방법이다.
하지만 이렇게하면 변수마다 지정을 해줘야 한다.
- @JsonFormat과 @DateTimeFormat 사용
- @RequestParam, @ModelAttribute: @DateTimeFormat을 사용하여 처리한다.
- @RequestBody, @ResponseBody: @JsonFormat, @DateTimeFormat을 사용하며, @JsonFormat이 우선 적용된다.
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
private LocalDate createDate; - json
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate createDate; - json, model, requestparam
- @JsonSerialize와 @JsonDeserialize 적용
- 직렬화 및 역직렬화 클래스를 만들어 개별 변수에 적용할 수 있다.
public class CustomJsonSerializer {
private CustomJsonSerializer(){
}
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateUtil.getDateTimeFormatter("yyyy-MM-dd HH:mm:ss");
private static final DateTimeFormatter DATE_FORMATTER = DateUtil.getDateTimeFormatter("yyyy-MM-dd");
private static final DateTimeFormatter TIME_FORMATTER = DateUtil.getDateTimeFormatter("HH:mm");
public static class LocalDateSerializer extends JsonSerializer<LocalDate> {
@Override
public void serialize(LocalDate localDate, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString(DATE_FORMATTER.format(localDate));
}
}
public static class LocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
@Override
public void serialize(LocalDateTime localDate, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString(DATE_TIME_FORMATTER.format(localDate));
}
}
public static class LocalTimeSerializer extends JsonSerializer<LocalTime> {
@Override
public void serialize(LocalTime localTime, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString(TIME_FORMATTER.format(localTime));
}
}
}
public class CustomJsonDeserializer {
private CustomJsonDeserializer(){
}
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateUtil.getDateTimeFormatter("yyyy-MM-dd HH:mm:ss");
private static final DateTimeFormatter DATE_FORMATTER = DateUtil.getDateTimeFormatter("yyyy-MM-dd");
private static final DateTimeFormatter TIME_FORMATTER = DateUtil.getDateTimeFormatter("HH:mm");
public static class LocalDateDeserializer extends JsonDeserializer<LocalDate> {
@Override
public LocalDate deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
String dateString = jsonParser.getText();
return LocalDate.parse(dateString, DATE_FORMATTER);
}
}
public static class LocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
@Override
public LocalDateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
String dateString = jsonParser.getText();
return LocalDateTime.parse(dateString, DATE_TIME_FORMATTER);
}
}
public static class LocalTimeDeserializer extends JsonDeserializer<LocalTime> {
@Override
public LocalTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
String dateString = jsonParser.getText();
return LocalTime.parse(dateString, TIME_FORMATTER);
}
}
}
@JsonSerialize(using = LocalDateSerializer.class)
@JsonDeserialize(using = LocalDateDeserializer.class)
private LocalDate createDate; - json
- 전역 설정 - JavaTimeModule 등록
- Jackson-datatype-jsr310 모듈을 추가하여 LocalDate, LocalDateTime과 같은 시간 객체를 전역적으로 처리할 수 있다.
// build.gradle
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
@ModelAttribute, @RequestParam 전역적용
public class LocalDateFormatter implements Formatter<LocalDate> {
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
@Override
public LocalDate parse(String text, Locale locale) {
return LocalDate.parse(text, formatter);
}
@Override
public String print(LocalDate object, Locale locale) {
return object.format(formatter);
}
}
public class LocalDateTimeFormatter implements Formatter<LocalDateTime> {
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public LocalDateTime parse(String text, Locale locale) {
return LocalDateTime.parse(text, formatter);
}
@Override
public String print(LocalDateTime object, Locale locale) {
return object.format(formatter);
}
}
// @ModelAttribute, @RequestParam 적용
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addFormatter(new LocalDateFormatter());
registry.addFormatter(new LocalDateTimeFormatter());
}
}
@RequestBody, @ResponseBody 적용
// LocalDateSerializer, LocalDateTimeSerializer은 jsr에서 제공해준다.
@Configuration
public class ObjectMapperConfig {
private final String DATE_FORMAT = "yyyy-MM-dd";
private final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
@Bean
public ObjectMapper objectMapper(){
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DATE_FORMAT)));
javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DATE_FORMAT)));
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT)));
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT)));
return new ObjectMapper().registerModule(javaTimeModule);
}
}
// 커스텀 클래스 생성
public class CustomJsonSerializer {
private CustomJsonSerializer(){
}
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateUtil.getDateTimeFormatter("yyyy-MM-dd HH:mm:ss");
private static final DateTimeFormatter DATE_FORMATTER = DateUtil.getDateTimeFormatter("yyyy-MM-dd");
private static final DateTimeFormatter TIME_FORMATTER = DateUtil.getDateTimeFormatter("HH:mm");
public static class LocalDateSerializer extends JsonSerializer<LocalDate> {
@Override
public void serialize(LocalDate localDate, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString(DATE_FORMATTER.format(localDate));
}
}
public static class LocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
@Override
public void serialize(LocalDateTime localDate, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString(DATE_TIME_FORMATTER.format(localDate));
}
}
public static class LocalTimeSerializer extends JsonSerializer<LocalTime> {
@Override
public void serialize(LocalTime localTime, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString(TIME_FORMATTER.format(localTime));
}
}
}
public class CustomJsonDeserializer {
private CustomJsonDeserializer(){
}
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateUtil.getDateTimeFormatter("yyyy-MM-dd HH:mm:ss");
private static final DateTimeFormatter DATE_FORMATTER = DateUtil.getDateTimeFormatter("yyyy-MM-dd");
private static final DateTimeFormatter TIME_FORMATTER = DateUtil.getDateTimeFormatter("HH:mm");
public static class LocalDateDeserializer extends JsonDeserializer<LocalDate> {
@Override
public LocalDate deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
String dateString = jsonParser.getText();
return LocalDate.parse(dateString, DATE_FORMATTER);
}
}
public static class LocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
@Override
public LocalDateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
String dateString = jsonParser.getText();
return LocalDateTime.parse(dateString, DATE_TIME_FORMATTER);
}
}
public static class LocalTimeDeserializer extends JsonDeserializer<LocalTime> {
@Override
public LocalTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
String dateString = jsonParser.getText();
return LocalTime.parse(dateString, TIME_FORMATTER);
}
}
}
// ObjectMapper 설정
@Bean
public ObjectMapper objectMapper(){
JavaTimeModule module = new JavaTimeModule();
module.addSerializer(LocalDate.class, new CustomJsonSerializer.LocalDateSerializer());
module.addDeserializer(LocalDate.class, new CustomJsonDeserializer.LocalDateDeserializer());
module.addSerializer(LocalDateTime.class, new CustomJsonSerializer.LocalDateTimeSerializer());
module.addDeserializer(LocalDateTime.class, new CustomJsonDeserializer.LocalDateTimeDeserializer());
module.addSerializer(LocalTime.class, new CustomJsonSerializer.LocalTimeSerializer());
module.addDeserializer(LocalTime.class, new CustomJsonDeserializer.LocalTimeDeserializer());
return new ObjectMapper().registerModule(module);
}
RestTemplate
- 기본 MappingJackson을 remove하고 다시 적용해줘야 한다.
기존 설정에 restTemplate.getMessageConverters().add(converter); 만 하면 기본적으로 들어가있는 messageConverter가 우선순위로 적용되어 설정이 적용되지 않는다. 삭제하고 다시 넣어줘야 한다.
restTemplate.getMessageConverters().add(converter); 시에 기본 설정 MappingJacksonConverter 뒤에 추가됨.
기본 설정 MappingJacksonConverter
기본설정 MappingJacksonCoveter 삭제 후 새로 입력
@Bean
public RestTemplate restTemplate(ObjectMapper objectMapper){
RestTemplate restTemplate = new RestTemplate();
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setObjectMapper(objectMapper);
converter.setSupportedMediaTypes(List.of(MediaType.APPLICATION_JSON));
restTemplate.getMessageConverters().removeIf(MappingJackson2HttpMessageConverter.class::isInstance);
restTemplate.getMessageConverters().add(converter);
return restTemplate;
}
RestClient
- 기본 MappingJackson을 remove하고 다시 적용해줘야 한다.
RestTemplate과 동일하게 기본설정을 삭제해줘야 한다.
// RestClient 설정
@Bean
public RestClient restClient(ObjectMapper objectMapper){
return RestClient.builder()
.messageConverters(
converters -> {
converters.removeIf(MappingJackson2HttpMessageConverter.class::isInstance);
converters.add(new MappingJackson2HttpMessageConverter(objectMapper));
})
.build();
}
WebClient
- ObjectMapper를 등록해준다.
// WebClient 설정
@Configuration
public class WebClientConfig {
@Bean
public WebClient webClient(ObjectMapper objectMapper) {
return WebClient.builder()
.codecs(configurer -> {
configurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(objectMapper));
configurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(objectMapper));
})
.build();
}
}
상황에 따라 전역설정을 활용하는게 좋을것 같다.