Spring Bean Validation — @Valid vs @Validated 정리 - f-lab-edu/msa-commerce-lab GitHub Wiki

비교 표

구분 @Valid @Validated
제공자 Jakarta Bean Validation (표준) Spring Framework
패키지 jakarta.validation.Valid org.springframework.validation.annotation.Validated
사용 위치 메소드 파라미터, 필드, 생성자 파라미터 클래스, 메소드 파라미터
중첩 검증 지원 (중첩 객체 검증 가능) 지원하지 않음
검증 그룹 지원하지 않음 지원 (Validation Groups)
적용 범위 개별 파라미터 단위 클래스 또는 메소드 레벨

1. @Valid (Jakarta Bean Validation)

특징

  • 표준 어노테이션: Jakarta Bean Validation 표준 스펙
  • 중첩 검증 지원: 객체 내부의 중첩된 객체도 검증 가능
  • 개별 파라미터 검증: 특정 파라미터에만 적용

사용 예제

Controller에서 사용

@RestController
@RequestMapping("/api/v1/products")
@RequiredArgsConstructor
public class ProductController {

    @PostMapping
    public ResponseEntity<ProductResponse> createProduct(
        @Valid @RequestBody ProductCreateRequest request) {
        // @Valid가 ProductCreateRequest의 검증 어노테이션을 활성화
        return ResponseEntity.ok(productCreateUseCase.createProduct(request));
    }

    @GetMapping
    public ResponseEntity<ProductPageResponse> retrieveProducts(
        @Valid ProductSearchRequest request) {
        // Query Parameter도 @Valid로 검증 가능
        return ResponseEntity.ok(productGetUseCase.searchProducts(request));
    }
}

중첩 객체 검증

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ProductVerifyRequest {

    @NotEmpty(message = "Product verification items cannot be empty.")
    @Valid  // 중첩 객체의 검증을 활성화
    private List<ProductVerifyItem> items;

    @Getter
    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    public static class ProductVerifyItem {

        @NotNull(message = "Product ID is required.")
        @Positive(message = "Product ID must be positive.")
        private Long productId;

        @NotNull(message = "Quantity is required.")
        @Positive(message = "Quantity must be positive.")
        private Integer quantity;
    }
}

중요: items 필드에 @Valid를 붙이면 List 내부의 각 ProductVerifyItem 객체도 검증됩니다.

검증 동작 방식

  1. Controller 메소드 파라미터에 @Valid 적용
  2. Spring이 요청을 받으면 MethodArgumentNotValidException 발생 가능
  3. 중첩된 객체는 필드에 @Valid를 명시해야 검증됨

2. @Validated (Spring Framework)

특징

  • Spring 전용 어노테이션: Spring Framework에서 제공
  • 검증 그룹 지원: 상황별로 다른 검증 규칙 적용 가능
  • 클래스 레벨 적용: 클래스 전체에 검증 활성화
  • 메소드 검증 지원: @PathVariable, @RequestParam 같은 단순 타입 검증 가능

사용 예제

클래스 레벨 적용

@RestController
@RequestMapping("/api/v1/orders")
@RequiredArgsConstructor
@Validated  // 클래스 레벨에 적용하여 모든 메소드의 파라미터 검증 활성화
public class OrderQueryController {

    @GetMapping("/{orderId}")
    public ResponseEntity<OrderResponse> getOrderById(
        @PathVariable UUID orderId) {  // 단순 타입도 검증 가능
        return ResponseEntity.ok(getOrderUseCase.getOrderById(orderId));
    }

    @GetMapping
    public ResponseEntity<PageResponse<OrderSummaryResponse>> getOrders(
        @Valid OrderSearchParams searchParams) {
        // @Valid와 함께 사용 가능
        return ResponseEntity.ok(getOrderUseCase.searchOrders(criteria));
    }
}

검증 그룹 사용

// 검증 그룹 정의
public interface CreateGroup {}
public interface UpdateGroup {}

// DTO에 그룹별 검증 규칙 적용
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class ProductRequest {

    @Null(groups = CreateGroup.class, message = "ID should be null for creation")
    @NotNull(groups = UpdateGroup.class, message = "ID is required for update")
    private Long id;

    @NotBlank(groups = {CreateGroup.class, UpdateGroup.class})
    private String name;

    @Positive(groups = {CreateGroup.class, UpdateGroup.class})
    private Integer price;
}

// Controller에서 그룹 지정
@RestController
@RequestMapping("/api/v1/products")
@RequiredArgsConstructor
public class ProductController {

    @PostMapping
    public ResponseEntity<ProductResponse> createProduct(
        @Validated(CreateGroup.class) @RequestBody ProductRequest request) {
        // CreateGroup 검증 규칙만 적용
        return ResponseEntity.ok(productService.create(request));
    }

    @PutMapping("/{id}")
    public ResponseEntity<ProductResponse> updateProduct(
        @PathVariable Long id,
        @Validated(UpdateGroup.class) @RequestBody ProductRequest request) {
        // UpdateGroup 검증 규칙만 적용
        return ResponseEntity.ok(productService.update(id, request));
    }
}

