Spring/Spring

[Spring][Validation] 2. Java Bean Validation

noahkim_ 2025. 4. 6. 21:34

1. jakarta.validation

  • 자바 애플리케이션에서 공통된 유효성 검증을 위한 Bean Validation API

 

Validator

항목 설명
정의 도메인 객체에 정의된 제약 조건을 검증함
유효성 검증 방법 수동적으로 호출하여 사용
- validator.validate(object)

 

예제)

더보기
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());
}
  • 기본적으로 예외를 던지지 않고 Set<ConstraintViolation<T>> 반환 (검증 실패 항목들의 정보가 담긴 집합)

 

ConstraintValidator

항목 설명
정의 커스터마이징하여 도메인 객체에 정의된 제약 조건을 검증함
유효성 검증 방법 도메인 모델의 필드에 annotation(제약 조건)을 선언하여 유효성 검증 수행
제네릭 타입 <A, T>
- A: 애너테이션 타입
- T: 검증할 값의 타입입니다.
사용 위치 커스텀 제약 애너테이션과 함께 사용되며, @Constraint(validatedBy = ...)에서 지정됩니다.
빈 등록 여부 Spring에서는 자동으로 등록되며, 별도로 Bean으로 등록할 필요는 없습니다.

 

@Constraint
항목 설명
목적
사용자 정의 제약 사항 어노테이션을 선언할 때 사용
필수 속성 (validatedBy) 검사 로직을 구현한 클래스 지정
→ ConstraintValidator<A, T> 구현체
메시지 커스터마이징 message 속성을 통해 기본 에러 메시지 지정 

 

예제) 커스터마이징

더보기

1. 커스텀 @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}

 

2. 커스텀 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");
    }
}

 

예제) Hibernate

더보기
public class PersonForm {
    @NotNull
    @Size(max=64)
    private String name;

    @Min(0)
    private int age;
}
  • Hibernate라는 벤더에서 @Size와 같은 제약 어노테이션의 jakarta.validation.ConstraintValidator 구현체들을 제공함

 

2. Spring Bean Validation

  • Spring은 Bean Validation API를 완전하게 지원
  • Spring 환경에서 Validator들을 통합하고 자동으로 유효성 검증을 수행함
    (jakarta.validation.Validator 및 org.springframework.validation.Validator)

 

Validator 제공자 빈 등록

LocalValidatorFactoryBean
@Configuration
public class AppConfig {
    @Bean
    public LocalValidatorFactoryBean validator() {
        return new LocalValidatorFactoryBean();
    }
}
항목 설명
정의
Spring에서 Bean Validation 제공자를 감지하고 등록하는 기본 Validator Bean
구현 인터페이스
jakarta.validation.Validator
org.springframework.validation.Validator
주요 역할
Bean Validation API(jakarta.validation.Validator)와
Spring Validation API(org.springframework.validation.Validator)를 통합하여 사용 가능하게 함
자동 감지
클래스패스에 있는 모든 Bean Validation 제공자들을 자동으로 감지하여 사용
적용 대상
도메인 모델 필드, 메서드 파라미터, 커맨드 객체 등
사용 예시
Spring MVC, Spring WebFlux, 메서드 유효성 검사 등

 

예제) LocalValidatorFactoryBean 사용

더보기
@Service
public class MyService {
    @Autowired
    private jakarta.validation.Validator validator;
    // private org.springframework.validation.Validator validator; // 이것도 됨
}

 

메서드 유효성 검사 빈 등록

MethodValidationPostProcessor
@Configuration
public class ApplicationConfiguration {
    @Bean
    public static MethodValidationPostProcessor validationPostProcessor() {
        return new MethodValidationPostProcessor();
    }
}
항목 설명
정의
메서드 호출 시, 인자나 반환값에 대한 Bean Validation을 수행하게 해주는 Spring Bean
역할
@Validated 또는 @Valid가 붙은 메서드의 파라미터, 반환값 검증 처리
내부 동작
AOP 기반으로 동작
- 메서드 호출 전에 validator로 유효성 검증을 수행

 

사용

@Service
@Validated
public class MyService {
    public void addStudent(@Valid Person person, @Max(2) int degrees) {
        // ...
    }
    
    @NotNull  // 반환값이 null이면 예외 발생
    public String getUserName(Long id) {
        // DB에서 못 찾았다고 치자
        return null;
    }
}
  • @Validated 필수 (있어야 MethodValidationPostProcessor가 작동함)
  • 파라미터나 리턴값에 @Valid, 제약 어노테이션(@Size, @Min 등) 사용

 

3. Spring Bean Validation 부가기능

메시지 커스터마이징

  • 제약 조건 실패 시 발생하는 에러 메시지를 MessageSource 기반으로 커스터마이징 가능

 

예제) ValidationMessages.properties

더보기

1. 메시지 프로퍼티 파일 생성

# src/main/resources/ValidationMessages.properties

Size.person.name=이름은 {2}자 이상 {1}자 이하여야 합니다.
NotBlank.person.name=이름은 필수 항목입니다.
Max.age=나이는 {1} 이하여야 합니다.
  • {0}: 필드명
  • {1}, {2}: 제약 조건 파라미터

 

2. 제약 조건 어노테이션에 메시지 키 지정 (@Constraint 속성)

public class Person {

    @Size(min = 2, max = 10, message = "{Size.person.name}")
    private String name;

    @Max(value = 100, message = "{Max.age}")
    private int age;
}
  • message = "{메시지 키}" 형태로 지정하면 MessageSource에서 해당 메시지를 찾아 사용함.

 

3. MessageSource 빈 등록하기

@Configuration
public class MessageConfig {

    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("ValidationMessages"); // 확장자 없이 basename만
        messageSource.setDefaultEncoding("UTF-8");
        return messageSource;
    }
}
  • 커스터마이징 메시지 프로퍼티를 사용하기 위해 설정 필요
  • 다국어 지원 시, ValidationMessages_ko.properties, ValidationMessages_en.properties 등 추가 가능

 

4. LocalValidatorFactoryBean에 MessageSource 연결

@Bean
public LocalValidatorFactoryBean getValidator(MessageSource messageSource) {
    LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
    validator.setValidationMessageSource(messageSource);
    return validator;
}

 

예외 처리

ConstraintViolationException
  • 검증에 실패할 경우 기본적으로 jakarta.validation.ConstraintViolationException 이 발생
  • Validator에서 발생하는 매우 로우레벨의 예외
  • 직접 핸들링할 경우 불편함이 발생함
문제점 설명
예외 메시지 파싱 필요
어떤 필드에서 어떤 문제가 생겼는지 파악하려면 constraintViolations를 직접 순회해야 해요.
비즈니스 예외와 구분 힘듦
ConstraintViolationException은 스프링의 일반적인 예외 처리 흐름과 조금 결이 달라요.
(ex. ResponseStatusException)
공통 포맷 통일 어려움
모든 검증 실패 응답을 공통 JSON 구조로 만들고 싶을 때, 이걸 일일이 다뤄야 함.

 

커스터마이징
  • 예외를 비즈니스 계층에 다루기 쉽게 만들기
  • MethodValidationException 같은 커스텀 예외로 변환하기

 

예제

더보기

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;
    }
}

 

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 Interface  (0) 2025.04.06
[Spring][Object] 1. Data Binding  (0) 2025.04.06
[Spring][Field] 2. Formatting  (0) 2025.04.06