ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring Boot 고급 시리즈 4편 – 커스텀 Validation과 도메인 유효성 설계 전략
    기술과 산업/언어 및 프레임워크 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
Designed by Tistory.