요구사항
프론트에서 받은 값을 백엔드에서 처리할 때, 값이 비어있는 경우에 대한 검증을 하고 오류 메세지를 응답으로 내려주고자 한다.
다음과 같은 코드가 있다. api/account/check-id?id=thisIsId
경로로 요청이 들어오면 응답으로 불린 타입을 리턴해 주는 Controller 를 작성한 것이다.
package kr.go.data.member.controller;
import jakarta.validation.constraints.NotEmpty;
import kr.go.data.member.service.MemberServiceImpl;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/account")
public class MemberController {
private final MemberServiceImpl memberService;
public MemberController(MemberServiceImpl memberService) {
this.memberService = memberService;
}
// 아이디 검증
@GetMapping("/check-id")
public ResponseEntity<?> checkIsAvailableId(@RequestParam("id") String id) {
Boolean result = memberService.checkIsAvailableId(id);
return ResponseEntity.status(HttpStatus.OK).body(result);
}
}
하지만 프론트에서 받은 값이 빈 값이면 불린 값을 리턴하는 것이 아닌, 오류 메시지를 리턴하고자 한다. 즉, 요청 경로가 api/account/check-id?id=
인 경우에 오류 메시지를 리턴한다.
구현
먼저, build.gradle 의 dependencies 에 다음을 추가한다. (이미 추가되어 있는 경우 생략한다.)
implementation 'org.springframework.boot:spring-boot-starter-validation'
클래스 단위에 @Validated
어노테이션을 작성한다.
@Validated는 @Valid 의 기능을 포함하고, 유효성을 검토할 그룹을 지정할 수 있는 기능을 추가로 가지고 있다.
@Validated
@RestController
@RequestMapping("/api/account")
public class MemberController { .. }
검증하고자 하는 값, 즉 프론트에서 RequestParam 으로 받은 id 를 검증하기 위해 @NotBlank
어노테이션을 사용한다. message 속성을 함께 사용하여 오류 메시지를 보여줄 수 있다.
public ResponseEntity<?> checkIsAvailableId(@RequestParam("id") @NotBlank(message = "아이디는 비어있을 수 없습니다.") String id) { .. }
참고로, @NotNull은 null만 허용하지 않으며, @NotEmpty는 null과 빈 값("") 을 허용하지 않고, @NotBlank는 null과 빈 값(""), 그리고 공백(" ")까지 허용하지 않는다.
위 상태에서 api/account/check-id?id=
로 요청을 보내면 아래와 같은 응답을 받는다.
{
"timestamp": "2024-01-01T14:31:28.019+00:00",
"status": 500,
"error": "Internal Server Error",
"trace": "jakarta.validation.ConstraintViolationException: checkIsAvailableId.id: 아이디는 비어있을 수 없습니다.\n\tat org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:170)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)\n\tat org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:765)\n\tat org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:717)\n\tat kr.go.data.member.controller.MemberController$$SpringCGLIB$$0.checkIsAvailableId(<generated>)\n\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\n\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)\n\tat java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\n\tat java.base/java.lang.reflect.Method.invoke(Method.java:568)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:262)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:190)\n\tat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:917)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:829)\n\tat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089)\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979)\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014)\n\tat org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903)\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564)\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885)\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:205)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)\n\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)\n\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)\n\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)\n\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167)\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:482)\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115)\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:340)\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:391)\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:896)\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1744)\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)\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:842)\n",
"message": "checkIsAvailableId.id: 아이디는 비어있을 수 없습니다.",
"path": "/api/account/check-id"
}
응답이 잘 정리된 것 같으면서도, 내가 원하는 만큼 간결하지는 않아서 ConstraintViolationException
에 대한 응답을 커스터마이징 하기로 했다.
프로젝트에서 global 패키지를 만들고 exception 패키지를 만든 후 그 안에 GlobalExceptionHandler
라는 클래스를 만들었다.
이 클래스는 전역의 Exception 을 처리할 수 있도록 하기 위해 클래스 단위에 @ControllerAdvice
어노테이션을 붙였다. handleConstraintViolationException
메소드는 ConstraintViolationException
클래스에 대해 다룰 것임을 명시하기 위해 @ExceptionHandler(ConstraintViolationException.class)
를 작성했다. 매개변수로는 해당 Exception과 응답에 요청 path도 포함하기 위해 HttpServletRequest
도 넣어주었다. Map 자료형을 사용해 JSON(key-value) 형식으로 응답을 받을 수 있게 했다. 상세 코드는 아래와 같다.
package kr.go.data.global.exception;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<Object> handleConstraintViolationException(ConstraintViolationException ex, HttpServletRequest request) {
Map<String, Object> body = new HashMap<>();
body.put("timestamp", LocalDateTime.now());
body.put("status", HttpStatus.BAD_REQUEST.value());
body.put("error", "값 검증 에러");
String message = ex.getConstraintViolations().stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining(", "));
body.put("message", message);
body.put("path", request.getRequestURI());
return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST);
}
}
해결 완료
이후 위에서와 같은 경로로 요청했을 때의 응답은 다음과 같다!
{
"path": "/api/account/check-id",
"error": "값 검증 에러",
"message": "아이디는 비어있을 수 없습니다.",
"timestamp": "2024-01-01T23:41:14.64588",
"status": 400
}
확실히 처음보다 응답이 간결해진 것을 볼 수 있다!
'TIL' 카테고리의 다른 글
[BadSqlGrammarException] StatementCallback; bad SQL grammar [TRUNCATE TABLE members] (0) | 2024.05.21 |
---|---|
[Kotlin + SpringBoot] JaCoCo 추가하기 (0) | 2024.05.21 |
[Ubuntu] git에서 SpringBoot 프로젝트 받아오고 jar 파일 빌드 후 Script 로 jar 파일 배포하기 (0) | 2024.02.11 |
[JPA] @CreatedDate와 @LastModifiedDate (0) | 2024.01.13 |
[SpringBoot] 에러 응답 형식 통일 (0) | 2024.01.02 |