Validation API

Validation API

JSR-380에 정의된 자바 유효성 검사 표준


✔ Validation


Validation APIJSR-380 에 정의된 자바 플랫폼의 유효성 검사 표준으로 javax.validation 패키지에 위치한다.


💡 JSR(Java Specification Request) ?

자바 스펙 요구서로 자바 플랫폼에 추가된 사양 및 기술을 정의하는 공식 문서이다.


다양한 사용 방법이 있으나, 이번 포스팅에서는 별도의 유틸리티 클래스를 통해 도메인 계층에서 유효성을 검증하는 방법을 작성한다.

Validation API에 대한 조금 더 자세한 사용방법들은 Baeldung - Validation 을 참고해보자.


설치


// file: 'build.gradle'

dependencies {
    implementation 'javax.validation:validation-api:2.0.1.Final'
    implementation 'org.hibernate.validator:hibernate-validator:7.0.1.Final'
}


만약 스프링 부트를 사용한다면 스타터를 지원한다.

// file: 'build.gradle'

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-validation'
}


사용


다음과 같은 클래스가 있다.


@Value(staticConstructor = "of")
public class Cat {

    String name;
    Long age;

}


고양이의 이름과 나이가 절대로 비어있으면 안된다고 가정하면 다음과 같은 코드를 매번 작성해야만 한다.


public class CatService {

    public void doSomething(Cat cat) {
        Objects.requireNonNull(cat, "cat must not be null");
        Objects.requireNonNull(cat.getName(), "cat name must not be null");
        Objects.requireNonNull(cat.getAge(), "cat age must not be null");
        // other codes...
    }

}


이런 귀찮은 작업을 모두 대신 처리해주는게 Validation API라고 볼 수 있는데, 이것을 사용하면 다음과 같이 바꿀 수 있다.


@Value(staticConstructor = "of")
public class Cat {

    @NotNull // 고양이 이름은 null일 수 없다
    String name;

    @NotNull // 고양이 나이는 null일 수 없다
    Long age;

}


이제 어노테이션을 사용하려면 일반적으로 @Valid@Validated 를 사용하면 되는데, 이는 컨트롤러 레이어에 사용시에만 제대로 동작한다.


@Validated@Valid를 포함하는 포괄적인 개념이라고 봐도 무방하겠다.


왜 컨트롤러 레이어에서만 제대로 동작하느냐면, Spring MVC를 사용 할 경우 제공되는 인터셉터에 위 어노테이션들을 사용해 검증하는 상세 구현이 있기 때문이다.


하지만 유효성 검사를 컨트롤러 레이어에 종속시키는 것보다 도메인에서 담당하게 하는 것이 설계상 조금 더 좋다고 보는데, 서비스 레이어에서 위 어노테이션들을 사용 할 경우 이 어노테이션들이 제대로 동작하지 않는 문제가 존재한다.

왜냐하면 서비스 레이어는 Spring MVC에서 구현한 인터셉터의 영향을 받지 않기 때문이다. (굳이 동작하게 하려면 번거로운 짓을 좀 해야한다.)

따라서 컨트롤러 레이어를 제외한 곳에서 사용 할 별도의 검증기를 만들어 주면 매우 유용하다.


// 일단 정적 메서드로 다 해결할 수 있으므로 추상 클래스로 작성
public abstract class ValidateUtils {

    // 어노테이션 기반으로 검증을 처리해주는 검증기를 선언
    private static final Validator VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator();

    // 추후에 확장될수도 있으므로 기본 생성자를 작성
    public ValidateUtils() {
    }

    // 검증 메서드
    public static void validate(@NonNull Object... objects) {
        // 넘어온 인자를 순회하며 검증
        for (Object object : objects) {
            // 인자에 선언된 어노테이션으로 검증. 만약 유효성 검사에 통과하지 못하면 에러메시지를 반환한다
            Set<ConstraintViolation<Object>> violations = VALIDATOR.validate(object);
            
            // 유효성 검사에 통과하지 못했다면 에러메시지가 들어있을 것이다.
            // 즉, isEmpty()==false일 경우 유효성 검사에 통과하지 못했음을 의미한다.
            if (!violations.isEmpty()) {
                throw new ConstraintViolationException(violations);
            }
        }
    }

}


이후 도메인 코드는 다음과 같이 바뀔 수 있다.


public class CatService {

    public void doSomething(Cat cat) {
        ValidateUtils.validate(cat);
        // other codes...
    }

}


class CatServiceTest {

    private CatService catService;

    @BeforeEach
    void setUp() {
        catService = new CatService();
    }

    // 테스트가 성공한다
    @Test
    void doSomething() throws Exception {
        assertThatThrownBy(() -> catService.doSomething(Cat.of(null, null)))
            .isInstanceOf(ConstraintViolationException.class)
            .hasMessage("age: 널이어서는 안됩니다, name: 널이어서는 안됩니다");
    }

}


하지만 위 방식에도 아주 큰 단점이 존재하는데, 모든 도메인에 검증하는 코드를 매번 추가로 작성해야 한다는 것이다.

이러한 것을 횡단관심사라고 부르며 이러한 문제를 해결할 수 있는 아주 좋은 방법이 존재하는데, 그것이 AOP이다.

별도의 Aspect를 작성하면 위의 단점또한 아주 쉽게 해결할 수 있다.

하지만 Aspect를 이 포스팅에서 함께 다루기엔 그 자체로 심오한 내용이 많기 때문에 주제가 묻힐것 같다는 생각이 든다.

따라서 그냥 이런 문제가 존재하고, 이런 문제를 어떻게 해결할 수 있다 정도만 알고있도록 하자.



© 2022. All rights reserved.