ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [WebFlux] query string에 담긴 String 파라미터를 localDate로 캐스팅하고 매핑하기
    웹 개발/WebFlux 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)
            }
        }
    

     

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

     

    출처:

Designed by Tistory.