본문 바로가기
Languages/Kotlin

스프링+코틀린에서 @Valid이 동작하지 않는 경우

by yoon_seon 2024. 7. 9.

문제 상황

스프링으로 구현한 API 서버를 코틀린으로 마이그레이션하는 작업을 진행 중 Controller에 적용된 @Valid 어노테이션이 동작하지 않는 상황이 생겼습니다.

 

아래는 @Valid 어노테이션을 통해 유효성 검증이 정상 동작하는 자바 소스코드 입니다.

 

자바 코드

@RestController
@RequiredArgsConstructor
public class MemberController extends BaseResource {
	
    private final MemberSignupService memberSignupService;
    
    @PostMapping(value = "/members", headers = X_API_VERSION)
    public ResponseEntity<SuccessResponse<PostMemberSignupResponse>> signup(
        @RequestBody @Valid final PostMemberSignupRequest request) {
       
        // ...
        
    }
}

// PostMemberSignupRequest.java
class PostMemberSignupRequest {
    @NotBlank
    private final String nickname;

    @Email
    private final String email;
}

@RequestBody@Valid가 적용된 검증 DTO에서 @NotBlank가 적용된 nickname 필드에 공백이 전달되어 유효성 검증이 정상 실패합니다.
즉, @Valid가 정상 동작하고있습니다.

 

이를 코틀린 코드로 변경해보겠습니다.

 

코틀린 코드

@RestController
class MemberController(

    private val memberSignupService: MemberSignupService,
    
) : BaseController() {

    @PostMapping("/members", headers = [X_API_VERSION])
    fun signup(
        @RequestBody @Valid request: PostMemberSignupRequest,
    ): ResponseEntity<SuccessResponse<PostMemberSignupResponse>> {
    
				// ...
				
    }

}

// PostMemberSignupRequest.kt
data class PostMemberSignupRequest(
    @NotBlank
    val nickname: String,
    
    @Email
    val email: String,
)

@NotBlank가 적용된 nickname 필드에 공백이 전달되었지만 유효성 검증에 실패하지 않았습니다!
즉, 스프링 + 코틀린에서 @Valid가 정상 동작하지 않고 있습니다!!

 

 

디컴파일을 해보자

문제 원인을 찾아보기위해 코틀린 코드를 자바코드로 디컴파일해서 확인해보았습니다.

 

디컴파일 결과 @NotBlank 어노테이션이 생성자에만 적용 되어 있었습니다.

참고 : @NotNull 어노테이션은 왜 적용되어 있을까? 
코틀린에서는 자바와 달리 nullable 타입과 non-nullable 타입을 명시적으로 구분하여 사용합니다. 위 예제에서는 필드 반환타입에 nullable 타입 선언인 ? 키워드를 을 명시하지 않아 코틀린 컴파일러가 non-nullable 타입이라고 추론하고 @NotNull 어노테이션이 적용된 것입니다.

 

 

프로퍼티를 자동 생성하지 말고 명시적으로 만들어보면

기본적으로 코틀린은 클래스 생성자 val, var 키워드로 매개변수를 선언하면 프로퍼티를 자동으로 생성해줍니다.

그렇다면 자바처럼 직접 프로퍼티를 명시적으로 만들고 @NotBlank같은 검증 어노테이션을 명시적으로 넣어줄때는 어떻게 동작할지 테스트 객체를 만들어서 디컴파일 해봤습니다.

 

검증 어노테이션을 명시적으로 지정

class FieldTest(
    _nickname: String,
    @NotBlank
    _email: String,
) {
    @NotBlank
    private val nickname: String = _nickname
    private val email: String = _email

    fun getNickname(): String {
        return this.nickname
    }

    @NotBlank
    fun getEmail(): String {
        return this.email
    }
}

디컴파일 결과, 직접 명시한 부분에 알맞게 어노테이션이 적용된 것을 확인할 수 있었습니다.

 

그럼 왜 객체 생성자안에 val, var 키워드로 매개변수를 선언했을 때는 @NotBlank 어노테이션이 생성자에만 적용 되었던 걸까요?

 

@NotBlank 어노테이션이 생성자에만 적용 된걸까?

원인은 타 블로그의 포스팅과 GPT를 통해서 찾을 수 있었습니다.

기본적으로 코틀린 클래스에서 생성자에서 필드를 초기화할 경우, 어노테이션 적용에 우선순위가 존재합니다.

  1. 생성자 매개변수 (constructor parameter)
  2. 프로퍼티 (property)
  3. 필드 (field)

따라서 코틀린 어노테이션 적용 우선순위로 인해 생성자 어노테이션에만 생성자가 적용되었던 것 이었습니다.

 

그렇다면 생성자 안에서 프로퍼티를 초기화 했을 때는 어떻게 해야할까요?

 

답변은 코틀린 공식문서에서 확인할 수 있었습니다.

 

Annotations | Kotlin

 

kotlinlang.org

공식문서에 따르면 어노테이션이 사용될 타겟을 아래와 같이 직접 명시해주어 어노테이션이 적용될 곳을 제어할 수 있다고 합니다.

class MyClass(@field:NotBlank val nickname: String) // 필드에 어노테이션 적용 예시

 

필드 이외에도 직접 명시할 수 있는 우선순위는 다음과 같습니다.

  • file
  • property(이 대상을 포함하는 주석은 Java에서 볼 수 없습니다)
  • field
  • get(속성 getter)
  • set(속성 setter)
  • receiver(확장 함수 또는 속성의 수신자 매개변수)
  • param(생성자 매개변수)
  • setparam(속성 설정 매개 변수)
  • delegate(위임된 속성에 대한 대리자 인스턴스를 저장하는 필드)

 

문제 해결

그럼 처음에 문제가 발생했던 코틀린 소스에 필드레벨에 어노테이션이 적용되도록 타겟 설정을 해주겠습니다.

data class PostMemberSignupRequest(
    @field:NotBlank // field 추가!
    val nickname: String,
    
    @Email
    val email: String,
)

이전 케이스와 동일하게 nickname의 값을 공백으로 요청했고, 요청 데이터의 유효성 검증이 잘 동작하는 것을 확인할 수 있었습니다.

 

이어서 수정한 파일을 디컴파일 해보겠습니다.

필드레벨에 @NotBlank 어노테이션이 적용된 것을 확인할 수 있습니다.

 

결론

자바와 달리 코틀린에서는 객체 생성자에 var, val 키워드를 통해 프로퍼티를 간편하게 생성할 수 있어서 편리하게 사용하고 있었습니다.

그러나 해당 내용과 같은 문제점을 만나니 이러한 편리함이 가끔 예상치 못한 문제를 야기할 수 있다는 것을 느끼게 되었던 것 같습니다.

따라서 코틀린에서 제공하는 편리한 기능을 적극 활용하되, 다양한 문제점이 발생할 수 있다는 것을 염두해두고 사용하며, 문제 상황을 많이 만나고 정리해야할 것 같습니다!

 

정리

코틀린에서는 어노테이션의 적용 위치가 명시되지 않으면 일반적으로 다음과 같은 우선순위로 결정된다.

 

  1. 생성자 매개변수 (constructor parameter)
  2. 프로퍼티 (property)
  3. 필드 (field)

우선순위를 변경하기 위해서는 타겟을 직접 설정할 수 있다.

댓글