이전에 자바의 동작 구조에 대한 포스팅을 정리해본적이 있다. 그리고 이번에 공부하면서 스프링의 동작 과정도 살펴보았다. 각각 따로 떼어놓고 살펴보았으니 이번에는 하나의 흐름(?)으로 쭈욱 정리해보고자 한다.
나는 총 3개의 층으로 나눠서 생각해보았다. 대부분 이미지를 보면 우측의 구조로 설명되어있는데, 나는 JVM 외에 JDK, JRE도 같이 생각하기 위해 왼쪽의 구조로 나타내보았다.
원래라면 OS에 대한 내용을 정리해야하지만, 내용은 방대하고 내가 아는 지식은 얕기 때문에 쿨하게 생략! 😎
2층은 말 그대로 자바 코드를 실행해주기 위한 환경(?)으로 생각하는게 마음이 편하다.
Heap 영역의 구조를 더 살펴보면 다음과 같은 구조로 되어있다.
💡 스택 영역에서는 스택 프레임을 기억하자!
스택 프레임은 메서드가 호출될 때 그 메서드만의 스택 영역을 구분하기 위해서 생성되는 공간이다. 해당 공간에는 메서드와 관계되는 지역 변수나 매개 변수가 저장되고, 메서드 호출 시 할당되고 종료시 소멸하는 특징을 가지고 있다.
이전에 정리했을 때 참고한 블로그의 이미지가 가장 이해하기 쉬워서 그대로 가져와보았다.
나는 3층에 해당하는 내용을 JVM과 엮어서 생각하다보니 많은 혼란이 있었다. 그러니 나처럼 혼란을 겪지 않으려면 이것만 꼭 기억해두자. 3층에 해당하는 내용은 개념적인 이론일뿐, 실제로 구현된건 모두 자바로 작성된 코드라는 것이다. 그리고 이 코드들은 JVM을 통해서 실행해준다는 점!
스프링의 흐름이라고 한다면, 사실 DispatcherServlet을 처리하는 과정을 이해하는 것과 동일하다고 생각한다. 스프링의 꽃인 DispatcherServlet을 스프링 부트 기준으로 확인해보자.
전체적인 동작 과정을 잘 표현한 그림이라고 생각해서 참고한 블로그에서 가져와보았다.
가장 중요한 코드들만 확인해보자.
public class DispatcherServlet extends FrameworkServlet {
// 서블릿이 사용할 전략들을 초기화하는 과정
// 각 전략들을 설정하는 과정에서 빈이 없을 경우 null이거나 기본값이 적용된다.
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
// 요청시 가장 먼저 호출되는 메서드
@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
// (생략)...
try {
doDispatch(request, response);
}
// (생략)...
}
// Dispatch는 각 요청을 처리할 Handler에게 분배하는 과정이다.
// DispatcherServlet의 가장 메인이 되는 메서드이기에 코드 생략은 하지않았다.
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
// Multipart 요청인지 확인하는 코드
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// 요청에 해당하는 HandlerMapping 찾는 코드
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// Handler를 실행할 HandlerAdapter를 찾는 코드
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// GET 요청일 때 캐시와 관련하여 처리하는 코드
String method = request.getMethod();
boolean isGet = HttpMethod.GET.matches(method);
if (isGet || HttpMethod.HEAD.matches(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
// Handler를 실행하기 전 전처리 (Interceptor)
// Interceptor 통과시에는 true, 그렇지 않을 경우에는 false
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// HandlerAdapter로 Handler를 실행하는 코드
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
// ModelAndView가 null이 아니고 View가 있을 경우 요청에 해당하는 기본 View 이름을 설정
applyDefaultViewName(processedRequest, mv);
// Handler를 실행하기 전 후처리 (Interceptor)
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
if (this.handlerAdapters != null) {
for (HandlerAdapter adapter : this.handlerAdapters) {
if (adapter.supports(handler)) {
return adapter;
}
}
}
throw new ServletException("No adapter for handler [" + handler +
"]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}
}
이 코드에서 Handler를 실행하는 handle()
코드를 더 살펴보자. 이 메서드는 HandlerAdapter 인터페이스의 구현체에서 동작하는 메서드다.
public interface HandlerAdapter {
boolean supports(Object handler);
@Nullable
ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
@Deprecated
long getLastModified(HttpServletRequest request, Object handler);
}
구현체에는 여러가지가 있지만, 가장 많이 사용되는 아이(?)를 살펴보자. 우리가 많이 사용하는 것은 RequestMappingHandler이고, 이에 맞는 Adapter는 RequestMappingHandlerAdapter다. 그럼 RequestMappingHandlerAdapter 클래스에 들어가서 handle()
메서드를 찾아보자.
여기서 이상한점을 눈치챌 수 있을 텐데, handle()
메서드는 어디에도 보이지 않는다는 것을 알 수 있다. 😫
살펴보니 RequestMappingHandlerAdapter는 AbstractHandlerMethodAdapter를 구현했는데, 이 추상 클래스를 들어가서 살펴보면 handle()
은 결국 handleInternal()
메서드를 호출하는 것을 확인할 수 있다. 그럼 이제 handleInternal()
메서드를 살펴보자.
이 메서드에서는 또 다시 한번 Handler를 실행하는 invokeHandlerMethod()
가 호출되는데, 그 결과값으로 ModelAndView를 받아와 반환해주는 것을 알 수 있다.
너무 깊게 들어가면 머리가 아프니 여기까지만 들어가자... 😅 참고로 Handler에 대한 정보는 HandlerMethod 객체에 담겨져있다! 🤭
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
implements BeanFactoryAware, InitializingBean {
@Override
protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ModelAndView mav;
checkRequest(request);
// Execute invokeHandlerMethod in synchronized block if required.
if (this.synchronizeOnSession) {
HttpSession session = request.getSession(false);
if (session != null) {
Object mutex = WebUtils.getSessionMutex(session);
synchronized (mutex) {
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
// No HttpSession available -> no mutex necessary
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
// No synchronization on session demanded at all...
mav = invokeHandlerMethod(request, response, handlerMethod);
}
if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
}
else {
prepareResponse(response);
}
}
return mav;
}
}
DispatcherServlet에서 기본적으로 제공하는 전략들은 뭘까? DispatcherServlet.properties 파일을 열어보면 쉽게 확인할 수 있다.
DispatcherServlet에 어떤 Default 전략들이 있고 어떤 역할을 하는지 확인해보기!
[LocaleResolver]
- AcceptHeaderLocaleResolver
- "accpet-language"라는 요청 헤더에 명시된 기본 Locale 정보를 사용하는 LocalResolver 구현체
---
[ThemeResolver]
- FixedThemeResolver
- 고정된 테마를 사용하는 ThemeResolver 구현체 (Theme 변경을 사용하지 않음)
---
[HandlerMapping]
- BeanNameUrlHandlerMapping
- URL을 '/'로 시작하는 이름의 Bean과 매핑시켜주는 HandlerMapping 구현체
- RequestMappingHandlerMapping
- @Controller 클래스 내 @RequestMapping 어노테이션을 보고 RequestMappingInfo 객체를 만들어주는 HandlerMapping 구현체
- RouterFunctionMapping
- RouterFunctions를 지원하는 함수형 방식의 HandlerMapping 구현체 (WebFlux)
---
[HandlerAdapter]
- HttpRequestHandlerAdapter
- HttpRequestHandler를 구현한 클래스를 컨트롤러로 사용할 때(Servlet과 유사) 이를 처리하는 HandlerAdapter 구현체
- SimpleControllerHandlerAdapter
- Controller 인터페이스를 구현하여 만든 컨트롤러 클래스에 요청을 보낼 때 사용하는 HandlerAdapter 구현체
- RequestMappingHandlerAdapter
- @Controller 클래스 내 @RequestMapping 어노테이션 핸들러 매핑(RequestMappingHandlerMapping)을 처리하는 HandlerAdapter 구현체
- HandlerFunctionAdapter
- RouterFunRouterFunctionMapping를 처리하기 위한 HandlerFunctions를 지원하는 HandlerAdapter 구현체
---
[HandlerExceptionResolver]
- ExceptionHandlerExceptionResolver
- @ExceptionHandler 어노테이션이 붙은 메소드를 처리하는 HandlerExceptionResolver 구현체
- ResponseStatusExceptionResolver
- @ResponseStatus 어노테이션이 붙은 메소드가 반환하는 HTTP 상태코드를 처리하는 HandlerExceptionResolver 구현체
- DefaultHandlerExceptionResolver
- 표준 Spring MVC 예외를 해당 HTTP 상태 코드로 변환하는 HandlerExceptionResolver 구현체
---
[RequestToViewNameTranslator]
- DefaultRequestToViewNameTranslator
- View 이름이 명시적으로 지정되지 않았을 때, 요청 URI를 View 이름으로 변환해주는 RequestToViewNameTranslator 구현체
- 기본 변환한 파일 확장자와 Prefix, Suffix를 제거
---
[ViewResolver]
- InternalResourceViewResolver
- 서블릿이나 JSP에서 사용하는 InternalResoureView를 지원하는 UrlBasedViewResolver의 하위 클래스
- 명시적인 매핑이 없어도 View의 이름으르 URL로 사용
- View를 찾을 때 지정한 Prefix, Suffix를 추가하여 찾을 수 있음
---
[FlashMapManager]
- SessionFlashMapManager
- HTTP 세션에서 FlashMap 객체를 저장하고 가져오는 FlashMapManager 구현체
어라... 이상하다? 분명 DispatcherServlet 코드에 중단점을 설정하여 확인했을 때는 DispatcherServlet.properties 파일에 있는 내용보다 더 많은 값들이 있고 순서가 달랐다. HandlerMapping을 대표로 살펴보자.
[HandlerMapping]
- RequestMappingHandlerMapping
- BeanNameUrlHandlerMapping
- RouterFunctionMapping
- SimpleUrlHandlerMapping
- WelcomePageHandlerMapping
왜 차이가 있을까?
이에 대한 해답은 - 스프링 부트는 스프링 프레임워크를 사용하기 위한 설정의 많은 부분을 자동화하여 사용자가 정말 편하게 스프링을 활용할 수 있도록 돕기 때문이다.
스프링 부트의 빈 자동 설정 값은 spring.factories 파일에서 확인할 수 있는데, 너무 많은 설정들이 존재하다보니 아직 모든 내용을 훑어보지 못해 정리하지는 못할 것 같다. 프링 부트는 기본적으로 제공하는 설정들이 미리 구성되어있어 편리하다! 라는 점만은 알아두자. 😝
External Libraries
- Gradle: org.springframework.boot:spring-boot-autoconfigure:<version>
- Spring-boot-autoconfigure-<version>.jar
- META-INF
- spring.factories