[Spring] LocalDate 타입 직렬화/역직렬화 적용하기

klmin·2024년 10월 18일
0

Spring에서 LocalDate 및 LocalDateTime 처리

  • Spring에서 Java 8 날짜/시간 API인 LocalDate, LocalDateTime을 처리할 때는 Jackson의 ObjectMapper를 사용하여 JSON 직렬화/역직렬화를 수행한다.

  • 하지만 기본적으로 JavaTimeModule이 등록되지 않으면 LocalDate와 같은 객체를 처리할 수 없으므로 별도의 설정이 필요하다.

JSON 직렬화/역직렬화 처리

  • @RequestBody, @ResponseBody, RestTemplate, RestClient, WebClient는 Jackson의 ObjectMapper를 사용하여 JSON 직렬화/역직렬화를 처리한다.
  • Spring은 이 과정에서 MappingJackson2HttpMessageConverter를 사용하여 JSON 데이터를 변환하며 이때 ObjectMapper가 사용된다.

파라미터 및 폼 데이터 처리

  • @ModelAttribute@RequestParamConversionServiceFormatter를 통해 요청 파라미터나 폼 데이터를 처리한다.
    이를 통해 문자열 데이터를 LocalDate나 LocalDateTime 객체로 변환할 수 있다.

아무런 설정을 하지 않고 objectMapper.convertValue에 LocalDate 데이터를 넣었더니 이런식으로 오류가 발생한다.

  • LocalDate 파싱 에러 로그

해결방법

LocalDate, LocalDateTime 등을 처리하기 위한 몇 가지 방법이 있다.

변수에 직접 @JsonFormat과 pattern을 사용해 적용하는 방법이다.
하지만 이렇게하면 변수마다 지정을 해줘야 한다.

  1. @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
  1. @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

  1. 전역 설정 - 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가 우선순위로 적용되어 설정이 적용되지 않는다. 삭제하고 다시 넣어줘야 한다.
  • LocalDate 파싱 에러 로그

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과 동일하게 기본설정을 삭제해줘야 한다.
  • LocalDate 파싱 에러 로그

// 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를 등록해준다.
  • LocalDate 파싱 에러 로그
// 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();

    }

}

상황에 따라 전역설정을 활용하는게 좋을것 같다.

profile
웹 개발자

0개의 댓글