[WebFlux] query string에 담긴 String 파라미터를 localDate로 캐스팅하고 매핑하기
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)
}
}
위의 설정을 추가하니 깔끔하게 테스트 통과
출처:
- StackOverFlow - How to use LocalDateTime RequestParam in Spring? I get “Failed to convert String to LocalDateTime”
- https://github.com/swagger-api/swagger-codegen/issues/4113
- https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/reactive/config/WebFluxConfigurer.html