tech/Spring

[Spring] @Valid와 @Validated

sunm2n 2025. 11. 12. 14:49

@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가 중첩되어 있다."

이때 두 가지 요구사항이 생긴다.

  1. 그룹 검증: '회원 가입'(OnCreate) 그룹에 해당하는 규칙만 검증해야 한다.
  2. 중첩 검증: 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;
}

동작 순서:

  1. 클라이언트가 /users/join으로 요청을 보낸다.
  2. Spring은 @Validated(UserRequest.OnCreate.class)를 보고, UserRequest 객체 검증을 시작한다. (이때 OnCreate 그룹 규칙만 활성화된다.)
  3. id 필드(@Null)와 name 필드(@NotBlank)를 OnCreate 그룹 규칙에 따라 검증한다.
  4. address 필드를 만난다. @NotNull로 null이 아님을 확인한다.
  5. @Valid 어노테이션을 보고, address 객체 내부로 '내려가서' 검증을 계속한다.
  6. Address 객체의 city와 zipCode 필드의 @NotBlank 규칙을 검증한다.

4. 왜 DTO 필드에는 @Validated를 쓰지 않지?

간단히 말해, Spring의 유효성 검사기가 DTO 내부 필드를 검사할 때 @Valid를 찾도록 설계되었기 때문이다.

  1. @Validated의 주된 임무 (at Controller/Service):
    • 메서드(파라미터) 레벨에서 "지금부터 이 객체의 유효성 검사를 시작해라!"라고 Spring에게 알린다.
    • 이때 어떤 그룹으로 검사할지(@Validated(OnCreate.class)) 지정하는 것이 핵심이다.
  2. @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)도 있다면, 이 둘을 조합해서 사용하는 것이다.