SpringBoot 로 개발을 하다 보면 클라이언트와 통신 중 데이터에 대해서 Validation(검증)이 필요한 경우가 있습니다.
검증 클래스를 별도로 생성하여 검증을 진행할 수 있지만 간단하게 어노테이션을 통해서 JSR 표준을 이용하여 검증도 가능합니다.
이번 글에서는 이 JSR 표준을 이용한 @Valid 어노테이션에 대해서 정리해보고자 합니다.
0. Dependency 추가
- Gradle(build.gradle)
implementation 'org.springframework.boot:spring-boot-starter-validation'
- Maven(pom.xml)
// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.6.6</version>
</dependency>
1. @Valid
- @Valid 는 JSR-303 표준 스펙(자바 진영)으로써 빈 검증기(Bean Validator)를 이용해 객체의 제약 조건을 검증하도록 하는 어노테이션입니다.
- Spring 에서는 일종의 어댑터인 LocalValidatorFactoryBean 이 제약 조건 검증을 처리합니다.
- 이를 사용하기 위해서는 빈으로 등록해야 하는데 위 Dependency 를 추가하면 사용할 수 있습니다.
1-1. 사용방법
- 간단하게 API 를 만들어보겠습니다.
@RestController
@Slf4j
public class TestController {
@PostMapping("/todos/save")
public ResponseEntity<String> createJsonTodo(@RequestBody @Valid TodoRequest request){
log.info("Post : Todo Save -: {}", request.toString);
return ResponseEntity.ok().body("Todo 객체 검증 성공");
}
}
- 클라이언트로부터 전달받는 파라미터에 @RequestBody 어노테이션을 붙이고 그 앞에 @Valid 어노테이션을 작성하면, RequestBody 로 들어오는 객체에 대해서 Validation(검증)을 수행합니다.
- @Valid 어노테이션의 검증의 세부적인 사항은 객체 안에 정의하여야 합니다.(여기서는 TodoRequest 에 정의)
@Getter
@Setter
@ToString
public class TodoRequest {
@NotNull(message = "제목은 필수입니다.")
private String title;
@NotEmpty(message = "내용은 필수입니다.")
private String content;
private boolean completed;
@Min(1)
@Max(7)
private int dDay;
}
- 예를 들어 @NotNull 어노테이션은 필드의 값이 null 이 아님을 확인하고, @Min 어노테이션은 해당 값의 최솟값을 지정할 수 있습니다.
- PostMan 으로 간단하게 @Valid 를 테스트하기 위해 content 값을 빈 값으로 요청하면 자동으로 일정 규격에 맞게 SpringBoot 에서 만든 에러를 응답합니다.
@Valid 와 검증 어노테이션을 같이 잘 사용하면, 객체 단에서 충분히 에러를 잡을 수 있다.
1-2. 동작원리
- SpringBoot 에서 모든 요청은 프론트 컨트롤러인 DispatcherServlet 을 통해 Controller 로 전달됩니다.
- 전달 과정에서는 컨트롤러 메소드의 객체를 만들어주는 ArgumentResolver 가 동작하는데, @Valid 어노테이션도 이 ArgumentResolver 에 의해 처리가 됩니다.
- 검증 도중 오류가 있다면 MethodArgumentNotValidException 예외가 발생하고, 디스패처 서블릿에 기본으로 등록된 예외 리졸버(Exception Resolver)인 DefaultHandlerExceptionResolver 에 의해 400 BadRequest 가 발생하게 됩니다.
Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public org.springframework.http.ResponseEntity<java.lang.String> com.todo.blog.controller.TestController.createJsonTodo(com.todo.blog.controller.TodoRequest): [Field error in object 'todoRequest' on field 'content': rejected value []; codes [NotEmpty.todoRequest.content,NotEmpty.content,NotEmpty.java.lang.String,NotEmpty]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [todoRequest.content,content]; arguments []; default message [content]]; default message [내용은 필수입니다.]] ]
- 이러한 내용으로 @Valid 는 기본적으로 Controller(컨트롤러)에서만 동작을 합니다.
- 다른 계층(ex: service)에서 사용하기 위해서는 @Validated 어노테이션과 결합하여 사용해야만 합니다.
2. @Validated
- 요청 파라미터의 유효성 검증은 Controller(컨트롤러)에서 최대한 처리하는 것이 좋습니다.
- 하지만, 개발 도중 불가피하게 다른 곳에서 파라미터를 검증해야 할 수 있는데, Spring 에서 이를 위해 AOP 기반으로 메소드의 요청을 가로채어 유효성 검증을 진행해주는 @Validated 어노테이션을 제공하고 있습니다.
- @Validated 어노테이션은 @Valid 어노테이션의 JSR 표준 기술이 아니며, 다르게 Spring Framework 에서 제공하는 어노테이션 및 기능입니다.
2-1. 사용방법
- Service 계층에서 주로 사용하고 @Validated 어노테이션을 붙여 사용하시면 됩니다.
@Service
@Validated
public class TestTodoService {
public ResponseEntity todoSave(@Valid TodoRequest request){
return ResponseEntity.ok().body("Todo 객체 검증 성공");
}
}
- PostMan 으로 간단하게 테스트하기 위해 content 값을 빈 값으로 요청하면 자동으로 일정 규격에 맞게 SpringBoot 에서 만든 에러를 응답합니다.
2-2. 동작 원리
- @Valid 는 특정 ArgumentResolver 에 의해 유효성 검사가 진행되었다면, @Validated 는 AOP 기반으로 메소드 요청을 Interceptor(인터셉터)하여 처리합니다.
- @Validated 어노테이션을 클래스 레벨에 선언하면 해당 클래스에 유효성 검증을 위한 인터셉터(MethodValidationInterceptor)가 등록됩니다.
- 클래스에 선언하게 되면 메소드들이 해당 클래스를 호출할 때 AOP 의 PointCut(포인트컷)으로써 요청을 가로채어 유효성 검증을 진행합니다.
- 이러한 이유로 @Validated 를 사용하면 계층과 무관하게 Controller, Service, Repository 등 스프링 빈에 등록되면 유효성 검증을 진행할 수 있습니다.
대신 @Validated 어노테이션은 클래스에 유효성 검증 인터셉터를 등록하도록 작성이 필요하고, @Valid 어노테이션은 검증을 진행할 메소드에 작성이 필요합니다.
- @Valid 어노테이션과 다르게 예외는 ConstaintViolationException 으로 발생합니다.
javax.validation.ConstraintViolationException: todoSave.request.content: 내용은 필수입니다. at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:120) ~[spring-context-5.3.23.jar:5.3.23] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.23.jar:5.3.23] at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763) ~[spring-aop-5.3.23.jar:5.3.23] at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:708) ~[spring-aop-5.3.23.jar:5.3.23] at com.todo.blog.service.TestTodoService$$EnhancerBySpringCGLIB$$9ae86a0a.todoSave(<generated>) ~[main/:na] at com.todo.blog.controller.TestController.createJsonTodo(TestController.java:25) ~[main/:na] at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na] at
...
3.@Valid 제약조건 어노테이션 정리
- JSR 표준 스펙은 다양한 제약 조건 어노테이션을 제공하고 있습니다.
Anotation | 제약조건 |
@NotNull | Null 검증 |
@Null | Null 만 입력 가능 |
@NotEmpty | Null 이 아니고, 빈 스트링("") 이 아닌지 검증(" " 은 허용) |
@NotBlank | Null 이 아니고, 공백(""과 " " 모두 포함)이 아닌지 검증 |
@Size(min=,max=) | 해당 값이 주어진 값 사이에 해당하는지 검증(String, Collection, Map, Array 에도 가능) |
@Pattern(regex=) | 주어진 패턴과 일치하는지 검증 |
@Max(숫자) | 지정 값 이하인지 검증 |
@Min(숫자) | 지정 값 이상인지 검증 |
@Future | 현재 보다 미래인지 검증 |
@Past | 현재 보다 과거인지 검증 |
@Positive | 양수만 가능 |
@PositiveOrZero | 양수와 0만 가능 |
@Negative | 음수만 가능 |
@NegativeOrZero | 음수와 0만 가능 |
이메일 형식인지 검증 | |
@Digits(integer=, fraction = ) | 대상 수가 지정된 정수와 소수 자리 수 보다 작은지 검증 |
@DecimalMax(value=) | 지정된 값(실수) 이하 인지 검증 |
@DecimalMin(value=) | 지정된 값(실수) 이상 인지 검증 |
@AssertFalse | false 여부 검증 |
@AssertTrue | true 여부 검증 |
참고
'BackEnd > Spring' 카테고리의 다른 글
[Spring] MyBatis 사용 시 resultType 에 InnerClass 사용하기 (2) | 2022.12.10 |
---|---|
[SpringBoot] CouchBase 연동하기 (0) | 2022.11.27 |
[Spring] @Scheduled 어노테이션에서 cron 사용 및 정리 (0) | 2022.08.11 |
[Spring] WebClient 정리(API 통신) (0) | 2022.08.07 |
[Spring] JPA - 영속성 컨텍스트(Persistence Context) 정리 (0) | 2022.07.25 |