환경: springboot 2.6.2, spring-web
아래와 같은 컨트롤러가 있다.
@RequestMapping("/api/member")
@RestController
@RequiredArgsConstructor
public class MemberController {
private final UserService userService;
@GetMapping("/loss-game-money")
public BaseResponse<MemberLossGameMoneyResponse> getMemberLossGameMoney(@ModelAttribute MemberLossGameMoneyRequest request) {
return userService.getMemberLossGameMoney(request);
}
@GetMapping("/{id}")
public BaseResponse<MemberInfoResponse> getMember(@PathVariable String id) {
return new BaseResponse<>(userService.getMemberInfo(id));
}
}
이 상황에서 아래 api를 요청한다면? endpoint가 정의되어 있지 않아 404가 날 것이라 기대했다.
http://localhost:8600/api/member
하지만 200 ok 가 떨어졌다.
분명 컨트롤러에 정의되어 있지 않고, 그렇다고 에러 내용이 controller advice에 정의되어 있지도 않은데..
그리고 과거에는 404로 떨어졌던 기억도 있던 터라 구글링을 해본다..
의심 1. 스프링 버전 이슈?
구글링 하다가 스프링 버전 2.3 이후부터 바뀐 스펙이라고 적힌 것을 봐서.. 스프링 버전의 문제인가 의심했다.(개소리로 판명)
The change in behavior where Spring Boot started returning a 200 OK response with an empty body for unmatched endpoints instead of a 404 response happened around Spring Boot version 2.3.x.
In versions prior to 2.3.x, the default behavior was to return a 404 Not Found response for unmatched endpoints. However, starting from version 2.3.x, the default behavior was changed to return a 200 OK response with an empty body.
공식 문서를 찾다가 실패하여 신규 프로젝트에 버전을 아래와 같이 중간 버전만 하나씩 올려서 테스트해 봤는데
- 테스트해 본 버전: 2.2.6.RELEASE, 2.3.4.RELEASE, 2.4.5, 2.5.6, 2.6.3, 2.7.3, 2.7.10, 2.7.18, 3.0.0, 3.2.4(현재 최신)
절대 재현되지 않는다.. 버전 문제는 아니고 소스 문제라고 판명..
아래와 같이 모두 동일한 결과를 return 한다. 404 잘만 나는구먼..
의심 2. controllerAdvice...?
혹시 exception handler가 잡아서 200으로 반환하나 싶어 소스를 뒤져봐도 그런 건 없었다.
의심 3. 그럼 필터? 인터셉터?
혹시나 싶어 필터나 인터셉터를 하나씩 주석해 보고 실행해 본다.
로그를 살펴보니 필터에 해당하는 로그는 잘 찍히고 있어 필터 이후에서 200을 반환하는 것이라 판단했다.
그리고 인터셉터를 하나씩 보는데.. 잡았다 요놈..
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
...
return HandlerInterceptor.super.preHandle(request, response, handler);
}
return false; ///
}
위 함수에서 컨트롤러에 정의된 api는 if (handler instanceof HandlerMethod) 구문에 true를 반환하여 작업을 하고 마지막에 true를 반환하지만 정의되지 않은 api는 false를 타게 된다. false를 타면 상태코드 200에 empty body로 리턴된다..! false를 반환한다는 의미가 controller를 타지 않고 작업을 종료한다는 의미고 그게 잘 되었으니 어쩌면 맞을 수도..
혹시나 싶어서 신규 프로젝트로 기본 세팅만 한 후 재현해 본다.
1. @EnableWebMvc 없고 + return false
@Component
public class TestInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return false;
}
}
@Component
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {
private final TestInterceptor interceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//TestInterceptor가 이미 빈으로 등록되어 있으므로 주입하여 사용하기위해 아래 주석;
//TestInterceptor에 @Component가 없으면 빈이 아니므로 아래 주석이 작동함
// registry.addInterceptor(new TestInterceptor()).addPathPatterns("/**");
registry.addInterceptor(interceptor);
}
}
@RequestMapping("/api")
@RestController
public class TestController {
@GetMapping("/test")
public String test( ) {
return "hello";
}
}
- interceptor 설정 전
- http://localhost:8080/api/test 요청 시
- 200 hello
- http://localhost:8080/api/test3 요청 시
- 404 기본 404 not found 메시지
- http://localhost:8080/api/test 요청 시
- interceptor 설정 후
- http://localhost:8080/api/test 요청 시
- 200 empty body
- http://localhost:8080/api/test3 요청 시
- 200 empty body
- http://localhost:8080/api/test 요청 시
200이 아닌 다른 상태 값으로 반환하려면 아래와 같이 수정하면 된다.
empty body에 404로 떨어진다.
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (! (handler instanceof HandlerMethod)) {
response.setStatus(404); //
return false;
}
return true;
}
이를 수정해 본다면? 에러를 던져보자.
2. @EnableWebMvc 없고 + throw exception
아래와 같이 false 반환대신 에러를 던지면 throw 절을 만나게 되고 BasicErrorController를 타고 500으로 떨어진다.
(어떤 종류의 exception이건 500으로 떨어진다. 그 이유는 Exception이 처리가 이뤄지지 않은 상태로 WAS에게 전달되었기 때문이다. WAS 입장에서는 처리되지 않은 Exception을 받으면 이를 예상치 못한 문제로 인해 발생했다고 간주하고 status코드 500에 Internal Server Error를 메시지에 설정하게 된다.)
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (! (handler instanceof HandlerMethod)) {
throw new RuntimeException("nono");
}
return true;
}
@EnableWebMvc 어노테이션의 유무
1. @EnableWebMvc 있고 + 인터셉터에서 return true로 진행시키면
여기서 @EnableWebMvc 어노테이션을 추가하면 (어떤 에러를 던지건) 404로 에러가 바뀐다....
spring-web dependency가 있으면 @EnableWebMvc를 선언하지 않아도 된다고 알고 있는데 결과가 다르다니..(아래에서 계속)
더 확인해 보니 @EnableWebMvc를 선언하면 아래 부분이 true로 내려와서
handler instanceof HandlerMethod == true
결국 preHandler가 true로 return 되어 에러를 만나지 않고, controller 단에 가서 url을 찾다가 없어서 404 -> BasicErrorController를 타는 플로우였다.
2. @EnableWebMvc + 인터셉터 내부에서 에러 발생 시키면
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
throw new HttpClientErrorException(HttpStatus.MULTI_STATUS);
}
2024-04-18 09:56:29.978 WARN 12950 --- [nio-8080-exec-8] o.s.web.servlet.PageNotFound : No mapping for GET /api/test3
2024-04-18 09:56:29.980 ERROR 12950 --- [nio-8080-exec-8] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] threw exception
org.springframework.web.client.HttpClientErrorException: 207 MULTI_STATUS
at com.example.demo.interceptor.TestInterceptor.preHandle(TestInterceptor.java:16) ~[main/:na]
...
2024-04-18 09:56:29.982 ERROR 12950 --- [nio-8080-exec-8] o.a.c.c.C.[Tomcat].[localhost] : Exception Processing ErrorPage[errorCode=0, location=/error]
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.springframework.web.client.HttpClientErrorException: 207 MULTI_STATUS
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014) ~[spring-webmvc-5.3.22.jar:5.3.22]
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) ~[spring-webmvc-5.3.22.jar:5.3.22]
...
해당 부분에서도 항상 에러를 뱉게 하면 에러가 발생해도 404로 떨어진다.
그 의미는 에러가 발생해도 controller 쪽으로 요청이 들어온다는 것인데, 그 이유는 WAS의 에러페이지를 위한 요청 때문이다.
interceptor에 에러를 반환하고 exception handler를 통해 에러를 반환하려고 하면 어떻게 해야 할까?
interceptor는 dispatcher servlet 이후 스프링 컨텍스트 안에 있기 때문에 controller advice를 통한 처리가 가능하다.
1. @EnableWebMvc + controllerAdvice
@EnableWebMvc를 선언한 상태에서 interceptor에서 에러를 뱉게 아래와 같이 수정하고
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
throw new RuntimeException("yesyes");
}
controllerAdvice를 통해 exception handler를 작성하면 의도한 대로 상태코드 400에 에러 메시지가 나온다!
@RestControllerAdvice
public class InterceptorExceptionHandler {
@ExceptionHandler({RuntimeException.class})
@ResponseStatus(HttpStatus.BAD_REQUEST)
public String error(RuntimeException exception){
return "Exception : " + exception.getMessage();
}
}
WARN 23846 --- [nio-8080-exec-1] o.s.web.servlet.PageNotFound : No mapping for GET /api/test3
로그에는 page not found 가 적힌다.
2. @EnableWebMvc 제거 + controllerAdvice
여기서 @EnableWebMvc를 선언을 제거한다면?
그래도 api는 동일한 결과가 반환된다.
그런데 로그는 다르게 찍힌다. 반환한 에러에 대한 값이 찍힌다.
ERROR 24122 --- [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: yesyes] with root cause
java.lang.RuntimeException: yesyes
at com.example.demo.interceptor.TestInterceptor.preHandle(TestInterceptor.java:14) ~[main/:na]
at org.springframework.web.servlet.HandlerExecutionChain.applyPreHandle(HandlerExecutionChain.java:148) ~[spring-webmvc-5.3.22.jar:5.3.22]
...
@EnableWebMvc 어노테이션 관련..
spring-web dependency가 있으면 @EnableWebMvc를 선언하지 않아도 된다고 알고 있는데 결과가 다르다니..
에 대해 좀 더 알아본다.
우선 맞다. springboot에 spring boot starter web 디펜덴시가 있으면 @SpringBootApplicaion 어노테이션에 의해 @EnableAutoConfiguration 어노테이션이 작동하고 웹의 기능(DispatcherServlet 등)을 사용하기 위한 기본 세팅을 자동으로 해준다.
@EnableWebMvc 어노테이션이 사용되면 스프링은 웹 애플리케이션을 위한 준비를 하게 되는데, 기본 설정값으로 아래 클래스를 읽어온다.
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport
The @EnableWebMvc annotation is used in Spring to enable the Spring MVC framework for a web application. When this annotation is used, it imports the Spring MVC configuration from WebMvcConfigurationSupport
and helps in setting up the necessary components for handling web requests, such as controllers, views, and more. This annotation is often used in conjunction with the @Configuration annotation to enable Spring MVC configuration in a Java-based configuration approach.
우리가 기본 세팅을 커스터마이징 하려면 WebMvcConfigurerAdapter라는 클래스를 extend 해야 하는데, 이 클래스는 spring5.0 이래로 deprecated 되었고, WebMvcConfigurer interface를 implement 해야 한다.
그런데 여기서 주의해야 할 점은 커스텀 세팅을 사용하려면 @EnableWebMvc 어노테이션이 없어야 한다는 것이다.
만약 @EnableWebMvc 어노테이션과 커스텀 세팅을 사용하려면 빈으로 등록된 DelegatingWebMvcConfiguration 클래스를 확장하여 재정의해야 한다. 기본 값을 사용하지 않기에 모든 함수를 재정의해야 함에 주의하자.
위에서 @EnableWebMvc 어노테이션 유무에 따라 결과가 다르게 나온 이유는 아마도,
@EnableWebMvc 어노테이션 + 커스텀 세팅(인터셉터)인데 DelegatingWebMvcConfiguration 클래스의 재정의가 없어 기본 세팅값으로 override 되어 다른 결과가 나온 것이 아닌가 추측된다.
위 내용을 학습하다 스프링 버전 3부터 루트 매핑(@RequestMapping("")과 @RequestMapping("/"))에 대한 이슈가 있다는 글을 보아서 참고로 링크 남긴다.. 관련 글 때문에 더 헷갈렸다.
https://github.com/spring-projects/spring-boot/issues/33499
interceptor in spring context: was 흐름까지 정리 https://cs-ssupport.tistory.com/494
'개발 > spring' 카테고리의 다른 글
[jpa] transaction propagation (0) | 2024.05.23 |
---|---|
[jpa] transaction isolation level (0) | 2024.05.23 |
[application.yml] 프로파일 옵션으로 배포 설정 분리 (0) | 2024.02.29 |
[이슈해결][jpa] native query에서 사용자 변수 사용 시 (0) | 2024.02.07 |
[security] jwt NoClassDefFoundError: javax/xml/bind/DatatypeConverter (0) | 2024.01.27 |