[Spring] Spring Boot Validation 설명 & 예제 (Kotlin)

728x90

Validation 필요한 이유

  1. 유효성 검증 하는 코드의 길이가 너무 길다. -> annotation 으로 해결
  2. service logic에 대해서 방해가 된다.
  3. 흩어져 있는 경우 어디서 검증 되었는지 찾기 힘들다.
  4. 검증 로직이 변경되는 경우 테스트 코드 등, 전체 로직이 흔들릴 수 있다. -> 한 곳에 몰아서 검증 가능

JSR-380 BeanValidation

build.gradle.kts 세팅

DeleteApiController

// https://beanvalidation.org/2.0-jsr380/spec/
// JSR-320
// hibernate Validation
// Spring boot Validation
@RestController
@RequestMapping("/api")
@Validated //_age는 Bean이 아니기 때문에 어노테이션 필요
class DeleteApiController {

    // 가질수 있는 것
    // 1. path variable
    // 2. request param

    @DeleteMapping(path = ["/delete-mapping"])
    fun deleteMapping(
        // 이름 지정 가능
        @RequestParam(value = "name") _name: String,

        //Validation
        @NotNull(message = "age 값이 누락되었습니다.")
        @Min(20, message = "20보다 커야 합니다.")

        @RequestParam(name = "age") _age: Int
    ): String {
        println(_name)
        println(_age)
        return _name + " " + _age
    }

    @DeleteMapping(path = ["/delete-mapping/name/{name}/age/{age}"])
    fun deleteMappingPath(
        @PathVariable(value = "name")
        @Size(min = 2, max = 5, message = "name의 길이는 2~5")
        @NotNull
        _name: String, // aa ~ aaaaa

        //Validation
        @NotNull(message = "age 값이 누락되었습니다.")
        @Min(20, message = "20보다 커야 합니다.")
        @PathVariable(name = "age") _age: Int
    ): String {
        println(_name)
        println(_age)
        return _name + " " + _age
    }
}

PutApiController

@RestController
@RequestMapping("/api")
class PutApiController {

    @PutMapping("/put-mapping")
    fun putMapping(): String {
        return "put-mapping"
    }

    @RequestMapping(method = [RequestMethod.PUT], path = ["/request-mapping"])
    fun requestMapping(): String {
        return "request-mapping - put method"
    }

    //Post와 동일, Put -> 내용없으면 생성, 있으면 수정
    //Bean에 Vaildation 적용할려면 @Vaild 필요
    //BindingResult -> Bean 에러 분기 나눠서 검출
    @PutMapping(path = ["/put-mapping/object"])
    fun putMappingObject(
        @Valid @RequestBody userRequest: UserRequest,
        bindingResult: BindingResult    //Vaild -> BindingResult -> hasError? -> logic 탄다
    ): ResponseEntity<String> {

        if (bindingResult.hasErrors()) {
            // 500 error
            val msg = StringBuilder()
            bindingResult.allErrors.forEach {
                val field = it as FieldError    // FieldError 로 형변환
                val message = it.defaultMessage     //Message Customize
                msg.append("${field.field} : $message\n")   //메세지 합치기
//                name : 크기가 2에서 8 사이여야 합니다
//                name : 비어 있을 수 없습니다
//                age : 0 이상이어야 합니다
            }
            return ResponseEntity.badRequest().body(msg.toString())
        }

        return ResponseEntity.ok("")
    }
}

Custom Validation Annotation

UserRequeset

//해당 Bean 검증 (UserRequest)
data class UserRequest(

    @field:NotEmpty
    @field:Size(min = 2, max = 8)
    var name: String? = null,

    @field:PositiveOrZero   // 0 < 숫자를 검증 0도 포함(양수)
    var age: Int? = null,

    @field:Email    // email양식
    var email: String? = null,

    @field:NotBlank // 공백을 검증
    var address: String? = null,

    @field:Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}\$")  // 정규식 검증
    var phoneNumber: String? = null, // phoneNumber 양식

    //Valid 까다로울 때, 원하는 조건의 어노테이션 없을 경우 -> annotation 커스터마이징
    @field:StringFormatDateTime(pattern = "yyyy-MM-dd HH:mm:ss", message = "패턴이 올바르지 않습니다.")
    var createdAt: String? = null   // yyyy-MM-dd HH:mm:ss  ex) 2020-10-02 13:00:00
)
//{
    //True면 정상 False면 비정상 -> 매번 이렇게 만들긴 힘들어서 커스텀 어노테이션 만든다 보통 : annotation package, StringFormatDateTime 참고
//    @AssertTrue(message = "생성일자의 패턴은 yyyy-MM-dd HH:mm:ss 여야 합니다") //method는 field X
//    private fun isValidCreatedAt(): Boolean {   // 정상 -> true , 비정상 -> false
//        return try {
//            LocalDateTime.parse(this.createdAt, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
//            true
//        } catch (e: Exception) {
//            false
//        }
//    }

//}

StringFormatDateTime

//Annotation으로써 작동하게 하는 Setting
//Validator필요 -> StringFormatDateTimeValidator
@Constraint(validatedBy = [StringFormatDateTimeValidator::class])
@Target(
    AnnotationTarget.FIELD,
    AnnotationTarget.PROPERTY_GETTER,
    AnnotationTarget.PROPERTY_SETTER
)
@Retention(AnnotationRetention.RUNTIME) //Runtime 에만 활용 할 수 있도록
@MustBeDocumented //Kotlin에서 붙여주기
annotation class StringFormatDateTime(
    //pattern을 받고 message를 출력할 수 있다.
    val pattern: String = "yyyy-MM-dd HH:mm:ss",
    val message: String = "시간형식이 유효하지 않습니다",

    //default 값
    val groups: Array<KClass<*>> = [],
    val payload: Array<KClass<out Payload>> = []
)

StringFormatDateTimeValidator

class StringFormatDateTimeValidator : ConstraintValidator<StringFormatDateTime, String> {

    private var pattern: String? = null

    //Annotation pattern 불러오기
    override fun initialize(constraintAnnotation: StringFormatDateTime?) {
        this.pattern = constraintAnnotation?.pattern
    }

    // 검증할때의 메소드 : 정상이면 True, 비정상이면 False
    override fun isValid(value: String?, context: ConstraintValidatorContext?): Boolean {
        return try {
            LocalDateTime.parse(value, DateTimeFormatter.ofPattern(pattern))
            true
        } catch (e: Exception) {
            false
        }
    }
}
728x90

댓글

Designed by JB FACTORY