Spring/Spring

[Spring][Validation] 2. Java Bean Validation

noahkim_ 2025. 4. 6. 21:34

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