Intro
๊ธฐ๋ณธ์ ์ผ๋ก Spring MVC๋ ์กด์ฌํ์ง ์๋ API ์์ฒญ ์ ์๋ต์ ๋ค์๊ณผ ๊ฐ์ด ์ฃผ๊ณ ์์ต๋๋ค.
{
"timestamp":"2023-09-21T11:30:05.517+00:00",
"status":404,
"error":"Not Found",
"path":"/api/404"
}
์ ์๋ต์ Spring MVC๊ฐ ๊ธฐ๋ณธ์ ์ผ๋ก ์ ๊ณตํด์ฃผ๋ ์ค๋ฅ ์๋ต ์ ๋๋ค. ๋ณดํต ์ค๋ฌด์์๋ ํ๋ ์์ํฌ์์ ๊ธฐ๋ณธ์ ์ผ๋ก ์ ๊ณตํด์ฃผ๋ ์๋ต๊ฐ์ ์ฌ์ฉํ๊ธฐ ๋ณด๋จ ๊ณตํต ์๋ต ํฌ๋งท์ ๋ง์ถฐ์ ์ฌ์ฉํ ๊ฒ ์ ๋๋ค. ์ด๋ป๊ฒ ํ๋ฉด Not Found์ ๋ํ ์๋ต์ ์ปค์คํ ํ ์ ์๋์ง์ ๋ด๋ถ์ ์ผ๋ก ์ด๋ป๊ฒ ๋์ํ๊ณ ์๋์ง๋ ํจ๊ป ์๋ฌ ๋ก๊ทธ์ ์ฝ๋๋ฅผ ํตํด ์ดํด๋ณด๊ฒ ์ต๋๋ค.
์๋ฌ ์๋ต์ด ์์ฑ๋๊ธฐ๊น์ง์ ์ฌ์
๊ทธ๋ ๋ค๋ฉด ์ด๋ป๊ฒ ์คํ๋งMVC์์๋ ์๋ฌ ๋ฉ์ธ์ง๋ฅผ ์์ฑํ๊ณ ์์๊น์?
๊ฐ๋จํ ํ ์คํธ๋ฅผ ํตํด์ ์์๋ณด๊ฒ ์ต๋๋ค.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class APINotFoundExceptionTest {
@LocalServerPort private int port;
@Test
void ์๋_API๋ผ๋ฉด_404์๋ฌ๊ฐ_๋ฐ์ํ๋ค() throws Exception {
// given
RestTemplate restTemplate = new RestTemplate();
// when & then
assertThatThrownBy(() -> restTemplate.getForObject("<http://localhost>:" + port + "/api/404", String.class));
}
}
์์ธํ ๋ก๊ทธ๋ฅผ ์ดํด๋ณด๊ธฐ ์ํด application.yaml์ web ๊ด๋ จ ๋ก๊ทธ ๋ ๋ฒจ์ DEBUG๋ก ์ค์ ํ๊ฒ ์ต๋๋ค.
logging:
level:
web: debug
์คํ์ ํด์ฃผ๋ฉด ๋ค์๊ณผ ๊ฐ์ ๋ก๊ทธ๊ฐ ์ถ๋ ฅ๋ฉ๋๋ค.
2023-09-21T21:09:26.457+09:00 DEBUG 56326 [1] [o-auto-1-exec-1] o.s.web.servlet.DispatcherServlet : GET "/api/404", parameters={}
2023-09-21T21:09:26.460+09:00 DEBUG 56326 [2] [o-auto-1-exec-1] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped to ResourceHttpRequestHandler [classpath [META-INF/resources/], classpath [resources/], classpath [static/], classpath [public/], ServletContext [/]]
2023-09-21T21:09:26.463+09:00 DEBUG 56326 [3] [o-auto-1-exec-1] o.s.w.s.r.ResourceHttpRequestHandler : Resource not found
2023-09-21T21:09:26.463+09:00 DEBUG 56326 --- [o-auto-1-exec-1] o.s.web.servlet.DispatcherServlet : Completed 404 NOT_FOUND
2023-09-21T21:09:26.466+09:00 DEBUG 56326 [4] [o-auto-1-exec-1] o.s.web.servlet.DispatcherServlet : "ERROR" dispatch for GET "/error", parameters={}
2023-09-21T21:09:26.467+09:00 DEBUG 56326 [5] [o-auto-1-exec-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
2023-09-21T21:09:26.477+09:00 DEBUG 56326 --- [o-auto-1-exec-1] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Using 'application/json', given [text/plain, application/json, application/*+json, */*] and supported [application/json, application/*+json]
2023-09-21T21:09:26.478+09:00 DEBUG 56326 --- [o-auto-1-exec-1] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing [{timestamp=Thu Sep 21 21:09:26 KST 2023, status=404, error=Not Found, path=/api/404}]
2023-09-21T21:09:26.493+09:00 DEBUG 56326 --- [ Test worker] o.s.web.client.RestTemplate : Response 404 NOT_FOUND
2023-09-21T21:09:26.493+09:00 DEBUG 56326 --- [o-auto-1-exec-1] o.s.web.servlet.DispatcherServlet : Exiting from "ERROR" dispatch, status 404
- [1]: RestTemplate์ผ๋ก ๋ณด๋๋ ์์ฒญ์ DispatcherServlet์ด ๋ฐ์์ต๋๋ค
- [2]: ์ฐ๋ฆฌ์ ์์ฒญ์ด ResourceHttpRequestHandler์๊ฒ ๋งคํ๋์๋ค๊ณ ํฉ๋๋ค? (*๋ค์์ ์์ธํ๊ฒ ์ดํด๋ณด๊ฒ ์ต๋๋ค)
- [3]: Resource๋ฅผ ๋ชป์ฐพ์๋ค๊ณ ํฉ๋๋ค.
- [4]: ๊ทธ๋ฌ๊ณ ๋ ์ฐ๋ฆฌ์ ์์ฒญ์ /error๋ก ๋ณด๋ ๋๋ค
- [5]: ์ต์ข ์ ์ผ๋ก BasicErrorController๊ฐ ์ฐ๋ฆฌ์๊ฒ ์๋ต์ ๋ด๋ ค์ฃผ๋ ๊ฒ ๊ฐ์ต๋๋ค.
์ ์ด์ ๋ก๊ทธ ๋ถ์์ด ๋๋ฌ์ผ๋ ํ๋ฒ ์ฝ๋๋ก ์ดํด๋ณผ๊น์?
ResourceHttpRequestHandler
public class ResourceHttpRequestHandler extends WebContentGenerator
implements HttpRequestHandler, EmbeddedValueResolverAware, InitializingBean, CorsConfigurationSource {
...
@Override
public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// For very general mappings (e.g. "/") we need to check 404 first
Resource resource = getResource(request);
if (resource == null) {
logger.debug("Resource not found"); // **[1]
response.sendError(HttpServletResponse.SC_NOT_FOUND); // **[2]
return;
}
...
}
- [1]: ๋ก๊ทธ์ ์ฐํ๋ “Resource not found”๊ฐ ๋ณด์ ๋๋ค!
- [2]: ์ฐ๋ฆฌ์๊ฒ 404 ์๋ต์ ์ฌ๊ธฐ์ ๋ณด๋ด๊ณ ์์์ต๋๋ค. ์ด ์๋ฌ๋ BasicErrorController์ ์ํด ์ฒ๋ฆฌ๋ฉ๋๋ค.
๊ทผ๋ฐ ๋ญ๊ฐ ์ด์ํฉ๋๋ค.. ResourceHttpRequestHandler๋ ์ ์ ๋ฆฌ์์ค๋ฅผ ์ ๊ณตํ๋ ํธ๋ค๋ฌ์ธ๋ฐ, ์๋ ์ฐ๋ฆฌ์ ์์ฒญ์ ์ ์ ๋ฆฌ์์ค ์์ฒญ์ผ๋ก ๋ณด๊ณ ์ ์ ํ์ผ์ ์ฐพ์ผ๋ ค๊ณ ์๋ํ๋ ๊ฒ์ด์ฃ . ์ค๊น์?
SimpleUrlHandlerMapping
2023-09-21T21:09:26.460+09:00 DEBUG 56326 [2] [o-auto-1-exec-1] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped to ResourceHttpRequestHandler [classpath [META-INF/resources/], classpath [resources/], classpath [static/], classpath [public/], ServletContext [/]]
[2] ๋ก๊ทธ์์ SimpleUrlHandlerMapping์ด ResourceHttpRequestHandler๋ฅผ ๋งคํํ๋ค๊ณ ๋ก๊ทธ๋ฅผ ์ฐ์์ต๋๋ค. ์ฐ๋ฆฌ๊ฐ ์์ฒญํ ์ /api/404๋ฅผ ์ฒ๋ฆฌํ ํธ๋ค๋ฌ๋ ์์ํ ๋ฐ Handler๊ฐ ์๋ค๋๊ฒ ๋ญ๊ฐ ์ด์ํฉ๋๋ค.
DispatcherServlet์ ์ดํด๋ด ์๋ค
๊ธฐ๋ณธ์ ์ธ Handler๋ฅผ ๊ฐ์ง๊ณ ๋ฐ๋ณต๋ฌธ์ ๋๋ ค์ ์ฐ๋ฆฌ์ ์์ฒญ์ ์ฒ๋ฆฌํ SimpleUrlHandlerMapping ํธ๋ค๋ฌ๋ฅผ ์ฐพ์๋ ๋๋ค. ์ฆ, ์ฐ๋ฆฌ์ ์์ฒญ์ ์ ์ ๋ฆฌ์์ค ์์ฒญ์ผ๋ก ๊ฐ์ฃผํ ๊ฒ์ ๋๋ค.
์คํ๋ง ๊ณต์๋ฌธ์์ ๋ฐ๋ฅด๋ฉด ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
By default, Spring MVC will send a 404 Not Found error response if a handler is not found for a request. To have a NoHandlerFoundException thrown instead, set configprop:spring.mvc.throw-exception-if-no-handler-found to true. Note that, by default, the serving of static content is mapped to /** and will, therefore, provide a handler for all requests. For a NoHandlerFoundException to be thrown, you must also set spring.mvc.static-path-pattern to a more specific value such as /resources/** or set spring.web.resources.add-mappings to false to disable serving of static content entirely.
๊ธฐ๋ณธ์ ์ผ๋ก Spring MVC๋ ์์ฒญ์ ๋ํ ํธ๋ค๋ฌ๋ฅผ ์ฐพ์ ์ ์๋ ๊ฒฝ์ฐ 404 Not Found ์ค๋ฅ ์๋ต์ ๋ณด๋ ๋๋ค. ๋์ NoHandlerFoundException์ด ๋ฐ์ํ๋๋ก ํ๋ ค๋ฉด configprop:spring.mvc.throw-exception-if-no-handler-found๋ฅผ true๋ก ์ค์ ํ์ธ์. ๊ธฐ๋ณธ์ ์ผ๋ก ์ ์ ์ฝํ ์ธ ์ ๊ณต์ /**์ ๋งคํ๋๋ฏ๋ก ๋ชจ๋ ์์ฒญ์ ๋ํด ํธ๋ค๋ฌ๋ฅผ ์ ๊ณตํฉ๋๋ค. NoHandlerFoundException์ด ๋ฐ์ํ๋ ค๋ฉด spring.mvc.static-path-pattern์ /resources/**์ ๊ฐ์ ๋ณด๋ค ๊ตฌ์ฒด์ ์ธ ๊ฐ์ผ๋ก ์ค์ ํ๊ฑฐ๋ spring.web.resources.add-mappings๋ฅผ false๋ก ์ค์ ํ์ฌ ์ ์ ์ฝํ ์ธ ์ ์๋น์ ์์ ํ ๋นํ์ฑํํด์ผ ํฉ๋๋ค.
spring.web.resources.add-mappings: false ๋ก ์ค์ ํด์ฃผ๋ฉด ๋ฉ๋๋ค!
NoHandlerFoundException ๋ฐ์์ํค๊ธฐ
2023-09-21T22:01:09.091+09:00 DEBUG 57850 --- [o-auto-1-exec-1] o.s.web.servlet.DispatcherServlet : GET "/api/404", parameters={}
2023-09-21T22:01:09.094+09:00 WARN 57850 [1] [o-auto-1-exec-1] o.s.web.servlet.PageNotFound : No mapping for GET /api/404
2023-09-21T22:01:09.095+09:00 DEBUG 57850 --- [o-auto-1-exec-1] o.s.web.servlet.DispatcherServlet : Completed 404 NOT_FOUND
2023-09-21T22:01:09.098+09:00 DEBUG 57850 --- [o-auto-1-exec-1] o.s.web.servlet.DispatcherServlet : "ERROR" dispatch for GET "/error", parameters={}
2023-09-21T22:01:09.100+09:00 DEBUG 57850 --- [o-auto-1-exec-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
2023-09-21T22:01:09.167+09:00 DEBUG 57850 --- [o-auto-1-exec-1] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Using 'application/json', given [text/plain, application/json, application/*+json, */*] and supported [application/json, application/*+json]
2023-09-21T22:01:09.169+09:00 DEBUG 57850 --- [o-auto-1-exec-1] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing [{timestamp=Thu Sep 21 22:01:09 KST 2023, status=404, error=Not Found, path=/api/404}]
2023-09-21T22:01:09.197+09:00 DEBUG 57850 --- [ Test worker] o.s.web.client.RestTemplate : Response 404 NOT_FOUND
2023-09-21T22:01:09.198+09:00 DEBUG 57850 --- [o-auto-1-exec-1] o.s.web.servlet.DispatcherServlet : Exiting from "ERROR" dispatch, status 404
- [1]: ์ด์ ๊ทธ ์ด๋ ํ HandlerMapping์ด ํ ๋น๋์ง ์์์ต๋๋ค.
DispatcherServlet์ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
public class DispatcherServlet {
...
protected void noHandlerFound(HttpServletRequest request, HttpServletResponse response) throws Exception {
if (pageNotFoundLogger.isWarnEnabled()) {
pageNotFoundLogger.warn("No mapping for " + request.getMethod() + " " + getRequestUri(request));
}
if (this.throwExceptionIfNoHandlerFound) { // **
throw new NoHandlerFoundException(request.getMethod(), getRequestUri(request),
new ServletServerHttpRequest(request).getHeaders());
}
else {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
}
}
...
}
- ํ์ฌ throwExceptionIfNoHandlerFound๊ฐ์ด ์ฐธ์ด๋ฉด NoHandlerFoundException์ ๋ฐ์์ํต๋๋ค. ๊ธฐ๋ณธ๊ฐ์ false๋ก ์ค์ ๋์ด ์์ต๋๋ค.
์ด์ NoHandlerFoundException๋ฅผ ๋ฐ์์ํค๋๋ก ํด์ ์ฐ๋ฆฌ๊ฐ ExceptionHandler๋ฅผ ํตํด ์์ธ๋ฅผ ์ฒ๋ฆฌํด์ฃผ๋ฉด ๋์ ๋๋ค! ๊ณต์๋ฌธ์์ ๋ฐ๋ฅด๋ฉด [spring.mvc.throw-exception-if-no-handler-found](<https://docs.spring.io/spring-boot/docs/3.1.4/reference/htmlsingle/#application-properties.web.spring.mvc.throw-exception-if-no-handler-found>) ๋ผ๋ ์ต์ ์ true๋ก ์ค์ ํ๋ฉด ๋๋ค๊ณ ํฉ๋๋ค! ์ด๋ ๊ฒ ์ค์ 1์ค๋ก ๊ฐ๋จํ๊ฒ ๋ฐ๊ฟ ์ ์๋ค๋ ๋๋ฌด ํธ๋ฆฌํ ๊ฒ ๊ฐ์ต๋๋ค..!
๋ค์ ํ ์คํธ๋ฅผ ์คํํด๋ณด๋ฉด?
2023-09-21T22:11:07.767+09:00 WARN 58155 --- [o-auto-1-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.servlet.NoHandlerFoundException: No endpoint GET /api/404.]
DefaultHandlerExceptionResolver์์ NoHandlerFoundException์ ์ฒ๋ฆฌํ ๊ฒ์ ๋ณผ ์ ์์ต๋๋ค!
์ปค์คํ ExceptionHandler ์์ฑ
์กด์ฌํ์ง ์๋ API๋ก ์์ฒญ์ด ์์ ๊ฒฝ์ฐ, NoHandlerFoundException์ด ๋ฐ์ํ๊ฒ๋ ์ค์ ํ์์ต๋๋ค. ๊ทธ๋ฆฌ๊ณ ์ด ์์ธ๋ฅผ Advice์์ ์ก์์ ์ฐ๋ฆฌ๊ฐ ์ปค์คํ ํ๊ฒ ์๋ต์ ๋ด๋ ค์ค ์ ์์ต๋๋ค. ๋ฐ๋ก ์๋์ฒ๋ผ ๋ง์ด์ฃ .
@RestControllerAdvice
public class GlobalControllerAdvice {
@ExceptionHandler(NoHandlerFoundException.class)
public ResponseEntity<Map<String, String>> noHandlerFoundHandle(NoHandlerFoundException e) {
return ResponseEntity.status(NOT_FOUND)
.body(Map.of(
"statusCode", "404",
"message", "์กด์ฌํ์ง ์๋ API ์
๋๋ค."
));
}
}
Outro
์กด์ฌํ์ง ์๋ API ์์ฒญ์ ๋ํด์ ์ถ์ ์ ํ๋๋ฐ ๊ฝค ์ ๋ฅผ ๋ง์ด ๋จน์์ต๋๋ค. ์ ์กด์ฌํ์ง ์๋ URL์ ๊ฐ์ง๊ณ Static Resource๋ฅผ ์ฐพ๋์ง ์คํ๋ง์ด ๊ธฐ๋ณธ์ ์ผ๋ก SimpleUrlHandlerMapping์ ํตํด ์ฒ๋ฆฌํ๋์ง, ๋ด๋ถ์ ์ผ๋ก ์ด๋ป๊ฒ ๋์ํ๋์ง ์ด๋ฒ ๊ธฐํ์ ์์ธํ๊ฒ ์๊ฒ๋์์ต๋๋ค. ์์ผ๋ก ์ด๋ฐ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋๋ผ๋ ์ฐจ๊ทผ์ฐจ๊ทผ ํ๋จ๊ณ์ ์ถ์ ํด๋๊ฐ๋ฉฐ ๋ด๋ถ๋์์ด ์ด๋ป๊ฒ ์ด๋ค์ง๋์ง, ๋ ์ด๋ป๊ฒ ํ๋ฉด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์๋์ง ๋์์ด ๋ ๊ฒ ๊ฐ์ต๋๋ค.
๊ธด ๊ธ ์ฝ์ด์ฃผ์ ์ ๊ฐ์ฌํฉ๋๋ค!