Spring DispatchServlet이 대체 뭐지???

fart man·2026년 1월 5일

Diagram과 실제 코드

diagram

이런 다이그램을 많이 보았지만 이게 실제로 코드랑은 어떻게 연결되는지 몰라 debugger를 이용해 뜯어 보기로 했다.

이를 위해 간단한 프로그램을 작성하였다.

package adapter;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.*;
import org.springframework.stereotype.Service;
import org.springframework.ui.Model;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.View;
import org.springframework.web.HttpRequestHandler;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.core.Ordered;
import org.springframework.boot.web.server.servlet.context.ServletComponentScan;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import java.util.Locale;
import java.io.IOException;
import java.util.Map;
import java.util.List;

@SpringBootApplication
@ServletComponentScan
public class AdapterApplication {

    @org.springframework.stereotype.Controller
    public static class AnnotationController{
        @GetMapping("/annotation")
        public String hello(Model model) {
            model.addAttribute("message", "annotation");
            return "page";
        }
    }
    
    @WebServlet("/servlet")
    public static class RawServlet extends HttpServlet {
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
            res.setContentType("text/html; charset=UTF-8");
            res.getWriter().print("servlet");
        }
    }

    public static class InterfaceController implements  org.springframework.web.servlet.mvc.Controller{
        @Override
        public ModelAndView handleRequest(
            HttpServletRequest req,
            HttpServletResponse res
        ) {
            return new ModelAndView("page", Map.of("message", "interface"));
        }
    }

    public static class ReqResController implements HttpRequestHandler {
        @Override
        public void handleRequest(
            HttpServletRequest req,
            HttpServletResponse res
        ) throws IOException {
            res.setContentType("text/plain; charset=UTF-8");
            res.getWriter().print("reqres");
        }
    }

    public static class MyView implements View {
        @Override
        public void render(Map<String, ?> model, HttpServletRequest req, HttpServletResponse res) throws IOException {
            res.getWriter().print(String.format("<h1>%s</h1>", model.get("message")));
        }
    }

    public static class MyViewResolver implements ViewResolver, Ordered {
        private MyView myView;

        public MyViewResolver(MyView myView) {
            this.myView = myView;
        }

        @Override
        public View resolveViewName(String viewName, Locale locale) {
            if (viewName.equals("page")) {
                return myView;
            }
            return null;
        }

        @Override
        public int getOrder() {
            return Integer.MIN_VALUE;
        }
    }

    @Configuration
    public static class Config {
        @Bean("/interface")
        public InterfaceController interfaceController() {
            return new InterfaceController();
        }

        @Bean("/req-res")
        public ReqResController reqResController() {
            return new ReqResController();
        }

        @Bean
        public MyView myView() {
            return new MyView();
        }

        @Bean
        public MyViewResolver myViewResolver(MyView myView) {
            return new MyViewResolver(myView);
        }
    }

    public static void main(String[] args) {
        SpringApplication.run(AdapterApplication.class, args);
    }
}

예시로 http://localhost:8080/interface 를 조회해보자

1 요청

DispatchServlet은 HttpServlet의 자식임으로 doGet에서 시작한다

// jakarta.servlet.http.HttpServlet

// 실제 이코드 가 불려지진 않고 이 코드가 Override되서 불려진다.
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    String msg = lStrings.getString("http.method_get_not_supported");
    sendMethodNotAllowed(req, resp, msg);
}

2 Handerler 조회

그럼 이제 저요청을 처리할 handler를 찾아야 하는데, 그 과정이 매우 충격적이게도 Spring답지 않게 사람이 읽으라고 쓰인거 같다.

// org.springframework.web.servlet.DispatcherServlet

private @Nullable List<HandlerMapping> handlerMappings;

...

protected @Nullable 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;
}

저위에 코드가 handler를 가져온다

3 Handler 실행

// org.springframework.web.servlet.DispatcherServlet

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    ...

    mappedHandler = getHandler(processedRequest);
    if (mappedHandler == null) {
        noHandlerFound(processedRequest, response);
        return;
    }

    if (!mappedHandler.applyPreHandle(processedRequest, response)) {
        return;
    }

    // Determine handler adapter and invoke the handler.
    HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

    ...
}

그럼 이제 handler한테 처리해달라고 해야하는데 handler는 우리의 코드에서도 볼 수 있듯이 각기 다르다.
AnnotationController도 있고, InterfaceController도 있고, ReqResController도 있다.

