웹 개발/WebFlux

[WebFlux] query string에 담긴 String 파라미터를 localDate로 캐스팅하고 매핑하기

희랍인 조르바 2019. 12. 23. 14:22

get 방식 query string으로 파라미터를 넘겼을 때, controller에서 파라미터로 받는 object에 속한 field의 localDate 타입을 캐스팅하고 매핑해주기

이 방법을 찾게 된 계기는 클라이언트단에서 넘겨줄 파라미터가 많아서 하나하나 받으면 코드가 더러워질 것 같아 dto로 하나의 클래스를 만들기로 했다.

 

단순히 localDate 타입이 되어야하는 딱 하나의 파라미터를 받는다면, 아래처럼 짜면 쉽게 받아와졌다. (@DateTimeFormat을 이용하면 된다)

하나의 localDate형식의 파라미터를 받고 싶을 때,

    // 요청 url: http://localhost:8080/festivals?eventStartDate=2010-10-01
    @GetMapping("/festivals")
    fun getFestival(
    @RequestParam("eventStartDate")
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
    eventStartDate: LocalDate
    ) {
    // do something..
    }

 

하지만 위에서 말했듯이 넘어와야할 파라미터가 많았으므로 DTO를 하나 만들었다.

여러개의 파라미터를 mapping할 DTO

  class FestivalSearchDto(
      val listYN: String? = "",
      val arrage: String? = "",
      val areaCode: String? = "",
      val sigunguCode: String? = "",
      val eventStartDate: LocalDate?,
      val eventEndDate: LocalDate?,
      val numOfRows: Int? = 10,
      val pageNo: Int? = 1,
      val mobileOS: String,
      val mobileApp: String,
      val _type: String = "json"
  )

query string으로 전달한 요청 url

 // 요청 url 
 http://localhost:8080/festivals?listYN=Y&arrage=A&areaCode=32&eventStartDate=2020-01-11&eventEndDate=2020-01-29&numOfRows=12&pageNo=1&mobileOS=ETC&mobileApp=TestApp

요청 url을 받을 api 부분

    @GetMapping("/festivals")
    fun getFestival(festivalSearchDto: FestivalSearchDto): Mono<List<FestivalInfo>> {
        log.debug("축제 정보 api 파라미터 festivalSearchDto 정보 확인 $festivalSearchDto")
        return festivalService.getFestivalInfos(festivalSearchDto)
    }

축제 정보 api가 정상작동하는지 작성한 테스트 코드

    @Test
    fun `축제 정보 url 테스트`() {
        val builder = UriComponentsBuilder.fromUriString("/festivals")
            .queryParam("listYN", "Y")
            .queryParam("arrage", "A")
            .queryParam("areaCode", AreaCode.GANGWON.areaCode)
            .queryParam("eventStartDate", "2020-01-11")
            .queryParam("eventEndDate", "2020-01-29")
            .queryParam("numOfRows", 12)
            .queryParam("pageNo", 1)
            .queryParam("mobileOS", "ETC")
            .queryParam("mobileApp", "TestApp")
            .build(false)

        val entity = restTemplate.getForEntity(
            builder.toUriString(), List::class.java
        )

        assertThat(entity.statusCode).isEqualTo(HttpStatus.OK)
        assertThat(entity.body).isNotEmpty
        println(">>축제 정보 url 테스트 결과 : $entity")
    }

 

아....... 그래 생각처럼 잘될리가 없지 ...

에러 로그

{
"timestamp": "2019-12-22T17:50:21.761+0000",
"path": "/festivals",
"status": 400,
"error": "Bad Request",
"message": "Validation failed for argument at index 0 in method: public reactor.core.publisher.Mono<java.util.List<com.festival.model.FestivalInfo>> com.festival.controller.FestivalController.getFestival(com.festival.model.dto.FestivalSearchDto), with 2 error(s): [Field error in object 'festivalSearchDto' on field 'eventEndDate': rejected value [2020-01-29]; codes [typeMismatch.festivalSearchDto.eventEndDate,typeMismatch.eventEndDate,typeMismatch.java.time.LocalDate,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [festivalSearchDto.eventEndDate,eventEndDate]; arguments []; default message [eventEndDate]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'java.time.LocalDate' for property 'eventEndDate'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.time.LocalDate] for value '2020-01-29'; nested exception is java.lang.IllegalArgumentException: Parse attempt failed for value [2020-01-29]]] [Field error in object 'festivalSearchDto' on field 'eventStartDate': rejected value [2020-01-11]; codes [typeMismatch.festivalSearchDto.eventStartDate,typeMismatch.eventStartDate,typeMismatch.java.time.LocalDate,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [festivalSearchDto.eventStartDate,eventStartDate]; arguments []; default message [eventStartDate]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'java.time.LocalDate' for property 'eventStartDate'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.time.LocalDate] for value '2020-01-11'; nested exception is java.lang.IllegalArgumentException: Parse attempt failed for value [2020-01-11]]] ",

Failed to convert property value of type 'java.lang.String' to required type 'java.time.LocalDate'

String을 LocalDate로 converting 할 수 없단다.....!

 

삽질을 얼마나 했던가.... DTO의 field에 @DateTimeFormat을 붙여보고, Json 형식을 매핑 문제인가, 직렬화 문제인가 이것저것 다 붙여봤는데, 계속 실패 실패 실패!!!

 

 

 

자... 처음부터 차근차근 구글링을 해봅시다.

첫번째로 스택오버플로를 타고 들어가서 단서를 찾았다.

spring/spring boot는 json Body 형태(보통 post 방식에서 사용)에만 date type을 처리한다. 그렇다는 건, query string에 대한 date type을 처리해주지 않는다. 

(spring에서는 json 데이터를 처리해주는 라이브러리 jackson이 있쥬)

 

그래서 필드에 @DateTimeFormat을 붙였던건데, object의 필드에는 @DateTimeFormat을 선언해도 formatting이 되지 않나보다.

 

내 문제를 해결하려면 request parameter인 query string을 date type으로 handling 할 수 있는 설정이 필요하다. 

 

해결법은 있었지만, mvcConfigurer로 설명되어 있었다. 난 mvc가 아니라 webFlux를 사용하고 있었으므로 webFlux로 설정을 맞춰야했고, reference를 참고해보니, 내가 원하는 추가 설정은 mvc에서 해주는거나 차이가 없었다. (둘 다 mvc, webflux 설정을 커스터마이징 해주는 설정이었다)

해결한 방법: IsoFormat 방식을 사용하도록 configuration 추가 및 변경

    @Configuration
    @EnableWebFlux
    class WebConfiguration : WebFluxConfigurer {

        // query string으로 넘어오는 string을 date(localDate, localDateTime)타입으로 casting 해줌
        override fun addFormatters(registry: FormatterRegistry) {
            val registrar = DateTimeFormatterRegistrar()
            registrar.setUseIsoFormat(true)
            registrar.registerFormatters(registry)
        }
    }

 

위의 설정을 추가하니 깔끔하게 테스트 통과

 

출처: