@Valid와 @Validated는 모두 유효성 검증(Validation)을 위해 사용되지만, 제공 주체와 핵심 기능에 차이가 있다.
1. @Valid (Java 표준)
- 제공자: Java
- 패키지: javax.validation.Valid (Spring Boot 2.x) 또는 jakarta.validation.Valid (Spring Boot 3.x)
- 핵심 기능: 중첩된 객체(Nested Object)의 유효성 검사를 실행하도록 지시한다.
@Valid는 컨트롤러 메서드의 파라미터(ex. @RequestBody)나 DTO 내부의 필드에 붙어, "이 객체의 필드도 검증해줘"라고 알리는 역할을 한다.
예시 (DTO 내부의 중첩된 객체 검증):
public class UserRequest {
@NotBlank(message = "이름은 필수입니다.")
private String name;
@Valid // <- 이 Address 객체 내부의 필드도 검증하라는 의미
@NotNull(message = "주소는 필수입니다.")
private Address address;
// ...
}
public class Address {
@NotBlank(message = "도시는 필수입니다.")
private String city;
@NotBlank(message = "우편번호는 필수입니다.")
private String zipCode;
}
위 예시에서 UserRequest를 검증할 때, address 필드에 @Valid가 없다면 Address 객체 내부의 city나 zipCode가 비어있어도 검증 오류가 발생하지 않는다. @Valid를 붙여야만 해당 객체 내부까지 '내려가서' 검증을 수행한다.
2. @Validated
- 제공자: Spring Framework
- 패키지: org.springframework.validation.annotation.Validated
- 핵심 기능: 유효성 검사 그룹(Validation Groups)을 지정할 수 있게 해준다. (표준 @Valid에는 이 기능이 없다.)
@Validated는 @Valid의 모든 기능을 포함하면서, 특정 시나리오(그룹)에 따라서만 유효성 검사 규칙을 적용할 수 있게 확장된 기능이다.
예시 (그룹 지정): 회원 가입(Create) 시에는 ID가 없어야 하지만, 정보 수정(Update) 시에는 ID가 꼭 있어야 하는 규칙을 정의할 수 있다.
public class UserDto {
// 그룹을 정의하기 위한 인터페이스
public interface OnCreate {}
public interface OnUpdate {}
@Null(groups = OnCreate.class, message = "가입 시 ID는 불필요합니다.")
@NotNull(groups = OnUpdate.class, message = "수정 시 ID는 필수입니다.")
private Long id;
@NotBlank(groups = {OnCreate.class, OnUpdate.class}, message = "이름은 필수입니다.")
private String name;
}
3. @Valid와 @Validated를 둘다 쓰는 이유
상황: "회원 가입(OnCreate 그룹) 기능을 구현하는데, UserRequest DTO 안에 Address DTO가 중첩되어 있다."
이때 두 가지 요구사항이 생긴다.
- 그룹 검증: '회원 가입'(OnCreate) 그룹에 해당하는 규칙만 검증해야 한다.
- 중첩 검증: UserRequest 내부의 Address 객체까지 검증해야 한다.
이 두 가지를 해결하기 위해 @Validated와 @Valid를 조합해야한다.
- @Validated: 컨트롤러에서 그룹을 지정하기 위해 사용한다.
- @Valid: DTO 내부에서 중첩된 객체를 검증하기 위해 사용한다.
코드 예시:
1. 컨트롤러 (Controller) @Validated를 사용해 OnCreate 그룹을 지정한다.
@RestController
@RequestMapping("/users")
public class UserController {
@PostMapping("/join")
public ResponseEntity<String> joinUser(
// 👇 그룹 지정을 위해 @Validated 사용
@Validated(UserRequest.OnCreate.class) @RequestBody UserRequest request
) {
// ... 로직 수행 ...
return ResponseEntity.ok("가입 성공");
}
}
2. DTO (Data Transfer Object) @Valid를 사용해 중첩된 Address 객체를 검증하도록 설정한다.
// 그룹 인터페이스 정의
public interface ValidationGroups {
interface OnCreate {}
interface OnUpdate {}
}
// UserRequest DTO
public class UserRequest {
@Null(groups = ValidationGroups.OnCreate.class)
@NotNull(groups = ValidationGroups.OnUpdate.class)
private Long id;
@NotBlank(groups = ValidationGroups.OnCreate.class, message = "이름은 필수입니다.")
private String name;
// 👇 중첩된 객체 검증을 위해 @Valid 사용
@Valid
@NotNull(groups = ValidationGroups.OnCreate.class, message = "주소는 필수입니다.")
private Address address;
// ...
}
// Address DTO
public class Address {
// ❗️ 중요: Address 필드에는 그룹을 지정하지 않습니다.
// 만약 그룹을 지정하면, 부모 DTO의 @Valid가 동작해도
// 컨트롤러에서 지정한 그룹(OnCreate)과 일치하지 않아 검증이 안 될 수 있습니다.
// (이 문제를 해결하는 @ConvertGroup이라는 것도 있지만, 일단 기본은 이렇습니다.)
@NotBlank(message = "도시는 필수입니다.")
private String city;
@NotBlank(message = "우편번호는 필수입니다.")
private String zipCode;
}
동작 순서:
- 클라이언트가 /users/join으로 요청을 보낸다.
- Spring은 @Validated(UserRequest.OnCreate.class)를 보고, UserRequest 객체 검증을 시작한다. (이때 OnCreate 그룹 규칙만 활성화된다.)
- id 필드(@Null)와 name 필드(@NotBlank)를 OnCreate 그룹 규칙에 따라 검증한다.
- address 필드를 만난다. @NotNull로 null이 아님을 확인한다.
- @Valid 어노테이션을 보고, address 객체 내부로 '내려가서' 검증을 계속한다.
- Address 객체의 city와 zipCode 필드의 @NotBlank 규칙을 검증한다.
4. 왜 DTO 필드에는 @Validated를 쓰지 않지?
간단히 말해, Spring의 유효성 검사기가 DTO 내부 필드를 검사할 때 @Valid를 찾도록 설계되었기 때문이다.
- @Validated의 주된 임무 (at Controller/Service):
- 메서드(파라미터) 레벨에서 "지금부터 이 객체의 유효성 검사를 시작해라!"라고 Spring에게 알린다.
- 이때 어떤 그룹으로 검사할지(@Validated(OnCreate.class)) 지정하는 것이 핵심이다.
- @Valid의 주된 임무 (at DTO Field):
- 이미 유효성 검사가 시작된 객체 내부의 필드에 붙어서, "이 필드 안에 있는 객체(중첩 객체)도 이어서 검증해 줘!"라고 '연쇄 검증'을 요청하는 '표준 마커' 역할을 한다.
놀이공원에 한번 비유를 해보면
- @Validated(OnCreate.class) (컨트롤러의 @RequestBody 앞):
- 이것은 놀이공원 정문 입장권이다.
- "나는 OnCreate(청소년) 티켓으로 입장할래!"라고 검사 시작과 그룹을 지정한다.
- @Valid (DTO 안의 Address 필드 앞):
- 이것은 정문 입장권에 딸린 '유아 동반' 스티커이다.
- 정문 직원이 입장권을 검사하다가 이 스티커(@Valid)를 보고 "아 동반한 유아(Address 객체)가 있군요. 이 유아의 소지품(필드)도 검사해야겠다"라고 안으로 한 단계 더 들어가서 검사한다.
만약 DTO 필드에 @Validated를 붙이면? 정문 직원이 유아 스티커(@Valid)를 찾아야 하는데, 뜬금없이 또 다른 정문 입장권(@Validated)을 보여주는 꼴이다. Spring의 검사기는 DTO 필드 레벨에서는 '유아 스티커'(@Valid)만 인식하고, @Validated는 중첩 검증 신호로 인식하지 않는다.
결론적으로 @Validated가 더 많은 기능을 가졌지만, 중첩 검증을 지시하는 표준 마커 역할은 여전히 Java 표준인 @Valid가 수행한다. Spring은 이 표준을 존중하고 그대로 사용하는 것이다.
그래서 컨트롤러에서는 그룹 지정을 위해 @Validated를 쓰고, DTO 내부에서는 중첩 검증을 위해 @Valid를 쓰는 조합이 되는 것이다.
5. 요약 및 비교
| 구분 | @Valid (Java 표준) | @Validated (Spring 제공) |
| 패키지 | jakarta.validation.Valid | org.springframework.validation.annotation.Validated |
| 핵심 기능 | 중첩된 객체의 유효성 검사 (Cascading Validation) | 유효성 검사 그룹 지정 |
| 사용 위치 | 1. DTO 내부의 필드 (중첩 객체) 2. 컨트롤러 메서드 파라미터 (그룹 불필요 시) |
1. 컨트롤러 메서드 파라미터 (그룹 지정 시) 2. 서비스( @Service ) 클래스 (메서드 레벨 검증) |
| 그룹 기능 | ❌ (불가능) | ⭕ (가능) |
결론:
- 단순히 DTO를 검증하고 중첩된 객체도 검증하려면 @Valid만 써도 된다.
- "가입", "수정"처럼 상황(그룹)에 따라 다른 규칙을 적용해야 한다면 @Validated를 쓴다.
- 그룹별 검증(@Validated)을 하는데 중첩된 객체(@Valid)도 있다면, 이 둘을 조합해서 사용하는 것이다.
'tech > Spring' 카테고리의 다른 글
| [Spring] 인증(Authentication)과 인가(Authorization) (0) | 2025.12.03 |
|---|---|
| [Spring] SpringSecurity란? (0) | 2025.12.03 |
| Java에서 컬렉션 sort 동작 방식 (0) | 2025.11.05 |
| [Spring] @ActiveProfiles("test") vs @DataJpaTest (0) | 2025.08.21 |
| [Spring] @Builder(toBuilder = true) (1) | 2025.07.24 |