이 셋다 부르는 방법이 다르므로 HandlerAdapter라는 interface로 감싸 주어야 한다.

package org.springframework.web.servlet;

...

public interface HandlerAdapter {
	boolean supports(Object handler);

	@Nullable ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
}
  • InterfaceController는 SimpleControllerHandlerAdpater라는 애가 감싸고
  • AnnotationController는 RequestMappingHandlerAdapter가 감싸고
  • ReqResController는 HttpRequestHandlerAdapter가 감싼다.

(단 RequestMappingHandlerAdapter는 class를 감싸는 것이 아니고 method를 감싼다)

4 ModelAndView 반환

내 InterfaceController가 ModelAndView를 반환한뒤

5 View 해석

ViewResolver한테 View 이름에 따라 View를 달라고 요청한다. 이역시 충격적이게도 사람이 읽을만 하다.

// org.springframework.web.servlet.DispatcherServlet

private @Nullable List<ViewResolver> viewResolvers;

...

private @Nullable View resolveViewNameInternal(String viewName, Locale locale) throws Exception {
    if (this.viewResolvers != null) {
        for (ViewResolver viewResolver : this.viewResolvers) {
            View view = viewResolver.resolveViewName(viewName, locale);
            if (view != null) {
                return view;
            }
        }
    }
    return null;
}

6 View rendering

그리고 저렇게 view를 찾은뒤 여러 과정을 거쳐 MyView로 가는 것을 확인 할 수 있다.

7 response

이 과정이 다 이루어진뒤 뭐 Spring이 다른 뻘짓을 한 후 다시 doGet을 올라가는 것을 볼 수 있다.

근데 RawServlet은 어떻게 작동하는 거지?

셋다 위에서 말한 방식대로 처리되는 것을 볼 수 있다.

근데 http://localhost:8080/servlet 은 어떻게 처리 되는 걸까?

이를 이해할려면 약간의 배경지식이 필요하다. 근데 문제는 나도 배경지식이 없다.

다만 아는 거라면 옛날, 1999년 12월에 J2EE (Java 2 Platform Enterprise Edition)이라는게 있었다는 것이다.

J2EE는 'Java로 웹앱을 짤 거면 이렇게 짜라' 하는 약속이다. 이 약속에는 서버를 짜는 방법, DB한테 물어보는애 짜는 방법, 서버에서 business logic을 짜는 방법등이 적혀 있다.

옛날에는 이 약속을 Sun이 관리하고 있었으나 Oracle이 Sun을 사면서 Eclipse Foundation한테 이걸 관리하라고 떠넘겨 주어서 이제는 Jakatra EE라는 이름으로 불리고 있다.

관심이 있으면 소스코드도 볼수 있다. 무슨 양을 보면 Implementation이 있을거 같으나 실제로 열어보면 interface밖에 없다.

어쨋든 J2EE방식으로 짜인 Server에는 Servlet이라는 것을 돌릴 수 있다. Servlet은 Server안에 살면서 Server가 '여기 요청 왔는데 처리 해줄래?' 하면 처리해주는 애이다.

그리고 Spring은 Tomcat이라는 J2EE Server에 DispatchServlet이라는 J2EE Servlet을 등록하여 돌아간다!

이를 눈으로 확인 할려면 이 코드에 브레이크를 걸면 된다.


// package org.apache.catalina.core;

@Override
public ServletRegistration.Dynamic addServlet(String servletName, String className) {
    return context.addServlet(servletName, className);
}


@Override
public ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet) {
    return context.addServlet(servletName, servlet);
}


@Override
public ServletRegistration.Dynamic addServlet(String servletName, Class<? extends Servlet> servletClass) {
    return context.addServlet(servletName, servletClass);
}

그럼 저 셋중 하나에 servletName = "dispatcherServlet" 으로 걸리게 된다.

Spring은 J2EE가 싫어서 J2EE를 거의 무시하고 돌리기 위해 만든 프레임워크인 것이다.

근데 이게 RawServlet하고 무슨 관련이 있는건데?

잠시 기다리고 Debugger에 continue를 눌러보자.

그럼 addServlet이 한번더 걸리게 된다,

servletName = "adapter.AdapterApplication$RawServlet"으로

그렇다, @WebServlet("/servlet")하는 일은 Tomcat한테 DispatchServlet말고 이 Servlet도 더해 달라고 요청하는 것이다.

결론

Spring 왠 쓰는 건지 모르겠다. 넘 복잡행.

0개의 댓글