BindProject

[Backend] Auth모듈 : Validator 만들기

dding-shark 2025. 6. 18. 17:36
728x90

유효성 검사기 만들기 – enum 기반 Validator 만들기

1. 도입

서비스 개발을 하다 보면 이메일, 비밀번호 등 입력값 유효성을 검증하는 로직은 필수적으로 들어간다.
하지만 이 유효성 로직이 이곳저곳 흩어져 있으면, 중복되고 테스트도 어렵고 책임도 애매해진다.

우리는 이를 해결하고자 입력값 검증을 단일한 구조로 추상화하고, 관리 가능한 구조로 개선하기로 했다.


2. 설계 목표

  • 각 검증 로직을 클래스 단위로 책임 분리한다.
  • 검증 대상 타입에 따라 검증기를 자동 주입/선택 가능하도록 한다.
  • OCP(Open-Closed Principle)를 지키며 확장 가능하도록 설계한다.
  • 단위 테스트가 가능해야 하며, 검증 로직의 재사용성을 높인다.

3. 설계 고려사항

타입 기반 매핑

EmailValidator, PasswordValidator처럼 구체 검증기는 모두 동일한 Validator<T> 인터페이스를 구현하고,
검증할 대상 타입(support() 반환값)에 따라 ValidatorFactory가 자동으로 연결되도록 설계했다.

DI 환경 고려

스프링 환경에서 @Component 스캔을 통해 모든 validator를 자동 주입하게 구성했다.
Map<Class<?>, Validator<?>> 형태로 관리해 ValidatorFactory에서 사용할 수 있도록 했다.

정규식 캡슐화

각 검증 클래스는 정규식을 내부에서 캡슐화하여 외부에 노출되지 않도록 구성했다.


4. 구현 설명

Validator 인터페이스

public interface Validator<T> {
    boolean validate(T value);
    Class<T> support();
}

EmailValidator

@Component
public class EmailValidator implements Validator<String> {
    private static final Pattern EMAIL_PATTERN = Pattern.compile(
        "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$"
    );

    @Override
    public boolean validate(String email) {
        return email != null && EMAIL_PATTERN.matcher(email).matches();
    }

    @Override
    public Class<String> support() {
        return String.class;
    }
}

ValidatorFactory

@Component
public class ValidatorFactory {

    private final Map<Class<?>, Validator<?>> validatorMap;

    public ValidatorFactory(List<Validator<?>> validators) {
        this.validatorMap = validators.stream()
            .collect(Collectors.toMap(Validator::support, Function.identity()));
    }

    @SuppressWarnings("unchecked")
    public <T> Validator<T> getValidator(Class<T> type) {
        Validator<T> validator = (Validator<T>) validatorMap.get(type);
        if (validator == null) {
            throw new IllegalArgumentException("Validator not found for type: " + type.getSimpleName());
        }
        return validator;
    }
}

5. 테스트 전략

  • 단일 유효성 검사기는 단위 테스트로 정규식 검증을 수행한다.
  • ValidatorFactory는 각 타입에 맞는 validator를 정확히 반환하는지 검증한다.

EmailValidatorTest

@DisplayName("EmailValidator 테스트")
class EmailValidatorTest {

    private final EmailValidator validator = new EmailValidator();

    @Test
    void 이메일_형식이면_통과() {
        assertTrue(validator.validate("user@example.com"));
    }

    @Test
    void null이면_실패() {
        assertFalse(validator.validate(null));
    }

    @Test
    void 이상한_문자면_실패() {
        assertFalse(validator.validate("not-an-email"));
    }
}
class PasswordValidatorTest {

    private final PasswordValidator validator = new PasswordValidator();

    @Test
    void 강력한_비밀번호_통과() {
        assertThat(validator.validate("Str0ng!Passw0rd")).isTrue();
    }

    @Test
    void 약한_비밀번호_실패() {
        assertThat(validator.validate("weakpass")).isFalse();
        assertThat(validator.validate("1234567890")).isFalse();
        assertThat(validator.validate(null)).isFalse();
    }
}

class ValidatorFactoryTest {

    private ValidatorFactory factory;

    @BeforeEach
    void setUp() {
        // 수동 DI
        factory = new ValidatorFactory(
                List.of(new EmailValidator(), new PasswordValidator())
        );
        factory.initialize();
    }

    @Test
    void EMAIL_검증기_조회_성공() {
        Validator<String> validator = factory.getValidator(ValidatorType.EMAIL);
        assertThat(validator).isNotNull();
        assertThat(validator.validate("test@example.com")).isTrue();
    }

    @Test
    void PASSWORD_검증기_조회_성공() {
        Validator<String> validator = factory.getValidator(ValidatorType.PASSWORD);
        assertThat(validator).isNotNull();
        assertThat(validator.validate("Str0ng!Passw0rd")).isTrue();
    }

    @Test
    void 존재하지_않는_타입_조회_시_NULL() {
        // ValidatorType이 더 추가된다면 대비
        assertThat(factory.getValidator(null)).isNull();
    }
}

6. 회고 및 확장 가능성

  • 각 validator는 독립적으로 동작하며, OCP에 따라 쉽게 추가/변경 가능하다.
  • factory 구조를 통해 단일 책임 원칙이 명확하게 지켜지고 있다.
  • 타입 기반이라는 한계는 있지만, 대부분의 케이스에서는 충분히 유연하게 대응 가능하다.
  • 향후 @Validated + ConstraintValidator 방식으로 스프링 검증기와 통합할 수도 있다.

728x90