Spring boot 에서 Validation은 보통 Entity와 Request body의 정보가 제대로 들어왔는지 확인하는 경우에 사용한다.
예를 들면, 아래와 같이 Request body의 Variable 의 값이 Null이거나 Blank 일 때를 확인해서 이러한 경우에는 Client에게 Error Reponse 를 보내거나, log 상에 에러를 발생시키게 된다.
@Getter
@Setter
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Schema(description = "Session Creation Request")
public static class SessionCreateRequest {
@Schema(example = "1L")
@NotNull(message = "{account.id.empty}")
private Long accountId;
@Schema(example = "{\"orderCount\":1}")
@NotBlank(message = "{session.value.empty}")
private String values;
}
이러한 Validation 작업은 Server의 완성도를 높여줄 수 있고 Client에게 Request 상의 문제를 알려줄 수 있기 때문에 굉장히 유용한 기능이라고 할 수 있다. 셋팅은 완료하고 나면 간단하게 Annotation을 붙이면 체크를 해주니 사용성 또한 아주 간단하다.
나중에 포스팅을 하겠지만, Custom annotation을 만들어서 체크도 할 수 있다.
그럼 설정방법에 대해서 알아보자
Dependency 설정
Spring boot 에서 Validation Dependency를 제공한다. 아래를 pom.xml에 추가하도록 하자.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Configuration setting
Validation Configuration
아주 기본적인 Configuration이다. Configuration이 없어도 Default로 셋팅이 되지만 Validation Message Source를 넣고자 Configuration file을 만들었다. Validation Message Source는 Validation error가 발생되었을 때 그 정보를 정의해둔 메세지 형태로 Client로 보내고자 할 때 사용한다. 아래에 설정 방법을 추가하도록 하겠다.
@Configuration
public class ValidationConfig {
private final MessageSource validationMessageSource;
@Bean
public LocalValidatorFactoryBean getValidator() {
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
bean.setValidationMessageSource(validationMessageSource);
return bean;
}
}
Message Accessor Configuration
Validation Message configuration은 ReloadableResourceBundleMessageSource를 생성하여 몇가지 옵션을 제공한다.
@Configuration
public class MessageConfig {
private static final String VALIDATION_MESSAGE_SOURCE_PATH = "classpath:/messages/validation_message";
@Bean
public MessageSource validationMessageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename(VALIDATION_MESSAGE_SOURCE_PATH);
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
@Bean("validationMessageSourceAccessor")
public MessageSourceAccessor validationMessageSourceAccessor() {
return new MessageSourceAccessor(validationMessageSource());
}
}
Basename은 Message 목록의 Path를 지정한다. 아래와 같이 Validation에는 message 옵션이 붙는데 Validation이 실패할 경우 보여줄 메세지를 지정한다. `{}`안에 있는 것이 key이며 파일에서 아래의 key를 찾아 메세지를 매핑하게 된다.
@NotBlank(message = "{session.value.empty}")
Message source 를 생성한 후 그것에 접근할 수 있는 Accessor를 Bean에 등록한다.
이렇게 설정할 경우에는 메세지를 더 효율적으로 관리할 수 있다.
Message properties 파일
위의 설정의 Basename으로 들어간 파일의 경로에 message.properties 파일이 있어야 한다. 아래와 같이 key값에 대응되는 값을 가지고 있어야 한다. 한가지 주의할 점은 Unit test를 만들 경우에는 아래의 파일이 test/resources에도 있어야 한다.
# JwtToken
account.id.empty=Account id is empty.
token.value.empty=Jwt token is empty.
# Session
session.value.empty=Session value is empty.
@Valid annotation
Vadilation check를 위한 Annotaiton을 붙였다라고 해서 Validation이 되지 않는다. 원하는 Request Body에 `@Valid` annotation을 붙여줘야 한다. 아래 Contoller source를 보면 `@RequestBody` 앞에 `@Valid` annotation이 붙은 것을 볼 수 있다.
@PostMapping("/session")
public SessionDTO.SessionInfo createSession(@Valid @RequestBody SessionDTO.SessionCreateRequest request) {
Session session = modelMapper.map(request, Session.class);
Session createdSession = sessionService.create(session);
return SessionDTO.SessionInfo.from(createdSession, modelMapper);
}
Validation Result
Validation 설정을 완료하고 테스트를 해보면 Validation error가 발생하였을 경우에는 아래와 같은 메세지가 나오게 된다.
{
"timestamp": "2022-01-19T19:53:48.832+00:00",
"status": 400,
"error": "Bad Request",
"trace": "org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public io.coolexplorer.session.dto.JwtTokenDTO$JwtTokenInfo io.coolexplorer.session.controller.JwtTokenController.createToken(io.coolexplorer.session.dto.JwtTokenDTO$JwtTokenCreateRequest): [Field error in object 'jwtTokenCreateRequest' on field 'jwtToken': rejected value []; codes [NotBlank.jwtTokenCreateRequest.jwtToken,NotBlank.jwtToken,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [jwtTokenCreateRequest.jwtToken,jwtToken]; arguments []; default message [jwtToken]]; default message [Jwt token is empty.]] \n\tat org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:141)\n\tat org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:121)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:179)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:146)\n\tat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)\n\tat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1067)\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963)\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)\n\tat org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:681)\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:764)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197)\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:540)\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135)\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357)\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:382)\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:895)\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1732)\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)\n\tat java.base/java.lang.Thread.run(Thread.java:829)\n",
"message": "Validation failed for object='jwtTokenCreateRequest'. Error count: 1",
"errors": [
{
"codes": [
"NotBlank.jwtTokenCreateRequest.jwtToken",
"NotBlank.jwtToken",
"NotBlank.java.lang.String",
"NotBlank"
],
"arguments": [
{
"codes": [
"jwtTokenCreateRequest.jwtToken",
"jwtToken"
],
"arguments": null,
"defaultMessage": "jwtToken",
"code": "jwtToken"
}
],
"defaultMessage": "Jwt token is empty.",
"objectName": "jwtTokenCreateRequest",
"field": "jwtToken",
"rejectedValue": "",
"bindingFailure": false,
"code": "NotBlank"
}
],
"path": "/api/v1/token"
}