문제 상황
스프링으로 구현한 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를 통해서 찾을 수 있었습니다.
기본적으로 코틀린 클래스에서 생성자에서 필드를 초기화할 경우, 어노테이션 적용에 우선순위가 존재합니다.
- 생성자 매개변수 (constructor parameter)
- 프로퍼티 (property)
- 필드 (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 키워드를 통해 프로퍼티를 간편하게 생성할 수 있어서 편리하게 사용하고 있었습니다.
그러나 해당 내용과 같은 문제점을 만나니 이러한 편리함이 가끔 예상치 못한 문제를 야기할 수 있다는 것을 느끼게 되었던 것 같습니다.
따라서 코틀린에서 제공하는 편리한 기능을 적극 활용하되, 다양한 문제점이 발생할 수 있다는 것을 염두해두고 사용하며, 문제 상황을 많이 만나고 정리해야할 것 같습니다!
정리
코틀린에서는 어노테이션의 적용 위치가 명시되지 않으면 일반적으로 다음과 같은 우선순위로 결정된다.
- 생성자 매개변수 (constructor parameter)
- 프로퍼티 (property)
- 필드 (field)
우선순위를 변경하기 위해서는 타겟을 직접 설정할 수 있다.
'Languages > Kotlin' 카테고리의 다른 글
코틀린에서 자바 코드 사용 시 nullable과 non-nullable를 주의하자 (0) | 2024.07.02 |
---|
댓글