Spring 은 워낙 추상화를 잘 해두었기 때문에 Spring 을 사용하는 개발자는 스프링의 구조에 대해서 전혀 몰라도 편하게 개발을 할 수 있다. 하지만 더욱 깊은 이해를 위해서 내부 동작에 대해 찾아보면 그만큼 복잡한 구조를 맞이하게 된다. HTTP 요청이 들어왔을 때 요청이 어떤 플로우를 거쳐서 로직이 처리되는지, 더 단순한 구조로 구성되어도 될것 같은데 왜 객체의 역할들을 상세하게 분리해두었는지 등등 스프링이 현재 구조를 가지게 된 이유를 고민해가며 정리해보았다.

DispatcherServlet 이 모든 요청을 우선 처리

일단 클라이언트로부터 HTTP 요청이 들어오면 DispatcherServlet 이 가장 먼저 요청을 받는다. 그리고 적절한 컨트롤러를 찾아 개발자가 직접 구현한 로직을 수행하고, 해당 결과를 DispatcherServlet 이 받아 클라이언트에게 응답한다. 즉, 요청이 들어왔을 때 가장 먼저 관련 처리를 수행하고, 응답을 보낼 때 가장 마지막에 처리를 수행한다. 스프링에 들어온 요청의 처음과 끝을 담당한다. 이렇게 여러 컨트롤러로 분산되어 있는 환경에서 하나의 객체가 우선 요청을 받고 적절한 컨트롤러에게 분산하는 방식을 Front Controller Pattern 이라고 한다.

실제 개발자가 정의한 비즈니스 로직을 수행하기 위해서는 적절한 Controller 를 찾아 Service 를 처리해야 한다.

DispatcherServlet -> (...) -> Controller

https://mangkyu.tistory.com/18

https://mangkyu.tistory.com/18

DispatcherServlet 은 HttpServletRequest 객체를 받는다. 클라이언트의 HTTP 요청에 대한 정보가 들어있으며, 이 정보를 분석하여 적절한 Controller 를 찾는다.

Controller 를 구현하는 다양한 방식

위의 그림을 보면 DispatcherServlet 에서 Controller 로 도달하기까지 HandlerMapping 과 HandlerAdapter 를 거쳐와야 한다. 각각 실행할 Controller 를 찾고, Controller 의 메서드를 실행하는 역할을 담당하는데 왜 이렇게 복잡한 구조를 가지게 되었을까?

개발이 발전하면서 Controller 를 구현하는 방식이 다양해지고 있다. 가장 간편하고 가장 많이 사용되는 방식은 어노테이션을 이용한 방식일 것이다. 라우팅받을 경로와 필요한 파라미터 등등을 선언하는 것만으로 내가 원하는 Controller 를 실행할 수 있다. 어노테이션 방식 전에도 다양한 방식이 있는데, 이처럼 Controller 를 구현하는 방식은 여러 차례 발전해왔다. 이전에 사용하던 방식을 현재에도 혼용하여 사용할 수 있다. 간단히 말하자면, Controller 를 구현하는 방식이 다양해졌고 방식이 확장됨에 따라 중심이 되는 DispatcherServlet 이 영향을 받지 않도록 각 기능을 세분화하여 한단계 더 추상화한 것이다.

현재는 Controller 를 구현하기 위한 방식이 총 4가지가 있다.

1. Servlet

JSP 를 사용할 당시에 구현하던 방식이다. 받아온 요청을 토대로 적절한 Servlet 을 찾아 로직을 처리하고, Response 객체에 원하는 파라미터 값을 주입시킨다. JSP 에서 스프링 프레임워크로 변경할 때 사용하던 Servlet 코드를 Controller 로 그대로 적용할 수 있다.

package springbook.learningtest.spring.web.controllers;

...

public class ServletControllerTest extends AbstractDispatcherServletTest {
	@Test
	public void helloServletController() throws ServletException, IOException {
		setClasses(SimpleServletHandlerAdapter.class, HelloServlet.class); // 빈 등록
		initRequest("/hello").addParameter("name", "Spring");
		assertThat(runService().getContentAsString(), is("Hello Spring"));
	}
	
	@Component("/hello")
	static class HelloServlet extends HttpServlet {
		protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
			String name = req.getParameter("name");
			resp.getWriter().print("Hello " + name);
		}
	}
}

2. HttpRequestHandler

3. Controller Interface

4. Annotation 기반 Contoller Class

Controller 를 실행하기 위한 HandlerAdapter

원래 요청 흐름상 HandlerAdapter 보다는 HandlerMapping 이 먼저 실행된다. 하지만 DispatcherServlet 의 흐름을 보다보면 ‘왜 HandlerMapping 과 HandlerAdapter 를 분리했을까?’ 하는 생각이 든다. 이를 이해하기 위해 먼저 HandlerAdapter 를 보자.