개요.

서비스 레이어 단에서 오브젝트 매퍼를 지속적으로 초기화해주어 사용하고 있었다. 같은 팀의 팀원분이 오브젝트 매퍼를 재사용하지 않고 매번 만들어 사용하면 비용이 크다고 한다. 그래서 일반적으로 빈(싱글톤) 으로 등록하여 사용한다고 한다.

 

사실 싱글톤을 사용하는게 좋았지만 한가지 걸리는 사항이 있었다. LocalDateTIme, LocalDate, LocalTime 등과 같은 시간에 대한 매퍼 변환이 문제였다. 이것저것 알아본 결과 간단한 설정을 통해 위의 문제를 해결할 수 있었다.

 

총 세개의 내용을 확인할 것이다.

  • HTTP Get Method 에 쿼리스트링으로 LocalDateTime 의 스트링 값이 들어오는 경우
  • HTTP Get Method 에 반환 JSON 값에 LocalDateTime 이 들어간 경우
  • HTTP Post Method 에 Http Body 값에 LocalDateTime 이 들어간 경우

 

오브젝트 맵퍼 빈 설정

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

 

Controller :: LocalDateTIme 을 테스트 용도

@RestController
@RequestMapping("test-api")
@Slf4j
public class TestController {

    @GetMapping
    public ResponseEntity<TestDto.Response> getTestDto(@DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") @RequestParam(value = "startDate", required = false) LocalDateTime startDate,
                                                       @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") @RequestParam(value = "endDate", required = false) LocalDateTime endDate) {

        log.debug("Get Mapping - startDate : {}", startDate);
        log.debug("Get Mapping - endDate : {}", endDate);

        return ResponseEntity.ok()
                .body(TestDto.Response.builder()
                .localDateTime(LocalDateTime.now())
                .build());

    }

    @PostMapping
    public ResponseEntity<TestDto.Response> postTestDto(@RequestBody TestDto.Request request){

        log.debug("Post Mapping - startDate : {}", request.getStartDate());
        log.debug("Post Mapping - endDate : {}", request.getEndDate());

        return ResponseEntity.ok()
                .body(TestDto.Response.builder()
                .localDateTime(LocalDateTime.now())
                .build());
    }
}

 

TestDto :: ReponseBody 로 반환하기 위한 Dto

public class TestDto {

    @Getter
    public static class Response{

        LocalDateTime localDateTime;
        LocalDate localDate;
        LocalTime localTime;

        @Builder
        public Response(LocalDateTime localDateTime) {
            this.localDateTime = localDateTime;
            this.localDate = localDateTime.toLocalDate();
            this.localTime = localDateTime.toLocalTime();
        }
    }

    @Getter
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    public static class Request{

        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss")
        LocalDateTime startDate;

        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss")
        LocalDateTime endDate;

    }
}

 

 

1. Get Method 수행 및 결과 (결과값이 장황함)

