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
728x90
반응형

1. 개요

마이크로서비스 아키텍처와 같이 작은 서비스들로 이루어진 환경에서 복잡한 트랜잭션과 데이터 일관성을 효과적으로 관리하기 위해 SAGA 패턴이 사용됩니다. 이 패턴은 여러 서비스 간의 트랜잭션을 조율하고 데이터 일관성을 유지하기 위한 강력한 도구로 활용됩니다.

 

분해된 큰 트랜잭션:

SAGA 패턴은 큰 트랜잭션을 작은 단위의 작업으로 분해합니다. 각 작업은 개별적인 서비스로 구현되며, 작은 범위의 트랜잭션을 조율하는 것이 가능해집니다.

 

단위 작업의 트랜잭션:

각 단위 작업은 자체적인 트랜잭션을 가집니다. 이러한 작은 트랜잭션은 각 서비스 내에서 안전하게 수행

될 수 있습니다.

 

SAGA 관리 로직:

SAGA 패턴은 전체 트랜잭션의 흐름을 관리하는 로직을 필요로 합니다. 이 로직은 각 단계의 작업이 순차적으로 진행될 수 있도록 조율하며, 작업의 완료 여부를 확인합니다.

 

복잡한 트랜잭션의 관리:

다중 서비스 간의 복잡한 트랜잭션을 분해하고 조율함으로써 전체 시스템의 확장성과 견고성을 높일 수 있습니다.

 

구현의 복잡성과 주의사항:

SAGA 패턴은 구현이 복잡하며, 각 작업이 성공적으로 완료되지 않을 경우 컴펜세이팅 트랜잭션을 관리해야 합니다. 따라서 신중한 계획과 테스트가 필요합니다.

 

2. 종류 

 

추가/수정 패턴 (Compensating Transaction Pattern): 

이 방식은 특정 작업에 대한 성공적인 완료가 보장되지 않을 경우, 이전 작업을 취소하는 로직을 구현합니다. 이전 작업을 취소하는 로직을 "보상 트랜잭션" 또는 "컴펜세이팅 트랜잭션"이라고 합니다. 이러한 컴펜세이팅 트랜잭션들이 순차적으로 실행되며, 일련의 작업들의 트랜잭션을 조율하게 됩니다.

 

사가 로깅 패턴 (Saga Logging Pattern): 

이 방식은 각 단계의 트랜잭션 상태와 진행 여부를 기록하는 로깅 메커니즘을 사용하여 트랜잭션의 상태를 추적합니다. 각 작업이 시작되고 완료될 때 로그를 남기고, 다음 작업으로 진행 여부를 확인할 수 있도록 합니다.

 

3. 장단점

 

장점

분산 시스템에서 트랜잭션 조율: 마이크로서비스 아키텍처와 같은 분산 시스템에서 여러 서비스 간의 트랜잭션을 조율하는데 유용합니다. 각 서비스의 트랜잭션을 분리하여 더 작은 범위에서 관리하고, 전체 시스템의 유연성과 확장성을 향상시킵니다.

부분 실패에 대한 대응: 일부 서비스에서 오류가 발생하여 트랜잭션이 실패할 경우, 해당 작업만 롤백하고 다른 작업은 계속 진행할 수 있습니다. 이로써 전체 트랜잭션을 망가뜨리지 않고 일부 실패를 처리할 수 있습니다.

변경의 용이성: 각 단위 작업은 개별적인 서비스로 구현되므로, 각 서비스의 변경이 다른 서비스에 영향을 덜 주며 유지보수가 용이합니다.

 

단점

구현 복잡성: SAGA 패턴은 복잡한 구조를 가지며, 작은 단위의 트랜잭션과 그에 따른 컴펜세이팅 트랜잭션을 구현해야 합니다. 이로 인해 개발과 유지보수가 어려울 수 있습니다.

컴펜세이팅 트랜잭션 처리의 어려움: 각 작업이 성공적으로 완료된 후 다음 작업으로 진행하는 것이 아니라, 중간에 실패할 경우 이전 작업을 롤백하고 컴펜세이팅 트랜잭션을 실행해야 합니다. 이런 컴펜세이팅 트랜잭션의 구현은 복잡하고 예외 처리가 중요합니다.

장애 복구 어려움: 트랜잭션 조율 중에 실패가 발생하거나 일부 서비스가 다운되면 전체 SAGA 트랜잭션이 실패할 수 있습니다. 이러한 상황에서의 복구 작업은 복잡하고 신중한 처리가 필요합니다.

응답 시간 지연: 각 작업이 별도의 트랜잭션으로 실행되기 때문에 전체 SAGA 트랜잭션이 완료될 때까지 시간이 걸릴 수 있습니다. 이로 인해 응답 시간이 지연될 수 있습니다.

 

 

SAGA 패턴은 복잡한 분산 시스템에서 효과적인 데이터 일관성과 트랜잭션 관리를 위한 방법이지만, 그 구현과 관리가 쉽지 않을 수 있습니다. 사용 시에는 장점과 단점을 고려하여 상황에 맞게 적용해야 합니다.

728x90
반응형

'이론 정리' 카테고리의 다른 글

Blocking/Non-blocking, Sync와 Async: 개념과 예시  (0) 2023.08.17
[OOP] 객체지향 프로그래밍  (0) 2021.02.27
한글 인코딩  (0) 2019.06.04
가상화 ( IO )  (0) 2018.02.11
가상화 ( 서버 )  (0) 2018.02.11

+ Recent posts