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) |
| 적용 범위 | 개별 파라미터 단위 | 클래스 또는 메소드 레벨 |
- 표준 어노테이션: Jakarta Bean Validation 표준 스펙
- 중첩 검증 지원: 객체 내부의 중첩된 객체도 검증 가능
- 개별 파라미터 검증: 특정 파라미터에만 적용
@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 객체도 검증됩니다.
- Controller 메소드 파라미터에
@Valid적용 - Spring이 요청을 받으면
MethodArgumentNotValidException발생 가능 - 중첩된 객체는 필드에
@Valid를 명시해야 검증됨
- 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)
-
복잡한 DTO 객체 검증
@PostMapping public ResponseEntity<?> create(@Valid @RequestBody CreateRequest request)
-
중첩 객체 검증이 필요한 경우
public class OrderRequest { @Valid // 중첩 객체 검증 활성화 private List<OrderItem> items; }
-
표준 스펙 준수가 필요한 경우
- Jakarta Bean Validation 표준만 사용하고 싶을 때
-
클래스 전체에 검증 활성화
@RestController @Validated // 모든 메소드의 파라미터 검증 활성화 public class OrderController { ... }
-
검증 그룹이 필요한 경우
@PostMapping public ResponseEntity<?> create( @Validated(CreateGroup.class) @RequestBody Request request)
-
단순 타입 검증
@GetMapping("/{id}") public ResponseEntity<?> get( @PathVariable @Positive Long id) // @Validated가 클래스에 있어야 동작
-
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));
}
}-
@Valid
- Jakarta 표준 스펙
- 복잡한 객체와 중첩 객체 검증
- 메소드 파라미터에만 사용
-
MethodArgumentNotValidException발생
-
@Validated
- Spring 전용 확장
- 검증 그룹 지원
- 클래스 레벨과 메소드 레벨 모두 사용 가능
- 단순 타입 검증 가능
-
ConstraintViolationException발생
-
Controller 클래스에는
@Validated를 클래스 레벨에 적용 -
복잡한 DTO 파라미터에는
@Valid사용 -
중첩 객체 필드에는
@Valid사용 -
검증 그룹이 필요한 경우
@Validated(Group.class)사용 -
Service Layer에서 메소드 파라미터 검증이 필요하면
@Validated사용