언어 및 프레임워크/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