@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을 체크할 수 있을 것이다.
- 일반적인 조건문 체크 후 에러 처리
- java의 assert 사용
- Spring이 제공하는 Assert 사용
- validation annotation을 사용
무엇이 더 효율적이라고 말할 수 없지만 최대한 간결한 코드 유지를 판단하여 선택하고 사용하면 될 것 같다.
'Study > Java' 카테고리의 다른 글
JDK 17부터 Locale language old ISO code 사용 비활성으로 기본 설정 변경 (0) | 2022.11.23 |
---|---|
JDK 19 New Features (0) | 2022.09.21 |
Spring Boot GraphQL 사용해보기 (0) | 2022.09.02 |
Spring Rest Docs로 OpenAPI (Swagger) 문서를 만들어 Swagger UI로 호출하여 보기 (0) | 2022.06.17 |
Spring Cloud Config Server jdbc backend 사용해보기 (0) | 2022.06.13 |
[troubleshooting] eclipse (STS) 에서 refactor rename이 동작하지 않는 현상 (0) | 2022.06.08 |
Spring Boot project STS에서 열어보기 (0) | 2022.05.26 |
Spring Boot 2.7 Release Notes (0) | 2022.05.20 |
maven project에서 junit 5, assertJ 사용하기 (1) | 2022.04.25 |
LWJGL 공부 내용 기록 (Java로 게임 개발하기) (0) | 2022.04.25 |