2026년 4월 7일 작성

Spring Bean Validation - Annotation 기반 객체 값 검증

Bean Validation은 annotation 기반으로 객체의 field 값을 검증하는 Java 표준 spec이며, Spring에서는 `@Valid`와 함께 DTO에서 요청 data를 검증하는 데 사용됩니다.

Bean Validation

  • Bean Validation은 Java 표준 spec(JSR 380)으로, annotation을 사용하여 객체의 field 값을 선언적으로 검증합니다.
    • jakarta.validation package에 정의된 annotation을 field에 붙이면, framework가 자동으로 값을 검증합니다.
    • Hibernate Validator가 가장 널리 사용되는 구현체입니다.
  • 보통 DTO에서 validation을 수행합니다.
    • 각 API의 request와 response에 맞추기 위해 domain이 수정되어서는 안 됩니다.
    • 필수 값 검사, DTO에서 domain으로의 변환 등의 logic은 domain이 아닌 DTO의 책임입니다.
    • @NotNull 같은 data validation도 DTO에 넣으면 역할과 책임이 명확해집니다.
public class UserLoginRequestDto {
    @NotNull(message = "이름은 Null일 수 없습니다.")
    @Size(min = 1, max = 10, message = "이름은 1 ~ 10자여야 합니다.")
    private String name;

    @NotNull(message = "email은 Null일 수 없습니다.")
    @Email
    private String email;
}

주요 Annotation

  • Bean Validation은 null, 빈 문자열, 공백 문자열을 구분하는 세 가지 annotation을 제공합니다.

@NotNull

  • null만 허용하지 않습니다.
    • ""(빈 문자열)이나 " "(공백 문자열)은 통과합니다.
    • null이 들어오면 logic에 예상치 못한 오류가 발생하는 경우에 사용합니다.

@NotEmpty

  • null""(빈 문자열) 둘 다 허용하지 않습니다.
    • @NotNull에 빈 문자열 검사가 추가된 것입니다.
    • " "(공백 문자열)은 통과합니다.

@NotBlank

  • null, "", " " 모두 허용하지 않습니다.
    • @NotEmpty에 공백 문자열 검사가 추가된 것입니다.
    • 세 annotation 중 validation 강도가 가장 높습니다.
annotation null "" " "
@NotNull 거부 허용 허용
@NotEmpty 거부 거부 허용
@NotBlank 거부 거부 거부

기타 Annotation

  • Bean Validation은 다양한 type에 맞는 검증 annotation을 제공합니다.
annotation 설명
@Size(min, max) 문자열 길이 또는 collection 크기 제한
@Min(value) 숫자 최솟값
@Max(value) 숫자 최댓값
@Email email 형식 검증
@Pattern(regexp) 정규 표현식 검증
@Positive 양수만 허용
@PositiveOrZero 0 또는 양수 허용
@Past 과거 날짜만 허용
@Future 미래 날짜만 허용

Spring에서의 사용

  • Spring MVC에서는 @Valid annotation을 controller parameter에 붙여 자동 검증을 수행합니다.

Controller 설정

  • @Valid@RequestBody 앞에 붙이면, request body를 DTO로 변환한 후 자동으로 validation을 수행합니다.
    • validation에 실패하면 MethodArgumentNotValidException이 발생합니다.
@PostMapping("/login")
public ResponseEntity<BaseResult> login(@Valid @RequestBody UserLoginRequestDto loginUser) {
    UserLoginResponseDto login = userService.login(loginUser);
    return new ResponseEntity<>(new BaseResult.Normal(login), HttpStatus.OK);
}

@Validated

  • Spring이 제공하는 annotation으로, @Valid와 달리 group 기반 검증을 지원합니다.
    • 같은 DTO를 여러 API에서 사용하되, API마다 다른 검증 규칙을 적용할 때 유용합니다.
public class UserDto {
    public interface Create {}
    public interface Update {}

    @NotNull(groups = {Create.class, Update.class})
    private String name;

    @NotNull(groups = Create.class)
    private String password;
}
@PostMapping("/users")
public ResponseEntity<User> create(@Validated(UserDto.Create.class) @RequestBody UserDto dto) {
    return ResponseEntity.ok(userService.create(dto));
}

@PutMapping("/users/{id}")
public ResponseEntity<User> update(@Validated(UserDto.Update.class) @RequestBody UserDto dto) {
    return ResponseEntity.ok(userService.update(dto));
}

예외 처리

  • validation 실패 시 발생하는 MethodArgumentNotValidException@ExceptionHandler로 처리합니다.
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<BaseResult> handleMethodArgumentNotValidException(
        MethodArgumentNotValidException e) {
    String errorMessage = e.getBindingResult()
            .getAllErrors()
            .get(0)
            .getDefaultMessage();

    return new ResponseEntity<>(
            new BaseResult.Normal(INVALID_PARAMETER),
            HttpStatus.BAD_REQUEST);
}
  • BindingResult를 controller parameter로 받으면, exception을 발생시키지 않고 직접 error를 처리합니다.
@PostMapping("/login")
public ResponseEntity<BaseResult> login(
        @Valid @RequestBody UserLoginRequestDto loginUser,
        BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
        String errorMessage = bindingResult.getAllErrors()
                .get(0).getDefaultMessage();
        return ResponseEntity.badRequest()
                .body(new BaseResult.Normal(errorMessage));
    }
    return ResponseEntity.ok(new BaseResult.Normal(userService.login(loginUser)));
}

Custom Validation

  • 기본 annotation으로 표현할 수 없는 검증 logic은 custom annotation과 ConstraintValidator로 구현합니다.
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneNumberValidator.class)
public @interface PhoneNumber {
    String message() default "올바른 전화번호 형식이 아닙니다.";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}
public class PhoneNumberValidator implements ConstraintValidator<PhoneNumber, String> {

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null) {
            return true;
        }
        return value.matches("^010-\\d{4}-\\d{4}$");
    }
}
public class UserDto {
    @PhoneNumber
    private String phone;
}

Reference


목차