📂 목차
- Business Entity
- Bean Validation 의존성 추가
- Bean Validation 을 통한 비즈니스 룰 검증
- Hibernate Validator Annotations
- Custom Bean Validation Annotation
📚 본문
비즈니스 엔티티 유효성 검증을 위한 밸리데이션을 사용을 생각할 수 있다.
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 들을 가져와서 검증을 수행하면 된다.