Spring Boot + Kotlin Bean Validation이 작동하지 않는 문제 해결

임동혁 Ldhbenecia·2025년 5월 4일

SpringBoot

목록 보기
16/28
post-thumbnail

Kotling SpringBoot로 개발을 진행하던 중 Validation이 동작하지 않아 학습 내용을 정리한다.

dto

data class TodoRequest(
    @field:NotNull(message = "User ID is required")
    val userId: UUID,

    @field:NotBlank(message = "Title is required")
    val title: String,

    @field:NotBlank(message = "Description is required")
    val description: String,

    @field:Future(message = "Scheduled time must be in the future")
    val scheduledTime: LocalDateTime,

    val isDone: Boolean = false
)

프로젝트에서 DTO 유효성 검증을 위해 @NotBlank, @NotNull, @Future 등의 애노테이션을 사용했다.


@field: 를 왜 명시적으로 붙여주는가?

data class User(
    @get:NotBlank       // getter 메서드에 붙음
    @set:NotBlank       // setter 메서드에 붙음
    @param:NotBlank     // 생성자 파라미터에 붙음
    @field:NotBlank     // 필드(자바 변수)에 붙음 ← ✅ Bean Validation이 요구하는 위치
    val name: String
)

예시로 보는 차이

data class TodoRequest(
    @NotBlank val title: String
)

이렇게 작성하면 Kotlin은 기본적으로 @param:에 어노테이션을 붙인다.

컴파일 후 바이트코드를 보면:

public class TodoRequest {
    public TodoRequest(@NotBlank String title) { ... }
}

즉, 어노테이션이 생성자 파라미터에만 붙은 것이고, 필드 자체에는 붙지 않는다.

Bean Validation (예: Hibernate Validator)은 자바의 field를 기준으로 유효성을 검사한다.

  • 자바의 @NotBlank는 필드에 붙어야 검사를 트리거 한다.
  • Kotlin에서는 자동으로 필드에 안 붙이므로, 명시적으로 @field:NotBlank라고 선언해야 필드에 붙여서 인식이 가능해지는 것이다.
@field:NotBlank
val title: String

이렇게 하면 자바 코드로는 다음처럼 보인다.

@NotBlank
private final String title;

이제야 Hibernate Validator가 인식할 수 있는 형태가 된 것이다.


📌 @field 명시 결론

  • Kotlin은 자바보다 어노테이션 적용 위치가 다양함 → 명시적으로 붙여야 정확히 동작
  • Bean Validation을 사용할 때는 항상 @field: 를 붙여야 의도한 유효성 검사가 실행됨
  • 바이트코드 기준으로 보면 @field: 만이 자바의 private final String title 필드에 붙는 어노테이션임

controller

@PostMapping
fun createTodo(@RequestBody @Valid request: TodoRequest): ResponseEntity<ApiResponse<TodoResponse>> {
    val todo = request.toEntity()
    val saved = todoService.createTodo(todo)
    return ResponseEntity.status(HttpStatus.CREATED)
        .body(ApiResponse(201, "Todo created", TodoResponse.fromEntity(saved)))
}

컨트롤러단에서 @Valid 어노테이션을 걸어주었다.

이때 자동완성으로 @Valid가 뜨지않고 @Validated가 떴다.
결론적으로 @Valid를 import 해주었다.

  • @Valid는 Java의 표준 애노테이션 (javax.validation.Valid 또는 jakarta.validation.Valid)
  • @Validated는 Spring에서 제공하며 groups를 지원하고 컨트롤러, 서비스 등에서 확장성 있게 사용 가능

그룹 단위 검증에 대해서는 아직 적용할 필요가 없으므로 설명은 생략한다.


test json data

{
  "userId": "da4d9626-7868-4426-866b-e83f96b7832a",
  "title": "",
  "description": "test data description",
  "scheduledTime": "2025-05-10T14:00:00",
  "isDone": false
}
{
    "status": 201,
    "message": "Todo created",
    "data": {
        "id": "cc3b9960-2d80-40ea-9b8c-3231ba8b8173",
        "userId": "da4d9626-7868-4426-866b-e83f96b7832a",
        "title": "",
        "description": "test data description",
        "scheduledTime": "2025-05-10T14:00:00",
        "isDone": false,
        "createdAt": "2025-05-04T17:58:25.685626"
    },
    "timestamp": "2025-05-04T17:58:25.699784"
}

title을 빈 값으로 처리했으나, 400이 아니라 201이 떠서 정상적인 응답이 반환된 것을 확인할 수 있다.

에러 요약

SpringBoot를 실행하는 과정에서 아래와 같은 오류코드를 발견할 수 있었다.

jakarta.validation.NoProviderFoundException: Unable to create a Configuration, 
because no Jakarta Bean Validation provider could be found.

🔍 원인 분석

기존 의존성 설정:

implementation("jakarta.validation:jakarta.validation-api:3.0.2")
  • jakarta.validation-apiAPI 정의만 포함하고 있으며, 실제 동작을 위한 구현체(Provider) 가 포함되어 있지 않다.
  • 즉, @Validated, @NotBlank, @NotNull 등의 어노테이션은 실행될 Provider가 없어 무시되거나 예외를 발생시킨다.

✅ 해결 방법

다음과 같이 Hibernate Validator (Jakarta Bean Validation의 Reference Implementation)를 명시적으로 추가하였다.


dependencies {
    implementation("org.hibernate.validator:hibernate-validator:8.0.1.Final")
    implementation("org.glassfish:jakarta.el:4.0.2") // EL 사용을 위한 필수 라이브러리
}

hibernate-validator: Bean Validation 3.0 (jakarta.validation)을 실제로 수행할 구현체

  • jakarta.el: Expression Language를 통한 메시지 표현 등에 필요 (예: message = "{some.property}")

jakarta.el의 역할은 무엇인가

jakarta.elExpression Language (EL) 를 지원하는 라이브러리이다.

✅ 사용 예시

1. 메시지 내 변수 바인딩

@Size(min = 3, message = "이름은 최소 {min}자 이상이어야 합니다.")

이 때 {min} 은 Expression Language 문법이며, jakarta.el이 없으면 처리되지 않는다.

2. ValidationMessages.properties에서 메시지 참조

@NotBlank(message = "{user.username.notblank}")
  • 이 경우, @NotBlank외부 properties 파일의 key를 EL로 해석해 해당 값을 메시지로 대체한다.
  • jakarta.el이 없으면 {user.username.notblank} 그대로 메시지에 출력되어버린다.

📌 최종 결론

jakarta.validation-api는 인터페이스만 제공하며, Spring Boot starter에 포함되지 않으면 직접 명시해줘야 한다.

  • Spring Boot 3.x 이후로는 jakarta.validation-api만 남고, Hibernate Validator는 자동 포함되지 않기 때문에 사용자가 명시해야 한다.

0개의 댓글