728x90
반응형

[Spring Boot] @Valid 검증이 동작하지 않는 경우

환경 : Spring Boot 3.x, Kotlin 1.9

 


에러 메시지

// @Valid를 붙였는데 유효하지 않은 값이 그대로 통과됨
// MethodArgumentNotValidException이 발생하지 않음

 

1. 원인

Spring Boot 3.x부터 spring-boot-starter-validation 의존성이 기본 포함 안 됨. 별도 추가 필요

또는 아래 케이스에서도 동작하지 않음

  • @Valid 어노테이션 위치가 잘못된 경우
  • Kotlin data class 필드에 어노테이션이 안 먹는 경우
  • @Validated@Valid 혼동

 

2. 해결 방법

2.1) 의존성 추가

// build.gradle.kts
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-validation")
}

2.2) @Valid 위치 확인

Controller 메서드의 @RequestBody 앞에 @Valid 선언

// 동작 안 함
@PostMapping
fun save(@RequestBody request: MemberRequest) // @Valid 누락

// 정상
@PostMapping
fun save(@Valid @RequestBody request: MemberRequest)

2.3) Kotlin data class에서 field 타겟 지정

Kotlin은 생성자 파라미터와 필드가 분리되어 있어서 어노테이션이 필드에 안 붙을 수 있음. @field: 사용 타겟 지정 필요

// 동작 안 함
data class MemberRequest(
    @NotBlank
    val name: String,
    @Email
    val email: String
)

// 정상
data class MemberRequest(
    @field:NotBlank
    val name: String,
    @field:Email
    val email: String
)

※ Kotlin에서는 @field:를 안 붙이면 어노테이션이 생성자 파라미터에만 적용되고 필드에는 적용 안 됨

2.4) @Validated로 클래스 레벨 검증

@RequestParam이나 @PathVariable 검증은 @Valid가 아니라 클래스에 @Validated 필요

@Validated // 클래스 레벨에 선언
@RestController
class MemberController(private val memberService: MemberService) {

    @GetMapping("/{id}")
    fun getById(@PathVariable @Min(1) id: Long): Member {
        return memberService.getById(id)
    }
}

 

3. 에러 응답 커스터마이징

기본 응답은 가독성이 떨어짐. @ExceptionHandler로 포맷 정리

@RestControllerAdvice
class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException::class)
    fun handleValidation(e: MethodArgumentNotValidException): ResponseEntity<Map<String, String>> {
        val errors = e.bindingResult.fieldErrors.associate {
            it.field to (it.defaultMessage ?: "")
        }
        return ResponseEntity.badRequest().body(errors)
    }
}

응답 예시

{
    "name": "must not be blank",
    "email": "must be a well-formed email address"
}

 

728x90
반응형

'Kotlin' 카테고리의 다른 글

Kotlin + Spring Boot 시작하기  (0) 2026.03.10
728x90
반응형

환경 : Spring Boot 3.1, Kotlin 1.9, Intellij

목표 : Kotlin 기반 Spring Boot 프로젝트 생성 및 간단한 CRUD API 구현


1. 코프링(Kopring)이란?

Kotlin + Spring의 합성어. Spring Boot 프로젝트를 Kotlin으로 개발하는 것을 의미

Java 대비 주요 차이점

  • Null 안정성 : Java는 런타임에 NPE 발생. Kotlin은 컴파일 타임에 체크
  • 코드량 : 동일한 기능 대비 약 30~40% 감소
  • 데이터 클래스 : Java는 Lombok 필요. Kotlin은 data class로 기본 제공

2. 프로젝트 생성

start.spring.io 에서 아래와 같이 설정

Project  : Gradle - Kotlin
  Language : Kotlin
  Spring Boot : 3.1.x

  Dependencies:
  - Spring Web
  - Spring Data JPA
  - H2 Database

3. build.gradle.kts 설정

plugins {
      kotlin("jvm") version "1.9.0"
      kotlin("plugin.spring") version "1.9.0"
      kotlin("plugin.jpa") version "1.9.0"
      id("org.springframework.boot") version "3.1.0"
      id("io.spring.dependency-management") version "1.1.0"
  }

  dependencies {
      implementation("org.springframework.boot:spring-boot-starter-web")
      implementation("org.springframework.boot:spring-boot-starter-data-jpa")
      implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
      runtimeOnly("com.h2database:h2")
  }
  • plugin.spring : Kotlin 클래스는 기본이 final이라 Spring 프록시 생성과 충돌. 자동으로 open 처리
  • plugin.jpa : JPA Entity에 필요한 기본 생성자 자동 생성
  • jackson-module-kotlin : Kotlin data class 직렬화/역직렬화 지원

4. CRUD API 구현

4.1) Entity

@Entity
  class Member(
      val name: String,
      val email: String,
      @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
      val id: Long = 0
  )

4.2) Repository

interface MemberRepository : JpaRepository<Member, Long>

4.3) Service

@Service
  class MemberService(
      private val memberRepository: MemberRepository
  ) {
      fun getAll(): List<Member> = memberRepository.findAll()

      fun save(name: String, email: String): Member {
          return memberRepository.save(Member(name = name, email = email))
      }
  }

생성자에 선언하는 것만으로 의존성 주입. @Autowired 불필요

4.4) Controller

@RestController
  @RequestMapping("/members")
  class MemberController(
      private val memberService: MemberService
  ) {
      @GetMapping
      fun getAll(): List<Member> = memberService.getAll()

      @PostMapping
      fun save(@RequestBody request: MemberRequest): Member =
          memberService.save(request.name, request.email)
  }

  data class MemberRequest(
      val name: String,
      val email: String
  )

5. 결과 확인

./gradlew bootRun

5.1) 전체 조회

curl http://localhost:8080/members

5.2) 저장

curl -X POST http://localhost:8080/members \
    -H "Content-Type: application/json" \
    -d '{"name":"홍길동","email":"hong@test.com"}'

5.3) 저장 후 전체 조회 결과

[{"id":1,"name":"홍길동","email":"hong@test.com"}]

 

728x90
반응형

'Kotlin' 카테고리의 다른 글

[Spring Boot] @Valid 검증이 동작하지 않는 경우  (0) 2026.03.27

+ Recent posts