진행하고 있는 무백스 스터디 이슈에서 스터디원분의 질문으로 인해 디버깅을 스프링의 기본 오류 페이지에 대한 생성과정을 직접 디버깅하여 정리해봤습니다.
아래 글 내용은 https://github.com/Invincible-Backend-Study/toby-spring-boot/issues/6 이슈에서도 볼 수 있습니다!
혹시라도 잘못된 내용이 있거나, 수정해야 하는 부분이 있다면 댓글로 알려주시면 감사하겠습니다!!!
✅ 테스트 환경 세팅
테스트용 Controller
존재하지 않는 viewName을 반환한다.
✅ Springframwork 디버깅 과정
DispatcherServlet은 가장먼저 “/”로 들어온 요청에 대해 처리를 시작합니다. 이때 요청 URI는 “/”가 되고, DispatcherType은 “REQUEST”가 됩니다.
TestController의 test()메소드가 해당 URI와 매핑되어있으므로 해당 컨트롤러를 실행하고 반환값으로 “dd”라는 viewName을 받게 됩니다.
해당 정보를 가지고 ModelAndView를 생성하고 내부 View 정보를 통해 RequestDispatcher가 forwarding 작업을 합니다.
포워딩 됐으므로 dispatcherType은 FORWARD이며, “/dd”
라는 리소스를 DispatcherServlet이 내부적으로 호출하게 됩니다.
다시 DispatcherServlet을 통해 내부적인 doService() → doDispatch 작업을 수행하게 됩니다.
DispatcherServlet.doDispatch()에서 HandlerAdapter를 찾아 handle()하게 되는데 이는 어댑터(HttpRequestHandlerAdapter)를 통해 찾아온 핸들러를 실행시키게 됩니다.
이때 정적 리소스를 처리하는 ResourceHttpRequestHandler
가 선택되고 “/dd”에 대한 실제 리소스를 가져오지만 존재하지 않으므로 null값이 반환됩니다.
resource가 null이므로 Not Found 에러를 Response 객체에 세팅합니다. 이때 sendError내부 코드에서 setError를 통해 0 → 1로 에러 플래그를 세팅합니다.
사용자의 “/” 요청이 마무리되고 WAS까지 404 Error가 전파됩니다. 그리고 Tomcat에서 에러가 존재하는지 묻게 됩니다.
이때 아까 setError를 통해 1로 세팅한 에러값을 통해 에러가 있음을 WAS가 인지하게 됩니다.
이후 404 Not Found에 대한 에러 페이지를 찾게 됩니다. 따로 생성한 에러페이지는 존재하지 않으므로 WAS에서 생성하는 디폴트 에러 페이지를 찾게 됩니다.
/error
로케이션을 받아오고 이후 각종 에러에 대한 세팅을 해줍니다(DispatcherType을 ERROR 세팅 등)
이후 /error
에 대해 WAS 내부에서 포워딩 작업을 실행합니다.
포워딩 작업이 시작됐으므로 다시 필터를 거치고 DispatcherServlet을 호출하게 됩니다.
“/error”로 requestURI가 선택되어 있는데 해당 uri는 BasicErrorController
의 @RequestMapping과 일치합니다.
또한 Accept header
가 text/html
이므로 BasicErrorController#errorHtml
핸들러 메소드가 선택됩니다.RequestMappingHandlerAdapter
는 이런 @RequestMapping
이 되어있는 핸들러를 지원하므로(HandlerAdapter#supports
를 지원함) 어댑터로 선택됩니다.
(이 이미지는 좌측 하단의 main 쓰레드내 메소드 스택을 통해 흐름을 이해하기 편하도록 첨부했습니다.)
이후 실제 BasicErrorController#errorHtml
을 실행합니다.
errorHtml의 내부 호출 메소드인 resolveErrorView에서는DefaultErrorViewResolver#resolveErrorView를 호출해 에러페이지에 대한 view resolving 수행합니다.
DefaultErrorViewResolver#resoveErrorView
에서는 두번째 사진의 staticLocations
과 같은 순서로 view를 찾습니다.
하지만 일치하는 View가 없으므로 null을 반환하게 됩니다. BasicErrrorController#errorHtml
에서는 반환된 ModelAndView 객체가 null값이라면 “error”
라는 viewName
을 가진 ModelAndView 객체를 생성해 반환합니다.
반환된 ModelAndView 객체를 처리하가 위해 ModelAndView 응답 value를 처리하는 핸들러로 ModelAndViewMethodReturnValueHandler가 선택됩니다.
이곳에서 뷰 이름 세팅, 응답 코드 세팅 , 리다이렉트에 대한 처리 등을 수행합니다.
이후 BasicErrorController#errorHtml
을 통해 생성된 ModelAndView 객체가 DispatcherServlet으로 반환됩니다.
받아온 ModelAndView 객체 내부의 View를 reolving하는데 이때 ContentNegotiatingViewReolver
가 선택됩니다.
ContentNegotiatingViewResolver#resolveViewName
에서는 getCandiateViews()
를 통해 후보가 될 수 있는 View를 선택하고 이후 getBestView()
를 통해 최적의 View를 선택합니다.
후보군 View로는 ErrorMvcAutoConfiguration$StaticView
와 정적 리소스를 처리하는 InternalResourceView
2개가 선택된다.
best view로는 ErrorMvcAutoConfiguration$StaticView가 더 높은 우선순위를 가지고 있고, text/html에 대해 ErrorMvcAutoConfiguration$StaticView가 처리할 수 있으므로 즉시 해당 뷰를 반환합니다.
이후 DispatcherServlet은 받아온 ErrorMvcAutoConfiguration$StaticView를 통해 뷰 렌더링을 진행합니다.
실제 StaticView의 render 메소드를 살펴보면 우리가 익히 볼 수 있는 Whitelabel Error Page
를 볼 수 있습니다. 여기서 이전에 정해진 error 내용과 status code를 결합해 응답 HTML을 완성합니다.
응답 처리를 마치고 브라우저에는 404 Whitelabel Error Page가 응답됩니다.
후기
스프링 프레임워크의 코드를 디버깅해보면서 느낀것지만 상당히 양이 방대하며, 사소한 것들 하나하나 정말 많은 코드들이 존재합니다.
모든 코드에 대한 설명을 해석하고 적기에는 굉장히 양이 많고, 시간적 압박이 컸으므로.. (사실 이것만 해도 하루 종일 디버깅을 했습니다.. ㅎㅎ) 최대한 핵심이 되는 내용을 담으려고 노력했습니다.
이전 자바 웹 프로그래밍 Next Step 책 스터디
를 통해 Spring MVC의 구조에 대해 꽤 알게 됐다고 생각하고 자신있게 디버깅을 시도했습니다만,
예상보다 방대한 양의 코드와 아직 부족한 디버깅 실력으로 코드를 이해하는데 꽤 많은 시간이 걸렸습니다.
그래도 이렇게 디버깅을 통해 실제 프레임워크를 분석해보면서 느낀건 어떤 코드를 보면 내부 구조를 반드시 전부 해석하고 이해해야했던 과거와 달리 직관적으로 해석할 수 있는 능력(메소드 이름, 클래스 명, 변수명 등을 통해)이 많이 향상된 것 같습니다!