메소드 파라미터 검증

@Service
@Validated  // Service 레벨에서도 사용 가능
public class ProductService {

    public ProductResponse getProduct(
        @NotNull @Positive Long productId) {
        // 메소드 파라미터 직접 검증
        return productRepository.findById(productId)
            .orElseThrow(() -> new ProductNotFoundException(productId));
    }
}

실제 프로젝트 적용 예제

현재 프로젝트 구조

OrderQueryController
├── @Validated (클래스 레벨)
└── getOrders(@Valid OrderSearchParams searchParams)
    └── @Valid로 복잡한 객체 검증

ProductController
├── @Validated (클래스 레벨)
├── createProduct(@Valid @RequestBody ProductCreateRequest)
├── verifyProducts(@Valid @RequestBody ProductVerifyRequest)
│   └── 중첩 객체 검증 (@Valid List<ProductVerifyItem>)
└── retrieveProducts(@Valid ProductSearchRequest)

언제 무엇을 사용할까?

@Valid를 사용하는 경우

  1. 복잡한 DTO 객체 검증

    @PostMapping
    public ResponseEntity<?> create(@Valid @RequestBody CreateRequest request)
  2. 중첩 객체 검증이 필요한 경우

    public class OrderRequest {
        @Valid  // 중첩 객체 검증 활성화
        private List<OrderItem> items;
    }
  3. 표준 스펙 준수가 필요한 경우

    • Jakarta Bean Validation 표준만 사용하고 싶을 때

@Validated를 사용하는 경우

  1. 클래스 전체에 검증 활성화

    @RestController
    @Validated  // 모든 메소드의 파라미터 검증 활성화
    public class OrderController { ... }
  2. 검증 그룹이 필요한 경우

    @PostMapping
    public ResponseEntity<?> create(
        @Validated(CreateGroup.class) @RequestBody Request request)
  3. 단순 타입 검증

    @GetMapping("/{id}")
    public ResponseEntity<?> get(
        @PathVariable @Positive Long id)  // @Validated가 클래스에 있어야 동작
  4. Service Layer 검증

    @Service
    @Validated
    public class ProductService {
        public void process(@NotNull String value) { ... }
    }

조합 사용 (권장)

실무에서는 두 어노테이션을 조합해서 사용하는 것이 일반적입니다:

@RestController
@RequestMapping("/api/v1/products")
@RequiredArgsConstructor
@Validated  // 1. 클래스 레벨: 단순 타입 검증 활성화
public class ProductController {

    @PostMapping
    public ResponseEntity<ProductResponse> createProduct(
        @Valid @RequestBody ProductCreateRequest request) {  // 2. @Valid: 복잡한 객체 검증
        return ResponseEntity.ok(productService.create(request));
    }

    @GetMapping("/{id}")
    public ResponseEntity<ProductResponse> getProduct(
        @PathVariable @Positive Long id) {  // 3. @Validated 덕분에 단순 타입도 검증됨
        return ResponseEntity.ok(productService.get(id));
    }
}

예외 처리

발생하는 예외 타입

@RestControllerAdvice
public class GlobalExceptionHandler {

    // @Valid 검증 실패
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(
        MethodArgumentNotValidException ex) {
        List<ValidationError> errors = ex.getBindingResult()
            .getFieldErrors()
            .stream()
            .map(error -> new ValidationError(
                error.getField(),
                error.getDefaultMessage()))
            .toList();
        return ResponseEntity.badRequest().body(new ErrorResponse(errors));
    }

    // @Validated 검증 실패 (Path Variable, Request Param 등)
    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<ErrorResponse> handleConstraintViolationException(
        ConstraintViolationException ex) {
        List<ValidationError> errors = ex.getConstraintViolations()
            .stream()
            .map(violation -> new ValidationError(
                violation.getPropertyPath().toString(),
                violation.getMessage()))
            .toList();
        return ResponseEntity.badRequest().body(new ErrorResponse(errors));
    }
}

주요 차이점 요약

  1. @Valid

    • Jakarta 표준 스펙
    • 복잡한 객체와 중첩 객체 검증
    • 메소드 파라미터에만 사용
    • MethodArgumentNotValidException 발생
  2. @Validated

    • Spring 전용 확장
    • 검증 그룹 지원
    • 클래스 레벨과 메소드 레벨 모두 사용 가능
    • 단순 타입 검증 가능
    • ConstraintViolationException 발생

권장 사항

  1. Controller 클래스에는 @Validated를 클래스 레벨에 적용
  2. 복잡한 DTO 파라미터에는 @Valid 사용
  3. 중첩 객체 필드에는 @Valid 사용
  4. 검증 그룹이 필요한 경우 @Validated(Group.class) 사용
  5. Service Layer에서 메소드 파라미터 검증이 필요하면 @Validated 사용
⚠️ **GitHub.com Fallback** ⚠️