1. Validator 인터페이스 (jakarta.validation)
- 자바 애플리케이션에서 공통된 유효성 검증을 위한 Bean Validation API
 - ✅ 어노테이션 기반 검증 (표준 애노테이션 지원)
 - ✅ 검증 결과 반환 (Set<ConstraintViolation<T>>)
 - ❌ 기본적으로 예외를 던지지 않음
 
예제
더보기
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Person person = new Person(); // name=null, age=–1
Set<ConstraintViolation<Person>> violations = validator.validate(person);
for (ConstraintViolation<Person> violation : violations) {
    System.out.println(violation.getPropertyPath() + ": " + violation.getMessage());
}
2. ConstraintValidator (jakarta.validation)
- 제약조건 어노테이션을 검증할 때 쓰는 인터페이스
 
| 항목 | 설명 | 
| 제네릭 타입 | - A: 애너테이션 타입 - T: 검증할 값의 타입  | 
| 사용 위치 | @Constraint(validatedBy = ...)에서 지정됨 | 
예제) 커스텀 ConstraintValidator 정의
더보기
public class MyConstraintValidator implements ConstraintValidator<MyConstraint, String> {
    @Autowired
    private Foo aDependency;
    @Override
    public void initialize(MyConstraint constraintAnnotation) {
        // 초기화 로직 (필요 없다면 생략 가능)
    }
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        // 실제 유효성 검사 로직
        return value != null && value.startsWith("ABC");
    }
}
@Constraint
- 사용자 정의 제약 사항 어노테이션 선언할 때 쓰는 애노테이션
 
| 속성 | 설명 | 
| validatedBy (필수) | 검사 로직을 구현한 클래스 지정 (ConstraintValidator<A, T> 구현체) | 
| message 속성 | 기본 에러 메시지 지정 (메시지 커스터마이징 가능) | 
예제) 커스텀 @Constraint 정의
더보기
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ... })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MyConstraintValidator.class)
public @interface MyConstraint {
    String message() default "유효하지 않습니다.";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}
| 속성 | 설명 | 사용 예시 | 
| message | 유효성 검사 실패 시 반환할 기본 에러 메시지입니다. ✅ ResourceBundle로 다국어 처리 가능  | 
 message = "이름은 비어있을 수 없습니다" 
 | 
| groups | 검증 그룹 지정 특정 그룹에서만 유효성 검사를 수행하도록 할 때 사용됨  | 
 groups = {Create.class, Update.class} 
 | 
| payload | 메타데이터 정보 커스텀 처리가 필요한 경우 활용됨  | 
 payload = {Severity.High.class} 
 | 
예제) Hibernate 제공
더보기
public class PersonForm {
    @NotNull
    @Size(max=64)
    private String name;
    @Min(0)
    private int age;
}
- Hibernate라는 벤더에서 표준 제약 어노테이션의 jakarta.validation.ConstraintValidator 구현체들을 제공함
 - ✅ Hibernate Validator가 내부적으로 표준 제약 애노테이션의 검증기들의 매핑을 등록해 둠
 - ✅ @Size ↔️ SizeValidationForCollection, SizeValidationForShortArray 등
 
3. @Valid vs @Validated
| 구분 | @Valid  (jakarta.validation)  | 
 @Validated  
(org.springframework.validation.annotation)  | 
| 소속 | 표준 Bean Validation 트리거 (Jakarta) | Spring의 AOP 기반 검증 활성화 | 
| 주 용도 | - DTO/엔티티 필드 검증  - 컨트롤러 파라미터 검증  | 
 메서드 레벨 검증 그룹 검증  (파라미터/리턴값) 
 | 
| 적용 위치 | 컨트롤러 파라미터 (@RequestBody, @ModelAttribute) | 
 - 클래스 레벨(@Service, @Controller)  
- 메서드 파라미터/리턴  | 
| 그룹 검증 | ❌ 지원 안 함 | 
 ✅ 지원 (@Validated(Group.class)) 
 | 
| 발생 예외 | - MethodArgumentNotValidException (@RequestBody)  - BindException (@ModelAttribute)  | 
 - ConstraintViolationException (메서드 파라미터/리턴 검증 위반) 
 | 
예제) @Validated - 그룹 검증
더보기
// 3-1) 그룹 인터페이스
public interface Create {}
public interface Update {}
// 3-2) DTO 제약에 그룹 지정
public record UserProfileCmd(
        @NotBlank(groups = {Create.class}) String email,
        @NotBlank(groups = {Create.class, Update.class}) String name,
        @Size(min = 8, groups = {Create.class}) String password
) {}
@Validated
@Service
public class UserProfileService {
    // 생성 시: Create 그룹으로 검증 (email, name, password 모두 체크)
    @Validated(Create.class)
    public void create(@Valid UserProfileCmd cmd) {
        // ...
    }
    // 수정 시: Update 그룹으로 검증 (name만 필수)
    @Validated(Update.class)
    public void update(@Valid UserProfileCmd cmd) {
        // ...
    }
}
4. 예외 인터페이스
BindingResult
- 바인딩 결과를 표현하는 인터페이스 입니다.
 - ✅ BindException의 부모 인터페이스
 - ✅ Errors 인터페이스를 확장함
 - ✅ 예외가 발생할 경우, 검증기는 유효하지 않은 검증결과를 BindingResult에 저장해줍니다.
 
