언어 및 프레임워크/Spring Boot
Spring Boot 고급 시리즈 4편 – 커스텀 Validation과 도메인 유효성 설계 전략
B컷개발자
2025. 4. 21. 19:35
728x90
Spring Boot에서 @Valid를 넘어서는 고급 Validation 전략을 소개합니다. 커스텀 애노테이션 생성과 도메인 중심 유효성 설계 방식을 실무 예제로 정리했습니다.
Spring Boot 고급 시리즈 4편 – 커스텀 Validation과 도메인 유효성 설계 전략
기본적인 유효성 검사는 @Valid와 @NotBlank, @Email 등의 애노테이션으로 충분합니다.
하지만 복잡한 비즈니스 조건이 추가되면, 다음과 같은 고민이 생기기 시작하죠.
- "이메일이 이미 존재하는지 DB에서 체크해야 해"
- "두 필드의 값이 서로 일치해야 하는데 어떻게 검사하지?"
- "도메인 정책에 따라 유효성 로직이 달라져야 해"
이번 글에서는 이런 복잡한 검증을 아름답고 유지보수 가능하게 처리하는 방법을 설명합니다.
🧱 1. 한계에 부딪힌 기본 유효성 검증
public class RegisterUserRequest {
@NotBlank
@Email
private String email;
@NotBlank
private String password;
@NotBlank
private String confirmPassword;
}
이 구조로는 다음과 같은 검증을 할 수 없습니다:
- 이메일 중복 여부(DB 조회 필요)
- password와 confirmPassword의 일치 여부
- 도메인에 따라 유효성 조건이 달라지는 경우
✅ 2. 커스텀 검증 애노테이션 만들기
예제: 이메일 중복 여부 검사
1️⃣ 애노테이션 생성
@Documented
@Constraint(validatedBy = EmailNotExistsValidator.class)
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface EmailNotExists {
String message() default "이미 사용 중인 이메일입니다.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
2️⃣ Validator 구현
@Component
public class EmailNotExistsValidator implements ConstraintValidator<EmailNotExists, String> {
private final UserRepository userRepository;
public EmailNotExistsValidator(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return value != null && !userRepository.existsByEmail(value);
}
}
3️⃣ DTO 적용
public class RegisterUserRequest {
@NotBlank
@Email
@EmailNotExists
private String email;
@NotBlank
private String password;
@NotBlank
private String confirmPassword;
}
- @EmailNotExists는 DB 검증을 자동화하며, 코드 분리와 가독성을 크게 향상시킵니다.
🔁 3. 필드 간 검증 – Cross-field Validation
예제: 비밀번호와 확인 비밀번호 일치 여부
1️⃣ 애노테이션 정의
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PasswordMatchesValidator.class)
public @interface PasswordMatches {
String message() default "비밀번호와 확인 비밀번호가 일치하지 않습니다.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
2️⃣ Validator 구현
public class PasswordMatchesValidator implements ConstraintValidator<PasswordMatches, RegisterUserRequest> {
@Override
public boolean isValid(RegisterUserRequest dto, ConstraintValidatorContext context) {
return dto.getPassword().equals(dto.getConfirmPassword());
}
}
3️⃣ DTO 적용
@PasswordMatches
public class RegisterUserRequest {
private String password;
private String confirmPassword;
// getter/setter 생략
}
🧠 4. 도메인 계층으로 유효성 이관하기
복잡한 정책 기반 검증은 DTO에서 해결할 수 없습니다. 이럴 땐 도메인 객체 내부에서 검증하는 것이 정답입니다.
@Entity
public class User {
public void changePassword(String currentPassword, String newPassword) {
if (!this.password.equals(currentPassword)) {
throw new BusinessException(ErrorCode.INVALID_PASSWORD);
}
this.password = newPassword;
}
}
- 도메인은 스스로의 유효성을 스스로 판단해야 합니다.
- 검증 로직이 도메인 외부에 흩어지지 않도록 주의하세요.
🧪 5. 테스트 전략
검증 대상 테스트 위치
커스텀 Validator | Validator 단위 테스트 (@WebMvcTest 가능) |
도메인 유효성 | Domain 단위 테스트 (JPA 없이도 가능) |
DTO @Valid 적용 | Controller 통합 테스트로 검증 |
✅ 마무리 요약
항목 전략 요약
기본 한계 | @Valid만으로 복잡한 검증 처리 어려움 |
커스텀 Validator | @EmailNotExists, @PasswordMatches 등 재사용 가능 구조 |
도메인 검증 | 엔티티 내부에서 책임 있게 정책 적용 |
테스트 | Validator/Domain 계층 별도 테스트 가능 |
유지보수성 | 조건 변경 시 DTO 수정 없이 Validator 교체만으로 대응 |
📌 다음 편 예고
Spring Boot 고급 시리즈 5편: REST API 버전 관리 전략 – URI vs Header 방식 실무 적용법
728x90