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
'BindProject' 카테고리의 다른 글
| [Backend] Auth모듈 :사용자 등록 로직의 책임 분리와 유효성 검사 개선 (2) | 2025.06.18 |
|---|---|
| [Backend] AUTH모듈 : 초기 세팅 (0) | 2025.06.18 |
| [기획] BFF에서의 필터링 전략과 정합성 보장 설계의 관해 (2) | 2025.06.18 |
| [Backend] JWT 토큰 모듈 설계 및 구현기 (1) | 2025.06.17 |
| [Backend] BFF 구현 하기 앞서 구상 및 정리 (0) | 2025.06.17 |