반응형
Validation 시리즈
1. Spring Bean Validation - Basic
2. Spring Bean Validation - Custom Annotation
3. Spring Bean Validation - Test Code

프로젝트를 진행함에 있어서 유효성 검사라는 건 중요한 로직중 하나다.

 

잘못 입력된 값을 클라이언트에게 알려주면서, 서버에서 발생할 수 있는 에러를 사전에 막을 수 있다. 처음 개발을 할 때에는 서비스 레이어에 하나하나 Null 체크, 사이즈 검사, 범위 검사를 추가하다 보니 하나의 함수에 영양가(?)가 없는 코드들이 많이 들어가 있어서, 작성한 개발자뿐만 아니라 협업하고 있는 다른 팀원들까지 보기 힘든 코드가 되어버릴 때가 있다.

Spring Framework를 사용하면서 Validator가 있는것을 알고 난 뒤에는 최대한 적용하려고 노력하고 있다. 사용하기도 편하고, 보기도 편하고, 재사용도 편한 Validator를 한번 정리해 본다

Bean Validation Dependency

자바 진영에서 많이 사용하고 있는 이 Valdation은 아래와 같이 간단하게 의존성을 추가할 수 있다(그레이들 기준)

// Starter for using Java Bean Validation with Hibernate Validator
implementation("org.springframework.boot:spring-boot-starter-validation")

예를 들어 Member에 대한 요청을 받는다고 했을 때, 다음 모델에 대해 어떻게 체크할까?

data class Member(
    // 빈문자열을 허용하지 않는다
    val name : String,
    // 이메일 형식을 맞춰야하며, 빈 문자열을 허용하지 않는다
    val email : String,
    // 18~36의 범위안에 들어야한다
    val age : Int,
    // 5를 넘지 말아야한다
    val count : Int
)

아마 서비스 레이어에서 다음과 같이 유효성 체크를 할 수도 있을 것이다

fun createMember(member: Member) {
    check(member.name.isNotEmpty()) { "이름을 입력하세요" }
    check(member.email.isNotEmpty()) { "이메일을 입력하세요" }
    val emailPattern = "^[_A-Za-z0-9-]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$".toPattern()
    check(emailPattern.matcher(member.email).matches()){ "이메일을 확인하세요" }
    check((18..36).contains(member.age)){ "나이를 확인하세요 ${member.age}" }
    check(member.age <= 5) { "5를 넘을 수 없습니다" }
}

이렇게 유효성 체크를 하다보면 프로퍼티가 적은건이야 그나마 괜찮을지 몰라도, 여러개의 유효성 검사가 들어간다던지 하다보면 계속 늘어나게 될것이다. Bean Validation을 사용하여 모델을 다음과 같이 변경한다면 위의 코드의 기능이 그대로 들어간다

data class Member(
    @field:NotBlank(message = "이름을 입력하세요")
    val name : String,

    @field:NotBlank(message = "이메일을 확인하세요")
    @field:Email(message = "이메일을 확인하세요")
    val email : String,

    @field:Range(min = 18, max = 36, message = "나이를 확인하세요")
    val age : Int,

    @field:Max(value = 5, message="5를 넘을 수 없습니다")
    val count : Int
)

코틀린을 사용한다면 @field:를 꼭 사용해주어야한다. 그리고 Controller에 다음과 같이 @Valid 어노테이션을 하나 추가해준다.

@PostMapping
fun createMember(@Valid @RequestBody member: Member)

이렇게 되면 컨트롤러에 해당 모델로 요청올 경우 Validation 체크를 하게 되어 다음과 같이 org.springframework.web.bind.MethodArgumentNotValidException을 던지게 된다.

Resolved [
org.springframework.web.bind.MethodArgumentNotValidException:
... 중략 ...
codes [member.age,age]; arguments []; default message [age],36,18]; default message [나이를 확인하세요]] [Field error in object 'member' on field 'name': rejected value []; codes [NotBlank.member.name,NotBlank.name,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [member.name,name]; arguments []; default message [name]]; default message [이름을 입력하세요]] ]

해당 Exception을 RestControllerAdvice를 만들어 사용하게 되면 일관성있는 에러 메세지를 출력할 수도 있다.

/**
* 모든 Exception을 처리 한다
*/
@RestControllerAdvice
class ControllerAdvice {
    @ExceptionHandler(MethodArgumentNotValidException::class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    fun methodArgumentNotValidException(e: MethodArgumentNotValidException) 
    : ExceptionResponse {
        return ExceptionResponse.bindingExceptionResponse(e.bindingResult)
    }
}

/**
* BindException 처리용 클래스
*/
class ExceptionResponse(
    val errorCode: String,
    val errorMessage: String
){
    val timestamp: LocalDateTime = LocalDateTime.now()
    companion object{
        fun bindingExceptionResponse(bindResult : BindingResult) : ExceptionResponse{
            return ExceptionResponse(
                errorCode = "E001",
                errorMessage = bindResult.fieldErrors.map {
                    "${it.field} : ${it.defaultMessage}"
                }.joinToString { it }
            )
        }
    }
}
// 해당 JSON으로 요청 했을 경우
{
    "name":"",
    "email":"brown@email.com",
    "age":18,
    "count":6
}

// 아래와 같이 응답이 온다 (400 : BAD_REQUEST)

좀더 복잡한 Validation의 경우 Validator 인터페이스를 구현하여 만드는 방법도 있지만, 그 내용은 다음 포스팅에서 진행할 예정이다.

 

Service 레이어에 넘기면서 Validation하는 방법도 있는데, 이상하게 코틀린에서는 잘 되지 않아 계속 찾아보는중이다.

반응형

'개발 > Kotlin' 카테고리의 다른 글

Gradle - dependency-management Plugin  (0) 2022.06.19
QueryDSL 설정 - Kotlin  (0) 2022.06.19
Kotlin Basic - 함수와 변수  (0) 2022.06.18
Spring Bean Validation - Test Code  (0) 2022.06.14
Spring Bean Validation - Custom Annotation  (0) 2022.04.17

+ Recent posts