Developer.

6. Spring Boot Validation

📂 목차


📚 본문

비즈니스 엔티티 유효성 검증을 위한 밸리데이션을 사용을 생각할 수 있다.

Business Entity

업무에서 중요하게 다루는 대상(실체)를 의미하며, 주로 정보 시스템이나 소프트웨어 설계에서 사용된다. 현실세계의 개념을 추상화 한 데이터이다.

특징

  • 현실 세계의 명사적 개념을 추상화
  • 대부분 데이터베이스 테이블 단위로 구현됨
  • 시스템 전반에서 핵심 로직의 주체로 동작함

관련 개념으로는 도메인 객체, DTO 등이 있으나 검색해보길 바란다.

Bean Validation 의존성 추가

validation 기능을 사용하기 위해 다음 의존성을 추가해주자.

// Bean Validation
implementation 'org.springframework.boot:spring-boot-starter-validation'

Bean Validation 을 통한 비즈니스 룰 검증

쓰기 위해 비즈니스 엔티티를 선언하자.

import jakarta.validation.constraints.*;

public class Account implements IAccount {
    private int number;
    private String name;

    @Min(value=0, message = "Account should have a minimum of 0 money")
    private int money;

    public Account(int number) {}

    public void setMoney(int money) {
        this.money = money;
    }
}

위와 같이 @Min을 통해 최소로 가질 값을 지정할 수 있다.

Validator를 통해 위반 사항 출력

대충 이를 쓰기 위해 application 클래스에서 CommandLineRunner 를 구현해서 써보자.

@SpringBootApplication
@EnableConfigurationProperties(CustomProperties.class)
public class StudyApplication
		implements CommandLineRunner
{
    ...
    	@Override
	public void run(String... args) throws Exception {
		Account account = new Account(123);
		account.setMoney(-1);

		Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

		Set<ConstraintViolation<Account>> violations = validator.validate(account);

		violations.forEach(accountConstraintViolation -> {
			logger.error("A constraint violation has occurred. Violation details: [{}].", accountConstraintViolation);
		}); 
		// [{}] 여기에 accountConstraintViolation 내용이 들어감
	}
}

account 로는 최소값이 0이기 때문에 0을 준다면 에러가 발생할 것이다. Validator 에서 해당 도메인 객체가 비즈니스 룰을 만족했는지 검증을 하고, 검증 로그를 ConstraintViolation 열거형으로 반환한다.

buildDefaultValidatorFactory 는 단순 테스트 목적에서 사용된다. Spring Context에서는 @Autowired 를 사용하여 가져오길 바란다.

2025-06-18 21:04:38.793 [restartedMain] ERROR StudyApplication:127 - A constraint violation has occurred. Violation details: [ConstraintViolationImpl{interpolatedMessage='Account should have a minimum of 0 money', propertyPath=money, rootBeanClass=class com.example.study.entity.account.Account, messageTemplate='Account should have a minimum of 0 money'}].

위와 같이 출력되는 것을 볼 수 있다.

Hibernate Validator Annotations

범주 애너테이션 설명
Null 여부 @NotNull null이 아니어야 함
  @NotEmpty null, 빈 문자열 모두 허용 안 함 (공백은 허용됨)
  @NotBlank null, 빈 문자열, 공백문자 불가
문자열 관련 @Size(min, max) 길이 제한 (문자열, 배열, 리스트 등)
  @Pattern(regexp) 정규표현식 패턴 검사
  @Email 이메일 형식 검사
  @Length(min, max) 문자열 길이 제한 (hibernate-validator 고유)
숫자 관련 @Min(value) 최소값 (정수형)
  @Max(value) 최대값 (정수형)
  @DecimalMin(value) 최소값 (실수 포함)
  @DecimalMax(value) 최대값 (실수 포함)
  @Positive 양수만 허용
  @PositiveOrZero 양수 또는 0 허용
  @Negative 음수만 허용
  @NegativeOrZero 음수 또는 0 허용
  @Digits(i, f) 정수 i자리, 소수 f자리
날짜 관련 @Past 과거 날짜만 허용
  @PastOrPresent 과거 또는 오늘
  @Future 미래 날짜만 허용
  @FutureOrPresent 미래 또는 오늘
계층 객체 @Valid 중첩 객체의 유효성 검사 수행

Custom Bean Validation Annotation

비즈니스 엔티티 유효성 검증을 위 애너테이션 외에 입맛대로 검증을 수행하도록 하는 커스텀 빈 밸리데이션 애너테이션을 정의 할 수도 있다.

비밀번호 검증 애너테이션 만들기

커스텀 애너테이션을 만들기 위해 ConstraintValidator 인터페이스를 구현해야 된다. 제네릭 변수로 첫번째는 커스텀 밸리데이터 로직을 적용하게 해주는 애너테이션(애너테이션을 따로 정의해야 함)을 넣어주고, 두 번째로는 커스텀 애너테이션을 적용해야 하는 데이터 타입을 넣어주면 된다(밸리데이션을 수행하는 대상의 타입을 적어주라는 말).

import jakarta.validation.*;

import java.lang.annotation.*;

public class PasswordRuleValidator implements ConstraintValidator<> {