예시) List<FieldError>
더보기
- 유효성을 위반한 필드의 에러내용을 담은 클래스의 리스트입니다.
 - BindingResult를 접근하여 사용자에게 예외메시지를 생성하는데 활용됩니다.
 
public record ValidationResult(List<FieldErrorDetail> errors) {
    public static ValidationResult of(Errors errors) {
        List<FieldErrorDetail> details =
                errors.getFieldErrors()
                        .stream()
                        .map(error -> FieldErrorDetail.of(error))
                        .collect(Collectors.toList());
        return new ValidationResult(details);
    }
}
public record FieldErrorDetail(String objectName, String field, String code, String message) {
    public static FieldErrorDetail of(FieldError error) {
        return new FieldErrorDetail(
                error.getObjectName(),
                error.getField(),
                error.getCode(),
                error.getDefaultMessage()
        );
    }
}
4. 예외 클래스
MethodArgumentNotValidException (org.springframework.web.bind)
- @RequestBody 데이터 바인딩 중, 검증 실패가 발생할 경우 던져지는 예외
 - ✅ 기본적으로 HTTP 400 오류로 처리됩니다.
 - ✅ 예외클래스 내부 필드에 BindingResult라는 객체가 있으며 검증 오류를 보관합니다.
 
BindException
- @ModelAttribute 데이터 바인딩 중, 검증 실패가 발생할 경우 던져지는 예외
 
ConstraintViolationException
- @Validated에 의해 트리거 된 검증에서 오류가 발생하는 경우 던져지는 예외
 - ✅ 객체가 아닌 단일 필드에 메서드 시그니처에서 직접검사시에 사용됨 (@PathVariable, @RequestParam)
 
5. 예외 처리 커스터마이징
MethodArgumentNotValidException / BindException
예제) 커스터마이징
더보기
1. 전역 예외 처리 핸들러 등록
@ExceptionHandler(value = {MethodArgumentNotValidException.class, BindException.class})
public ResponseEntity<?> handleMethodArgumentNotValidException(Exception e) {
    BindingResult bindingResult = e instanceof MethodArgumentNotValidException manv ? manv.getBindingResult() : (BindingResult) e;
    
    // 예외 처리
}
ConstraintViolationException
- ConstraintViolationException은 매우 로우레벨의 예외
 - ❌ 직접 핸들링할 경우 불편함이 발생함
 
| 문제점 | 설명 | 
| 예외 메시지 파싱 필요 | 
 어떤 필드에서 어떤 문제가 생겼는지 파악하려면 constraintViolations를 직접 순회해야 함 
 | 
| 비즈니스 예외와 구분 힘듦 | 
 ConstraintViolationException은 스프링의 일반적인 예외 처리 흐름과 조금 결이 다름 
(ex. ResponseStatusException)  | 
| 공통 포맷 통일 어려움 | 
 모든 검증 실패 응답을 공통 JSON 구조로 만들고 싶을 때, 이걸 일일이 다뤄야 함. 
 | 
예제) 커스터마이징
더보기
1. MethodValidationPostProcessor 커스터마이징
public class CustomMethodValidationPostProcessor extends MethodValidationPostProcessor {
    @Override
    protected MethodValidationInterceptor createMethodValidationInterceptor(Validator validator) {
        return new MethodValidationInterceptor(validator) {
            @Override
            public Object invoke(MethodInvocation invocation) throws Throwable {
                try {
                    return super.invoke(invocation);
                } catch (ConstraintViolationException ex) {
                    // 예외를 감싸서 던지기
                    throw new CustomValidationException(ex.getConstraintViolations());
                }
            }
        };
    }
}
2. Spring Bean으로 등록
@Configuration
public class ValidationConfig {
    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor(Validator validator) {
        CustomMethodValidationPostProcessor processor = new CustomMethodValidationPostProcessor();
        processor.setValidator(validator);
        return processor;
    }
}
3. CustomValidationException 정의
public class CustomValidationException extends RuntimeException {
    private final Set<ConstraintViolation<?>> violations;
    public CustomValidationException(Set<ConstraintViolation<?>> violations) {
        super("Validation failed");
        this.violations = violations;
    }
    public Set<ConstraintViolation<?>> getViolations() {
        return violations;
    }
}
- MethodValidationException 같은 커스텀 예외로 변환하기
 
4. @ExceptionHandler로 통합 처리
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(CustomValidationException.class)
    public ResponseEntity<Map<String, Object>> handleCustomValidation(CustomValidationException ex) {
        Map<String, String> errors = new HashMap<>();
        for (ConstraintViolation<?> v : ex.getViolations()) {
            String field = v.getPropertyPath().toString();
            String message = v.getMessage();
            errors.put(field, message);
        }
        Map<String, Object> response = new HashMap<>();
        response.put("code", "VALIDATION_FAILED");
        response.put("errors", errors);
        return ResponseEntity.badRequest().body(response);
    }
}
출처
'Spring > Spring' 카테고리의 다른 글
| [Spring][AOP] 2. Pointcut API (1) | 2025.04.09 | 
|---|---|
| [Spring][AOP] 1. Advice API (0) | 2025.04.09 | 
| [Spring][Validation] 1. Validator (0) | 2025.04.06 | 
| [Spring][Object] 1. Data Binding (0) | 2025.04.06 | 
| [Spring][Field] 2. Formatting (0) | 2025.04.06 |