- Spring Validation을 이용한 입력값과 에러처리에 대한 내용이다.
- 우선 dto 클래스에 기본 validation annotation을 붙여준다.
data class UserSignupRequest(
@field:NotEmpty
val userName: String,
val oAuthChannelType: OAuthChannelType,
@field:NotEmpty
@field:Min(6)
val password: String,
@field:Email
val email: String
)
- 이 때 주의할 점은 @NotEmpty만을 사용하면 유효성 검증이 작동하지 않는다는 점이다.
- Kotlin 특성 상 Primary 생성자에 필드를 선언 할 경우 해당 필드는 파라미터이면서 동시에 getter, 멤버변수가 되기도 한다. 따라서 적용된 애노테이션을 파라미터, getter, 필드 중 어디에 적용해야 할지 모호해진다. field 키워드를 사용하지 않고 단순히 애노테이션만 붙였을 경우(예: @NotEmpty) java 바이트 코드에서는 애노테이션이 아예 사라지는 문제가 있으므로 field키워드를 붙여줘야 한다.
- DTO를 사용하는 컨트롤러에는 해당 DTO를 사용하는 곳에 @Validated 또는 @Valid 애노테이션을 사용해 입력값 검증이 필요하다는 것을 명시해 줘야 한다.
@RestController
@RequestMapping("/user")
class UserController(
private val userService: UserService
) {
@PostMapping("/signup")
fun signup(@RequestBody @Validated request: UserSignupRequest): CommonApiResponse<UserSignUpResponse>
= CommonApiResponse(
success = true,
data = userService.signupUser(request))
}
- 만약 컨트롤러에서 직접 에러를 처리하고 싶다면 BindingResult를 사용하면 된다. 현재 프로젝트의 경우 ControllerAdvice를 이용해 전역 예외처리를 하고 싶기 때문에 BindingResult는 따로 선언해주지 않았다.
- 입력값 유효성 검사 중 문제가 발생한 경우에는
MethodArgumentNotValidException
이 발생한다. 이 예외를 처리해주기 위해 핸들러를 등록한다.
- 필드에 발생한 에러에 대해서는 MethodArgumentNotValidException의 bindingResult 필드를 통해 자세한 내용을 알 수 있다.
@ExceptionHandler(MethodArgumentNotValidException::class)
fun argumentNotValidHandler(e: MethodArgumentNotValidException): ResponseEntity<CommonApiResponse<Any?>>{
log.info("Invalid user input: {}", e.bindingResult.toString())
val sb = StringBuilder()
for (fieldError in e.bindingResult.fieldErrors) {
sb.append("field : ${fieldError.field}, rejectedValue : ${fieldError.rejectedValue}, msg : ${fieldError.defaultMessage}")
sb.append("\n")
}
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(
CommonApiResponse(
success = false,
errorCode = ErrorCode.INVALID_USER_INPUT.code,
message = sb.toString()
)
)
}
- 접근 가능한 필드는 아래 에러 메시지를 통해 유추 가능하다.
Field error in object 'userSignupRequest' on field 'email': rejected value [sdfdff]; codes [Email.userSignupRequest.email,Email.email,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [userSignupRequest.email,email]; arguments []; default message [email],[Ljakarta.validation.constraints.Pattern$Flag;@1814901,.*]; default message [올바른 형식의 이메일 주소여야 합니다]
Field error in object 'userSignupRequest' on field 'userName': rejected value []; codes [NotEmpty.userSignupRequest.userName,NotEmpty.userName,NotEmpty.java.lang.String,NotEmpty]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [userSignupRequest.userName,userName]; arguments []; default message [userName]]; default message [비어 있을 수 없습니다]
- java 표준이 제공하는 annotation을 이용해 커스텀 유효성 검증 애노테이션을 작성할 수 있다.
- Enum값을 검증하기 위한 로직을 작성한다고 가정하였다.
- Enum 값에는 존재하는 값이나 실제 사용은 막는다고 하였을 때 아래와 같은 코드를 이용헤 입력값 검증에 사용할 수 있다.
enum class OAuthChannelType {
KAKAO, GOOGLE
}
class OAuthValidator : ConstraintValidator<ValidOAuth, Enum<OAuthChannelType>> {
private lateinit var annotation: ValidOAuth
override fun initialize(constraintAnnotation: ValidOAuth) {
this.annotation = constraintAnnotation
}
override fun isValid(value: Enum<OAuthChannelType>?, context: ConstraintValidatorContext?): Boolean {
return when (value as OAuthChannelType) {
OAuthChannelType.KAKAO -> true
OAuthChannelType.GOOGLE -> false
}
}
}
@Constraint(validatedBy = [OAuthValidator::class])
@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class ValidOAuth(
val message: String = "Not Supported Channel",
val enumClass: KClass<out Enum<OAuthChannelType>>,
val groups: Array<KClass<*>> = [],
val payload: Array<KClass<out Any>> = []
)
- 애노테이션을 아래와 같이 적용해 입력값 검증을 할 수 있다.
data class UserSignupRequest(
@NotEmpty
val userName: String,
@field:ValidOAuth(enumClass = OAuthChannelType::class)
val oAuthChannelType: OAuthChannelType,
@field:NotEmpty
@field:Min(6)
val password: String,
@field:Email
val email: String
)