    @Override
    public void initialize(Annotation constraintAnnotation) {
        ConstraintValidator.super.initialize(constraintAnnotation);
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        return false;
    }
}

구현시 isValid 와 initialize가 생성된다. initialize는 ConstraintValidator가 특정 애너테이션에 대해 초기 설정을 수행하는 로직인데, 이는 첫번째 Generic을 인자로 받아 사용하게 된다.

만약 ConstraintValidator<Id, String> 으로 구현을 했다면, Id 에서 가져올 수 있는 변수로 멤버변수들을 선언하여 통해 나중에 isValid 에서 이 변수들을 활용해 다양한 검증을 수행할 수 있을 것이다. 하지만 여기서는 불필요하기 때문에 그냥 삭제한다.

제네릭 인자에 넣을 애너테이션을 구현해주자.

import jakarta.validation.*;

import java.lang.annotation.*;

@Target({ElementType.TYPE, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Password {
    // 기본 메시지 정의
    String message() default "Password do not adhere to the specified rule";
    Class<?>[] groups() default {};
    /*
     검증 대상 객체에 대한 부가적인 메타데이터를 제공하는 Payload를 사용
     어떤 검증 테스트가 실패했는지를 payload 객체로 받게 됨
     */
    Class<? extends Payload>[] payload() default {};
}

message는 위반 사항에 대한 출력, groups는 그룹별 검증 시 유용하며, 실무에서는 계층적 유효성 검증을 할 때 사용한다. 예를 들어 회원가입에서는 name, email, password 가 모두 필수지만, 로그인 시에는 email, password만 필요하게 된다. 이때 모든 필드를 한꺼번에 검증하면 불필요한 제약이 생기게 되는데, 이럴 때 groups로 유효성 검증을 분리시켜 상황에 맞게 필요한 것만 검증을 할 수 있다.

// interfaces def.class
public interface OnRegister {}
public interface OnLogin {}

// User.class
public class User {

    @NotBlank(groups = OnRegister.class)
    private String name;

    @NotBlank(groups = {OnRegister.class, OnLogin.class})
    private String email;

    @NotBlank(groups = {OnRegister.class, OnLogin.class})
    private String password;
}

// validate
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

User user = new User();
Set<ConstraintViolation<User>> violations =
    validator.validate(user, OnLogin.class);  // 로그인에 필요한 것만 검증

이제 다시 PasswordValidator 로 가서 isValid() 를 구현해주자.

여기서 Password 에 대한 비즈니스 검증은 굉장히 많이 쓰이기 때문에 라이브러리로 따로 구현이 되어 있다. passay 라이브러리를 추가하여 이를 쓰도록 하자.

implementation 'org.passay:passay:1.6.3'

Bean Validation 구현

import jakarta.validation.*;
import org.passay.*;

import java.util.*;

public class PasswordRuleValidator implements ConstraintValidator<Password, String> {
    private static final int MIN_COMPLEX_RULE = 2;
    private static final int MAX_REPETITIVE_CHARS = 3;
    private static final int MIN_SPECIAL_CASE_CHARS = 1;
    private static final int MIN_UPPER_CASE_CHARS = 1;
    private static final int MIN_LOWER_CASE_CHARS = 1;
    private static final int MIN_DIGIT_CASE_CHARS = 1;

    @Override
    public boolean isValid(String password, ConstraintValidatorContext context) {
        List<Rule> passwordRules = List.of(
                new LengthRule(8, 30),
                new CharacterCharacteristicsRule(
                        MIN_COMPLEX_RULE,
                        new CharacterRule(EnglishCharacterData.Special, MIN_SPECIAL_CASE_CHARS),
                        new CharacterRule(EnglishCharacterData.UpperCase, MIN_UPPER_CASE_CHARS),
                        new CharacterRule(EnglishCharacterData.LowerCase, MIN_LOWER_CASE_CHARS),
                        new CharacterRule(EnglishCharacterData.Digit, MIN_DIGIT_CASE_CHARS)
                ),
                new RepeatCharacterRegexRule(MAX_REPETITIVE_CHARS));

        PasswordValidator passwordValidator = new PasswordValidator(passwordRules);
        PasswordData passwordData = new PasswordData(password);
        RuleResult ruleResult = passwordValidator.validate(passwordData);
        return ruleResult.isValid();
    }
}

이제 이를 비즈니스 엔티티에 적용시켜주면 되지만, 하기 전에 어노테이션과 Validator 를 연결시켜줄 무언가가 필요하다. 지금은 설계만 끝난 것이지 어노테이션이 어떤 Validate 를 할지 명시를 안해주었기에 그냥 메타데이터만 넣게 된 것 뿐이다. 따라서 어노테이션에 가서 @Constraint를 사용하여 해당 어노테이션을 사용하는 target 에게 어떤 validator 를 쓸 것인지 명시를 해준다.

@Target({ElementType.TYPE, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PasswordRuleValidator.class)
public @interface Password {
	...

이제 엔티티에 적용시켜주자.

import com.example.study.validator.*;
import lombok.*;

@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private String name;
    @Password // 커스텀 애너테이션 사용
    private String password;
}

이제 동일하게 validator 를 통해 ConstraintViolation 들을 가져와서 검증을 수행하면 된다.


🔗 관련 출처