본문 바로가기

TIL

TIL) @JsonFormat/@JsonSerialize, Rest response 방식의 404, configureDefaultServletHandling

1. LocalDate 직렬화 - 기본 serizlizer 가 objectMapper 에 등록되어 있고,  또 다른 포멧으로 직렬화 해야 하는 요구사항이 있다면 어떻게 해야하나?

 

Spring boot 의 Jackson 라이브러리로는 LocalDate, LocalDateTime 타입이 이상적으로 직렬화되지 않는다.

 

 

따라서 아래와 같이 @JsonFormat 을 붙여서 직렬화, 역직렬화 시 패턴을 지정할 수 있다.

@JsonFormat(pattern = "yyyy-MM-dd")
val createAt: LocalDate

 

모든 LocalDate, LocalDateTime 에 @JsonFormat 을 붙이기는 불편하다.

Serializer 를 직접 생성하여 ObjectMapper 에 등록하는 방법이 있다.

 

@Configuration
class BeanConfig {

    @Bean
    fun getObjectMapper(): ObjectMapper {
    	val simpleModule = SimpleModule().apply {
            addSerializer(LocalDate::class.java, LocalDateSerializer())
            addDeserializer(LocalDate::class.java, LocalDateDeserializer())
        }

    
        ObjectMapper().apply {
            registerModules(simpleModule)
        }
    }
}



class LocalDateSerializer : JsonSerializer<LocalDate> {

    override fun serialize(date: LocalDate, g: JsonGenerator, sp: SerializerProvider) {
        val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
        val format = dateTime.format(formatter)
        g.writeString(format)
    }
}

 

 

LocalDate, LocalDateTime 을 사용하는 프로젝트라면, 보통 기본적으로 이 기본 직렬화, 역직렬화 클래스가 ObjectMapper 에 등록되어 있을 것이다.

 

이 때, 내가 다른 방법으로 직렬화를 하고 싶어서, 특정 클래스에 @JsonFormat 을 붙여도, 먹히지 않는다. 이 때에는 @JsonFormat 말고 @JsonSerialize 를 선언하여 직렬화 클래스를 선언해줘야 한다.

 

class LocalDateCustomSerializer : JsonSerializer<LocalDate> {

    override fun serialize(date: LocalDate, g: JsonGenerator, sp: SerializerProvider) {
        val formatter = DateTimeFormatter.ofPattern("yyyyMMdd")
        val format = dateTime.format(formatter)
        g.writeString(format)
    }
}

@JsonSerialize(using = LocalDateCustomSerializer::class)
val createAt: LocalDate

// 출력 형식 : 20211118

 

 

 

 

 

 

2. rest API 서버에서 404 상황 시 공통 response 처리 방법을 알아보다가 정리한 점.

 

 

dispatcher.setThrowExceptionIfNoHandlerFound(true)

 

옵션은 doDispatch 의 mappedHandler 가 null 일 때, 즉 handler 를 찾지 못했을 때, 즉 404일 떄 noHandlerFound 메서드를 실행시키게 되는데, 이 noHandlerFound 메서드에서 ThrowExceptionIfNoHandlerFound 값이 true 일 때 NoHandlerFoundException 예외 를 던지게 해줌. 따라서 @ExceptionHandler 로 잡을 수 있음.

 

그런데, WebMvcConfigurer 구현 클래스에서 

 

override fun configureDefaultServletHandling(configurer: DefaultServletHandlerConfigurer) {
    configurer.enable()
}

 

해버리면, DefaultServletHttpRequestHandler, SimpleUrlHandlerMapping 을 등록시켜버려서,  mappedHandler가DefaultServletHttpRequestHandler 가 들어와서 null 이 아니게 되어서 예외가 발생 안함.

 

따라서, enable() 설정을 껐더니 에러가 잘 발생했고 @ExceptionHandler 에서 원하던 처리를 할 수 있었다.

 

그런데, 여기서 @RequestMapping("/**")  라는 컨트롤러를 설정해주게 되면, 뭔가 mappedHandler 가 들어왔다. 마치 enable() 설정을 킨 것 처럼. 따라서, enable() 을 없앴다 하더라도, 저런 컨트롤러가 있으면 예외가 발생하지 않았다. 그리고 그 핸들러가 404 상황에 대해 처리해주고 있었다. 뭔가 핸들러를 찾지 못했을때 DefaultServlet 처럼 기본 처리를 하는 로직인 것 같은데, (마치 디폴트 서블릿 = @RequestMapping("/") 인 것 처럼) 결국 해답은 찾지 못했다.  그리고 우선순위는 @RequestMapping("/") 의 처리가 DefaultServlet 처리보다 먼저였다.

 

문제상황의 결론은, 404 상황에 대한 처리로 커스텀한 @RequestMapping("/") 를 만들기로 했다.

 

 

->

@RequestMapping("/**") 라는 것은 모든 URL 에서 핸들러를 찾을 수 있다는 말과 같다.

따라서 dispatcherServlet 의 mappedHandler 가 null 일 리가 없다.

결국 @RequestMapping("/**") 가 있다면, DefaultServelt 이 필요없다는 말과 같다. 핸들러를 찾지 못하는 notfound 상황이 없으니까.

단지,@RequestMapping("/**") 와 @RequestMapping("/book") 과 같이 특정지은 핸들러가 있을 때, 둘 다 해당할텐데, 더 비슷한 이름의 핸들러 엔드포인트를 찾는 로직을 아직 모른다. 더 똑같은걸 선택하는 건 당연하지만, 구체 로직이 궁금하긴 하다.

 

 

 

 

3. configureDefaultServletHandling

override fun configureDefaultServletHandling(configurer: DefaultServletHandlerConfigurer) {
    configurer.enable()
}

이 코드는 아래 두 클래스를 빈으로 등록시킨다.

  • DefaultServletHttpRequestHandler
  • SimpleUrlHandlerMapping

~Handler 는 컨트롤러라 생각하면 쉬운, 실제 요청을 처리하는 핸들러이다.

~Mapping 은 요청에 맞는 요청을 처리할 핸들러에 맵핑시킨다.

 

 

위 의 enable() 설정으로 인해, Dispatcher Servlet 은 요청을 아래와 같은 순서로 처리한다.

  1. RequestMappingHandlerMapping 을 사용해서 요청을 처리할 핸들러를 검색한다.
    1. 존재하면 해당 핸들러를 이용해서 요청을 처리한다.
  2. 존재하지 않으면 SimpleUrlHandlerMapping 을 사용해서 요청을 처리할 핸들러를 검색한다.
    1. SimpleUrlHandlerMapping 은 모든 경로("/**")에 대해 DefaultServletHttpRequestHandler를 리턴한다. 즉 모든 경로에서 DefaultServletHttpRequestHandler 를 사용한다는 것이다.
    2. DispatcherServlet 은 DefaultServletHttpRequestHandler 에 처리를 요청한다.
    3. DefaultServletHttpRequestHandler 는 디폴트 서블릿에 처리를 위임한다.

 

여기서, 디폴트 서블릿은, 톰켓에 정의된 기본 디폴트 페이지를 보여줄 수도 있고, 또는 따로 설정한 프로젝트의 페이지일 수 도 있다.