서비스 계층에 Spring validation을 사용하지 말아야 하는 이유

찬디·2025년 5월 4일

우테코

목록 보기
6/19

선결론

  • 단위 테스트를 위해서 사용하지 말아야한다

Service에서 사용 예제 코드

public class User {

    @NotNull(message = "Name cannot be null")
    @Size(min = 2, max = 50, message = "Name must be between 2 and 50 characters")
    private String name;

    // Getters and setters
}

@Service
public class UserService {

    @Autowired
    private Validator validator;

    public void validateUser(User user) {
        Set<ConstraintViolation<User>> violations = validator.validate(user);
        if (!violations.isEmpty()) {
            for (ConstraintViolation<User> violation : violations) {
                System.out.println(violation.getMessage());
            }
        }
    }
}

validation을 사용하는 방법은 많지만 위와 같이 validation 기능을 사용했다고 하자

그럼 테스트 코드를 어떻게 작성해야할까?

[!NOTE]
(스프링에 익숙하지않은 사람들을 위해)
스프링 기능을 사용할려면 스프링 컨텍스트가 필요하다.
단지 new를 해서는 어노테이션 기반 스프링 기능이 동작하지 않을 수 있다는 사실만 생각하고 넘어가자.

public class UserServiceTest {

    @Test
    public void testValidateUser() {
        // 'new'로 객체 생성
        User user = new User();
        user.setName(null); // 유효성 검사 실패 조건

        UserService userService = new UserService();
        userService.validateUser(user);  // Validation 동작하지 않음
    }
}

따라서 위 코드는 동작하지 않는다.

그럼 어떻게할까?

SpringContext 주입


@SpringBootTest
public class UserServiceTest {

    @Autowired
    private UserService userService;

    @Autowired
    private Validator validator; // Spring에서 제공하는 Validator 빈 주입

    @Test
    public void testValidateUser() {
        // User 객체 생성
        User user = new User();
        user.setName(null); // 유효성 검사 실패 조건

        // Validation 수행
        Set<ConstraintViolation<User>> violations = validator.validate(user);

        // 검증 결과 확인
        assertFalse(violations.isEmpty()); // 유효성 검사 실패해야 함
        for (ConstraintViolation<User> violation : violations) {
            System.out.println(violation.getMessage());
        }
    }
}

스프링 컨텍스트를 주입하면 된다.

문제점

  • 문제점은, 스프링은 무겁다는 것이다.
    내부적으로 리플렉션을 사용해 어노테이션들을 처리하는 방식을 채택하고 있기 때문에
    컨텍스트를 전부 올리는것은 성능적으로 매우 큰 손해를 보는것이다.

    • 지금은 간단한 테스트 한개뿐이라 괜찮을 수 있지만, 만약 클래스가 100개,1000개가 넘어가면 매우 느려져서 몇시간이 걸릴수도 있는 것이다.
  • 이 문제를 가장 간단하게 해결하는 방법은 스프링 컨텍스트를 안쓰는 것이다.

ConstraintValidator 구현해보자

어차피 지금 테스트하고싶은것은 검증이니까 ConstratintValidatator를 구현하면되지않을까?

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CustomNameValidator.class)
public @interface ValidName {
    String message() default "이름이 유효하지 않습니다.";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}
public class CustomNameValidator implements ConstraintValidator<ValidName, String> {
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        return value != null && value.length() >= 2 && value.length() <= 50;
    }
}
public class CustomNameValidatorTest {

    private final CustomNameValidator validator = new CustomNameValidator();

    @Test
    void testValidName() {
        assertTrue(validator.isValid("홍길동", null));
    }

    @Test
    void testInvalidNameTooShort() {
        assertFalse(validator.isValid("홍", null));
    }

    @Test
    void testNullName() {
        assertFalse(validator.isValid(null, null));
    }
}

문제점

  • 위와 같이 구현하면, @ValidName이란 어노테이션을 붙이면 동작하기는 한다.
    - 그런데 이제 validation를 위한 클래스가 너무 많아진다.
    • 이렇게 할거면 spring validation을 사용함으로써 얻는 장점이 없어진다

new로만_객체_생성하면_스프링_검증기능_작동하지_않음
테스트 메서드를 살펴보면, 자카르타 validation 기능이 동작되지 않음을 알 수 있다.

만약 억지로 spring validation을 유지하면서 테스트를 하고 싶다면

직접_검증기_생성하면_유효성_수동_검사_가능
위 메서드와 같이 ConstraintViolation를 직접 명시해서 구현해야한다.

리뷰어분의 의견

결론

  • 비즈니스 로직 테스트는 순수할수록 좋다
  • 프레임워크도 의존성이다
  • 의존성도 줄일수록 좋다

스프링 기능을 무조건 많이 쓴다고 테스트가 간편해지는 것은 아니다.
오히려 번거로워질 수도 있다.

  • 추가로, 컨트롤러에서 dto에 있어서는 적극 사용해도 된다고 생각한다.
    webMvc쪽을 스프링에게 매우 의존하고있기 때문에 컨트롤러는 통합테스트로 보통 하기도하고.
profile
깃허브에서 velog로 블로그를 이전했습니다.

0개의 댓글