파란하늘의 지식창고
반응형

@Controller가 아닌 @Service, @Component 등에서 @Validated 사용하기

일반적으로 @Controller에서 @Validated를 사용하여 validation을 사용한다.

@PostMapping
public BlogArticle create(@RequestBody @Validated(BlogArticle.Create.class) BlogArticle blogArticle) {
    return blogArticleService.create(blogArticle);
}

service나 component에서도 @Validated annotation을 사용할 수 있다.

다만 controller에서 사용하는 것과 그 외의 layer에서 사용하는 방법이 좀 차이가 있다.

@Service
@Validated
public class BlogArticleService {

    @Autowired
    private BlogArticleRepository blogArticleRepository;
    
    @Validated(BlogArticle.Create.class)
    public BlogArticle create(@Valid BlogArticle blogArticle) {
        return blogArticleRepository.save(blogArticle);
    }
}

위처럼 대상 class와 method, parameter 세 위치에 @Validated, @Valid를  선언해야 한다.

왜 사용하는 방법에 차이가 있을까?

@Controller, @Service, @Repository, @Component  등 bean을 생성하기 위해 구분 지어진 @Component annotation들이 있다.

이 중 @Controller는 외부에서 받은 요청(Request)을 처리하는 가장 앞단의 @Component이며 사용자가 요청한 정보에 대한 validation을 체크한다.

따라서 validation 결과가 invalid 한 경우 해당 문제는 외부 요청에 의해 발생한 문제로 간주하기 때문에 사용자 오류로 처리한다.

하지만 @Service, @Component에서 validation을 체크할 경우 외부에서 받은 요청이 아닌 내부에서 내부로 요청한 내용에 대한 결과로 간주하기 때문에 시스템 오류로 처리한다.

처리하는 과정도 다르다.

@Controller는 DataBinder를 통해 HandlerMethodArgumentResolver에서 validation을 체크하며 이 경우 org.springframework.validation.Validator를 사용하며 validation 결과를 Error에 쌓아 BindException을 발생시킨다.

그 외의 경우는 MethodValidationPostProcessor에서 호출되는 MethodValidationInterceptor에서 javax.validation.Validator를 사용하여 체크하며 invalid 한 경우 ConstraintViolationException을 발생시킨다.

Spring의 기본 응답 설정은 ConstraintViolationException은 내부 오류로 간주하며 DB에서 오류가 발생한 경우 사용되는 DataIntegrityViolationException 보다는 낫지만 둘 다 동일하게 내부 오류로 판단하고 500 HttpStatus (Internal Server Error)로 처리하며 BindException의 경우 사용자 오류로 간주하고 400 HttpStatus (Bad Request)로 처리한다.

Controller와 Controller가 아닌 경우 처리가 다르고 보통 Controller에서 @Validated를 사용하는 방법을 주로 사용하지만 굳이 Service나 Component에서 @Validated를 사용하지 않을 이유는 없다.

Controller에서 쓰는 것처럼 쓰고 싶다면?

3군데 @Validated, @Valid를 지정하는 방법이 불편하다면 다음처럼 개별 aspect를 선언하여 사용하는 방법도 있다.

사용할 annotation을 만들고

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface BlueskyValidated {

    Class<?>[] value() default {};

}

해당 annotation에 대한 aop를 만든다.

@Aspect
public class BlueskyValidatedAspect {
    
    private final Validator validator;
    
    public BlueskyValidatedAspect(Validator validator) {
        this.validator = validator;
    }
    
    @Around(value = "execution(* *(.., @io.github.luversof.boot.autoconfigure.validation.annotation.BlueskyValidated (*), ..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        
        var parameters = method.getParameters();
        var targetIndex = -1;
        BlueskyValidated targetAnnotation = null;
        for (int i = 0; i < method.getParameters().length ; i++) {
            var annotation = parameters[i].getAnnotation(BlueskyValidated.class);
            if (annotation != null) {
                targetIndex = i;
                targetAnnotation = annotation;
                break;
            }
        }
        var targetObject = joinPoint.getArgs()[targetIndex];
        
        Set<ConstraintViolation<Object>> result = validator.validate(targetObject, targetAnnotation.value());
        
        if (!result.isEmpty()) {
            throw new ConstraintViolationException(result);
        }
        return joinPoint.proceed();
    }

}

해당 annotation을 @Service에서 사용하면 된다.

@Service
public class BlogArticleService {

    @Autowired
    private BlogArticleRepository blogArticleRepository;
    
    public BlogArticle create(@BlueskyValidated(BlogArticle.Create.class) BlogArticle blogArticle) {
        return blogArticleRepository.save(blogArticle);
    }
}

이렇게 되면 Controller에서와 동일하게 method parameter에 대해서만 custom @Validated annotation을 지정하여 사용할 수 있다.

내부 오류이기 때문에 ConstraintViolationException으로 처리되었다.

custom @Validated annotation을 사용하지 않더라도 여러 방법을 사용해 validation을 체크할 수 있을 것이다.

  1. 일반적인 조건문 체크 후 에러 처리
  2. java의 assert 사용
  3. Spring이 제공하는 Assert 사용
  4. validation annotation을 사용

무엇이 더 효율적이라고 말할 수 없지만 최대한 간결한 코드 유지를 판단하여 선택하고 사용하면 될 것 같다.

반응형
profile

파란하늘의 지식창고

@Bluesky_

도움이 되었다면 광고를 클릭해주세요