서두
본 글은 코틀린 + 스프링부트 환경에서 @Valid가 동작하지 않는 케이스를 다루었고 해당 이슈를 완전히 해결하지 못했음. 해결을 하기는 했는데 해결이 된 이유를 알 수 없음. 완벽한 해결책을 찾고자 했다면 "뒤로가기" 버튼을 누르기 바람
설명에 앞서 개발환경을 간단히 소개하고자 한다. 혹시나 나와 비슷한 상황을 겪고 이를 해결한 분이 계시다면 댓글로 안내를 부탁드...립니다. (제발) 아래는 gradle 스크립트 중 일부이다.
plugins {
id("org.springframework.boot") version "2.5.0"
id("io.spring.dependency-management") version "1.0.11.RELEASE"
kotlin("jvm") version "1.5.10"
kotlin("plugin.spring") version "1.5.10"
...
dependencies {
implementation("org.springframework.boot:spring-boot-starter-validation")
...
🤯 이슈 한 줄 요약
@Valid를 사용했으나 값 검증을 하지 않는다.
💡 해결 한 줄 요약
@Valid를 사용할 때, @Validated도 함께 사용한다.
이제 구구절절 설명을 해보자면... 지금 다루려는 @Valid는 자바 표준 검증 어노테이션으로 JSR-303이란 이름으로 채택된 서블릿 2.3 표준스펙 중 유효성 검증을 위한 어노테이션이다. 그리고 또 하나의 유효성 검증을 하기 위해 사용하는 어노테이션인 @Validated는 스프링에서 제공해주고 있다.
@Valid를 코틀린 + 스프링 환경에서 아래와 같이 사용하고 있다.
import io.swagger.annotations.Api
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import org.springframework.data.web.PageableDefault
import org.springframework.web.bind.annotation.*
import javax.validation.Valid
@Api(description = "서적 관리")
@RestController
@RequestMapping("sample")
class MyController(
val myService: MyService,
) {
@Operation(summary = "서적 등록")
@PostMapping
fun createBook(@RequestBody @Valid list: List<BookCreateModel>) {
myService.createBook(list)
}
...
}
import io.swagger.annotations.ApiModelProperty
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotNull
data class MyCreateModel(
@ApiModelProperty(value = "소유자 ID", example = "1")
@field:NotNull(message = "소유자 ID를 입력하세요")
val userId: Long? = null,
@ApiModelProperty(value = "서적 코드", example = "SAMPLE00001")
@field:NotBlank(message = "서적 코드를 입력하세요")
val bookCode: String? = null,
...
) {
...
}
위 @Valid에서 실패가 검증되도록 API를 호출해보자
// POST http://localhost:8088/mybook/sample
[
{
"userId" : 1
}
]
위와 같이 실행했을 때, 필자가 기대한 것은 에러 메시지로 "서적 코드를 입력하세요"가 나타나는 것이었다. 하지만 검증이 Pass가 되고 서비스단에서 필요한 값이 없어서 NullPointerException이 나타났다. 즉 @Valid가 동작하지 않았다. 어떻게 된 일일까?
@Valid가 동작하지 않는 케이스를 살펴보았다. 아래 포스팅에서 해당 케이스를 잘 정리하고 있다. 자, 이제 필자의 케이스와 비교해보자.
🧐 필자의 케이스
data class의 생성자에 제약 조건을 걸지 않았는지 체크
제대로 @field에 입력하고 있다. 해당없음
적절한 곳에 @Valid 사용했는지 체크
RequestBody와 함께 매개변수 받는 쪽에서 사용했다. 해당없음
<arg>-Xemit-jvm-type-annotations</arg>
메이븐이라서 그래들에 맞게 테스트를 했으나 상황은 같았다. 또한 코틀린 버전이 낮지도 않고 (1.5.10) 더 올려서(1.6.10) 테스트 해 보아도 상황이 같다.
// build.gradle.kts
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict", "-Xemit-jvm-type-annotations")
jvmTarget = "11"
}
}
dependency 체크
위에 공유한 블로그에서는 jakarta를 추가하지 않았는지 확인해보라고 하지만, 내 프로젝트에서는 jakarta가 존재하지 않으며 spring-boot-starter-validation도 제대로 추가되어 있다.
그러면 어떻게 해야 원하는대로 동작할 것인가? 바로 Validated 어노테이션을 추가하면 된다. 필자가 알기로는 Validated와 Valid, 둘 중 하나만 사용해도 유효성 검증이 가능한데 코틀린 + 스프링 환경에서는 이 둘 모두를 사용해야 한다. 문제는 "왜"인지 알 수 없다는 것. 둘 모두를 사용해야 유효성 검증을 할 수 있다는 것은 알겠는데 아무리 찾아봐도 이유를 알 수 없었다.
...
import org.springframework.validation.annotation.Validated
@Api(description = "서적 관리")
@Validated
@RestController
@RequestMapping("sample")
class MyController(
val myService: MyService,
) {
@Operation(summary = "서적 등록")
@PostMapping
fun createBook(@RequestBody @Valid list: List<BookCreateModel>) {
myService.createBook(list)
}
...
}
이 방법말고 모델 클래스를 Java로 만드는 방법도 있다. 원하는 형태로 진행하면 된다.
이 사항에 대해 제대로 원인/이유 등을 아는 분이라면 댓글로 꼭! 꼭! 꼭! 알려주길 간절히 부탁...드립니다.
'Spring' 카테고리의 다른 글
코틀린 환경에서 Async 어노테이션 테스트하기 (0) | 2022.02.08 |
---|---|
Kotlin에서 RestTemplateClient를 만들어보자 (0) | 2021.12.23 |
스프링 게이트웨이에서 dev, prod 등 여러 환경 적용하기 (0) | 2021.10.07 |
oauth 세팅 중 yml과 properties가 이해되지 않을 때, 스프링 시큐리티와 프로바이더 (0) | 2021.09.28 |
Postman에서 Mock Server과 API 만들고 테스트하기 (하) (0) | 2021.06.23 |
Comment