택시짱의 개발 노트

Kotlin Spring Boot 중첩 클래스에 @Valid 사용 - feat) 이모지 체크(Emoji Valid) 본문

spring

Kotlin Spring Boot 중첩 클래스에 @Valid 사용 - feat) 이모지 체크(Emoji Valid)

택시짱 2023. 11. 28. 12:24

입력 값에 대한 Validation 처리 필요할 때가 있다

예를들어 아래와 같은 User를 생성하는 간단한 예제가 있을때

@RestController
@RequestMapping("/user")
class UserController(
    private val userUseCase: UserUseCase,
){
    @PostMapping("")
    fun createUser(
        @RequestBody request: CreateUserRequest,
    ){
        userUserCase.create(command=request.toCommand())
    }
}
data class CreateUserRequest(
    val name: String,
    val age: Int,
    val address: AddressRequest,
){
    fun toCommand(): CreateUserRequest{
        ....
        return CreateUserRequest(....)
    }
}
data class AddressRequest(
    val detailAddress
)

다음과 같은 제약조건이 있다고 가정

  1. 이름에는 이모지가 포함되면 안된다.
  2. 나이는 1살 이상 100살 이하 이여야 한다
  3. 주소에는 이모지가 포함되면 안된다.

일단 이모지를 검증하기 위해서 EmojiCheck 라는 Annotation과 EmojiValidator를 생성

  • EmojiValidator
class EmojiValidator : ConstraintValidator<EmojiCheck, String> {
    override fun isValid(text: String?, context: ConstraintValidatorContext): Boolean {
        if (checkIsEmpty(text)) {
            return false
        }
        return !containsEmoji(text!!)
    }
    companion object {
        private fun checkIsEmpty(value: String?): Boolean {
            return value == null || value.isEmpty()
        }
        private fun containsEmoji(value: String): Boolean {
            val codePointCount = value.codePointCount(0, value.length)
            for (index in 0 until codePointCount) {
                val codePoint = value.codePointAt(index)
                if (isEmoji(codePoint)) {
                    return true
                }
            }
            return false
        }
        private fun isEmoji(codePoint: Int): Boolean {
            return codePoint in 0x1F600..0x1F64F // Emoticons
                    || codePoint in 0x1F300..0x1F5FF // Misc Symbols and Pictographs
                    || codePoint in 0x1F680..0x1F6FF // Transport and Map
                    || codePoint in 0x1F1E0..0x1F1FF // Regional country flags
                    || codePoint in 0x2600..0x26FF // Misc symbols
                    || codePoint in 0x2700..0x27BF // Dingbats
                    || codePoint in 0xFE00..0xFE0F // Variation Selectors
                    || codePoint in 0x1F900..0x1F9FF // Supplemental Symbols and Pictographs
                    || codePoint in 0x1F018..0x1F270 // Various asian characters
                    || codePoint in 0x238C..0x2454 // Misc items
                    || codePoint in 0x20D0..0x20FF // Combining Diacritical Marks for Symbols
        }
    }
}
  • EmojiCheck Annotation
@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
@Constraint(validatedBy = [EmojiValidator::class])
annotation class EmojiCheck(
    val message: String = "이모지는 사용할 수 없습니다.",
    val groups: Array<KClass<*>> = [],
    val payload: Array<KClass<*>> = [],
)

1. 이름에는 이모지가 포함되면 안된다.

일반적으로 Controller 의 requestBody앞에 @Valid 를 추가해주면 유효성 검증이 진행된다.

@RestController
@RequestMapping("/user")
class UserController(
    private val userUseCase: UserUseCase,
){
    @PostMapping("")
    fun createUser(
        @Valid @RequestBody request: CreateUserRequest,
    ){
        userUserCase.create(command=request.toCommand())
    }
}

data class CreateUserRequest(
    @field:EmojiCheck(message = "한글/영문/숫자/특수문자만 입력해 주세요.")
    val name: String,
    
    val age: Int,
    
    val address: AddressRequest,
){
    fun toCommand(): CreateUserRequest{
        ....
        return CreateUserRequest(....)
    }
}

data class AddressRequest(
    val detailAddress
)

2. 나이는 1살 이상 100살 이하 이여야 한다.

age에 Range를 추가하여 유효성 검사 진행

@RestController
@RequestMapping("/user")
class UserController(
    private val userUseCase: UserUseCase,
){
    @PostMapping("")
    fun createUser(
        @Valid @RequestBody request: CreateUserRequest,
    ){
        userUserCase.create(command=request.toCommand())
    }
}

data class CreateUserRequest(
    @field:EmojiCheck(message = "한글/영문/숫자/특수문자만 입력해 주세요.")
    val name: String,
    
    @field:Range(min = 1, max = 100, message = "1~100자 사이로 입력해 주세요.")
    val age: Int,
    
    val address: AddressRequest,
){
    fun toCommand(): CreateUserRequest{
        ....
        return CreateUserRequest(....)
    }
}

data class AddressRequest(
    val detailAddress
)

3. 주소에는 이모지가 포함되면 안된다.

아래 처럼 AddressRequest의 detailAddress에 EmojiCheck Annotation을 추가하면 유효성 검사가 될 것이라 생각 했으나 유효성 검사가 되지 않고 바로 통과됨..

stackoverflow를 찾아보니 nested valid를 하게 될 경우에는 nested field에 @Valid 를 추가해 주어야 유효성 검사가 된다고 함

@RestController
@RequestMapping("/user")
class UserController(
    private val userUseCase: UserUseCase,
){
    @PostMapping("")
    fun createUser(
        @Valid @RequestBody request: CreateUserRequest,
    ){
        userUserCase.create(command=request.toCommand())
    }
}
data class CreateUserRequest(
    @field:EmojiCheck(message = "한글/영문/숫자/특수문자만 입력해 주세요.")
    val name: String,
	
    @field:Range(min = 1, max = 100, message = "1~100자 사이로 입력해 주세요.")
    val age: Int,
	
    @field:Valid
    val address: AddressRequest,
){
    fun toCommand(): CreateUserRequest{
        ....
        return CreateUserRequest(....)
    }
}
data class AddressRequest(
    @field:EmojiCheck(message = "한글/영문/숫자/특수문자만 입력해 주세요.")
    val detailAddress
)

 

 

결론

중첩클래스에서 유효성 검사를 진행하려면 중첩 클래스가 선언된 field에 @Valid 를 추가하는걸 잊지 말자

반응형
Comments