curl -X GET --header 'Accept: application/json' 'http://localhost:8080/masterpiece-club-dev/v1.0/test-api'
{
  "localDateTime": {
    "dayOfMonth": 25,
    "dayOfWeek": "WEDNESDAY",
    "dayOfYear": 268,
    "month": "SEPTEMBER",
    "year": 2019,
    "monthValue": 9,
    "hour": 22,
    
    (중략) ... 복잡하게 막 나온다.

 

 

2. Get Method 쿼리스트링 수행 및 결과 (정상수행)

curl -X GET --header 'Accept: application/json' 'http://localhost:8080/masterpiece-club-dev/v1.0/test-api?startDate=2019-09-25T11:11:11&endDate=2019-09-30T11:11:11'
TestController  : Get Mapping - startDate : 2019-09-25T11:11:11
TestController  : Get Mapping - endDate : 2019-09-30T11:11:11

따로 매퍼가 수행하는 것이 아니며, @DateTimeFormat 덕분에 정상적으로 출력됨을 확인할 수 있다.

 

 

3. Post Method 수행 및 Request Body 결과 (에러발생)

curl -X POST --header 'Content-Type: application/json' --header 'Accept: application/json' -d '{ \ 
 "endDate" : "2019-01-01T15:15:15", \ 
 "startDate" : "2019-05-15T15:15:15" \ 
 }' 'http://localhost:8080/masterpiece-club-dev/v1.0/test-api'
{
  "timestamp": 1569417952439,
  "status": 500,
  "error": "Internal Server Error",
  "message": "Type definition error: [simple type, class java.time.LocalDateTime]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `java.time.LocalDateTime` (no Creators, like default construct, exist): no String-argument constructor/factory method to deserialize from String value ('2019-01-01T15:15:15')\n at [Source: (PushbackInputStream); line: 2, column: 13] (through reference chain: com.club.masterpiece.web.test.TestDto$Request[\"endDate\"])",
  "trace": "org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class java.time.LocalDateTime]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `java.time.LocalDateTime` (no Creators, like default construct, exist): no String-argument constructor/factory method to deserialize from String value ('2019-01-01T15:15:15')\n at [Source: (PushbackInputStream); line: 2, column: 13] (through reference chain: com.club.masterpiece.web.test.TestDto$Request[\"endDate\"])\r\n\tat org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:242)\r\n\tat org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.read(AbstractJackson2HttpMessageConverter.java:227)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters(AbstractMessageConverterMethodArgumentResolver.java:204)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.readWithMessageConverters(RequestResponseBodyMethodProcessor.java:157)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:130)\r\n\tat org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:127)\r\n\tat org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:167)\r\n\tat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:134)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:104)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:892)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797)\r\n\tat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)\r\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1039)\r\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942)\r\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1005)\r\n\tat org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:908)\r\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:660)\r\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:882)\r\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:741)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\r\n\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\r\n\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\r\n\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:92)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\r\n\tat org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:93)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\r\n\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:200)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\r\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)\r\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)\r\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:526)\r\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)\r\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)\r\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)\r\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)\r\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:408)\r\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)\r\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:860)\r\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1587)\r\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)\r\n\tat java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)\r\n\tat java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)\r\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)\r\n\tat java.lang.Thread.run(Thread.java:748)\r\nCaused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `java.time.LocalDateTime` (no Creators, like default construct, exist): no String-argument constructor/factory method to deserialize from String value ('2019-01-01T15:15:15')\n at [Source: (PushbackInputStream); line: 2, column: 13] (through reference chain: com.club.masterpiece.web.test.TestDto$Request[\"endDate\"])\r\n\tat com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)\r\n\tat com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1452)\r\n\tat com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1028)\r\n\tat com.fasterxml.jackson.databind.deser.ValueInstantiator._createFromStringFallbacks(ValueInstantiator.java:371)\r\n\tat com.fasterxml.jackson.databind.deser.std.StdValueInstantiator.createFromString(StdValueInstantiator.java:323)\r\n\tat com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromString(BeanDeserializerBase.java:1373)\r\n\tat com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:171)\r\n\tat com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:161)\r\n\tat com.fasterxml.jackson.databind.deser.impl.FieldProperty.deserializeAndSet(FieldProperty.java:138)\r\n\tat com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:288)\r\n\tat com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:151)\r\n\tat com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4014)\r\n\tat com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3085)\r\n\tat org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:239)\r\n\t... 55 more\r\n",
  "path": "/masterpiece-club-dev/v1.0/test-api"
}

500 에러를 만났고, 내용을 살펴보면 들어오는 String 값에 대해서 역직렬화 할 수 없다라는 내용이다. 오브젝트 매퍼를 단순 빈 등록만 해두어서 저렇게 에러가 발생하였다.

 

  • AbstractMessageConverterMethodProcessor 클래스 내부
  • writeWithMessageConverters() 메소드 내부
  • for (HttpMessageConverter<?> converter : this.messageConverters) { ... }  영역 안에서 문제가 발생한다.
  • 여기서 Http 바디 값을 자바 Pojo 로 역직렬화 하는데 있어서 문제 발생인데, 이 부분에 대해서 추가적인 공부가 필요하다. 또한 HttpMessageConverter 에는 미리 등록된 디폴트 컨버터가 꽤나 많다. for 구문을 순회하면서 적당한 컨버터를 사용하고 있는듯 하다.

 

결론, 오브젝트 매퍼 빈 설정 

@Configuration
public class ObjectMapperConfiguration {

    @Bean
    public ObjectMapper objectMapper() {

        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new JavaTimeModule());
        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

        return mapper;
    }
}

 

위의 내용으로 변경해주고, 앞선 내용들을 수행하면 정상적으로 수행되고 결과를 제대로 확인할 수 있다.

WRITE_DATES_AS_TIMESTAMPS 는 날짜를 JSON 에서 문자열로 표시하도록 나타내고 있다.

 

 

1. Get Method 수행 및 결과

{
  "localDateTime": "2019-09-25T22:30:57.11",
  "localDate": "2019-09-25",
  "localTime": "22:30:57.11"
}

3. Post Method 수행 및 Request Body 결과

TestController  : Post Mapping - startDate : 2019-05-15T15:15:15
TestController  : Post Mapping - endDate : 2019-01-01T15:15:15

 

 

+) 오브젝트 매퍼를 싱글톤으로 사용하여도 괜찮을까?

https://fasterxml.github.io/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/ObjectMapper.html

 

reference

https://reflectoring.io/configuring-localdate-serialization-spring-boot/

Posted by doubler
,