본문 바로가기

Kotlin

Kotlin의 @ModelAttribute와 Custom Validation 어노테이션 적용 문제 해결 사례

Spring Boot 프로젝트에서 @ModelAttribute를 통해 요청 데이터를 받으면서, Kotlin의 데이터 클래스를 활용해 커스텀 유효성 검사 어노테이션을 적용할 때 발생했던 문제와 이를 해결한 과정을 정리한다.


문제 상황

 

다음과 같이 @ModelAttribute로 선언한 데이터 클래스 PayRequest가 있다. userId 필드에는 커스텀 유효성 검사 어노테이션이 적용되어 있다.

 

PayRequest 클래스

data class PayRequest(
    val amount: Double, // 결제 금액
    @CheckUserId
    val userId: Long, // 사용자 ID
) {
    init {
        require(amount > 0) { "금액이 유효하지 않습니다." }
        require(userId >= 0) { "사용자 ID는 음수일 수 없습니다." }
    }
}

 

커스텀 어노테이션 @CheckUserId는 특정 사용자 ID가 존재하는 확인하기 위해 다음과 같이 정의되어 있다.

@CheckUserId 어노테이션 (예시)

@Retention(AnnotationRetention.RUNTIME)
@Target(
    AnnotationTarget.TYPE,
    AnnotationTarget.FIELD,
    AnnotationTarget.VALUE_PARAMETER,
)
@Constraint(validatedBy = [CheckUserIdValidator::class])
annotation class CheckUserId(
    val message: String = "해당 사용자가 존재하지 않습니다.",
    val groups: Array<KClass<*>> = [],
    val payload: Array<KClass<out Payload>> = [],
)

@Component
class CheckUserIdValidator(
    private val userDQryBus: IUserDomainQueryBus
) : ConstraintValidator<CheckUserId, Long> {
    override fun isValid(value: Long, context: ConstraintValidatorContext?): Boolean {
        return userDQryBus.existById(value)
    }
}

 

컨트롤러에서는 다음과 같이 @Valid를 붙여 데이터를 검증하고자 했다.

컨트롤러 코드

@PostMapping("/pay")
fun processPayment(@Valid @ModelAttribute payRequest: PayRequest) {
    // 
}

 

테스트를 진행했을 때, @CheckUserId 와 같은 커스텀 유효성 검사 어노테이션이 작동하지 않았다.


문제 원인

Kotlin의 어노테이션 적용 위치와 Spring의 유효성 검사 처리 방식의 차이 때문이다. Kotlin에서 어노테이션을 val userId: Long와 같이 생성자의 필드에 선언하면, 기본적으로 생성자의 파라미터에 적용된다.

즉, Kotlin에서 data class나 일반 클래스의 주 생성자에 선언된 val 또는 var 프로퍼티에 어노테이션을 붙이면, 이 어노테이션은 해당 프로퍼티의 필드가 아니라 생성자의 파라미터에 적용된다.

이를 디컴파일된 바이트코드로 확인해보면 다음과 같다.

public final class PayRequest {
    private final double amount;
    private final long userId;

    public PayRequest(double amount, @CheckUserId long userId) {
        this.amount = amount;
        this.userId= userId;
    }
}

여기서 @CheckUserId 는 생성자의 파라미터에만 적용되어 있으며, Spring의 유효성 검사(@Valid)는 필드 또는 getter에 적용된 어노테이션만을 인식한다. 따라서, 커스텀 유효성 검사 어노테이션이 작동하지 않은 것이다.

 


 

해결 방법

 

1. @field를 사용해 어노테이션을 필드에 적용

data class PayRequest(
    val amount: Double,
    @field:CheckUserId
    val userId: Long
)

 

2. Kotlin에서 어노테이션이 필드에 적용되도록 명시적으로 @field 키워드를 붙인다.

 

 

수정 후 디컴파일된 바이트코드를 확인하면 다음과 같다.

public final class PayRequest {
   private final double amount;
   @CheckUserId
   private final long userId;

   public final double getAmount() {
      return this.amount;
   }

   public final long getUserId() {
      return this.userId;
   }

   public PayRequest(double amount, long userId) {
      this.amount = amount;
      this.userId= userId;
   }

 

위와 같이, @CheckUserId 어노테이션이 userId필드에 잘 적용된 것을 확인할 수 있다. 이는 의도한 대로, 어노테이션이 필드에 부착되어 Spring에서 유효성 검사를 정상적으로 처리할 수 있게 된 것을 의미한다.