Spring에서 DispatcherServlet은 Spring MVC의 단일진입점으로 동작하며,
프론트 컨트롤러의 역할을 맡는 것을 저번 포스트에서 정리하였다.

모든 클라이언트의 HTTP 요청을 가장 먼저 받고,
다양한 컴포넌트들 사이의 중앙 관리자 역할을 수행하며 요청에 대한 처리를 수행하는데

단일진입점으로 동작하는 프론트 컨트롤러의 특성상 사용자 요청이 많아지면
병목현상이 발생하지 않을까 고민하였으나,

DispatcherServlet 자체에서 직접적으로 처리하는 로직은 많이 없고
빠르게 요청에 따른 처리들을 각 컴포넌트에 위임해주어서 부하로 인한 문제는 드물다.

그럼에도 모든 요청을 DispatcherServlet이 먼저 받는다면,
요청을 HttpServletRequest로 감싸고 HttpServletResponse를 함께 생성하는 로직을 수행할텐데 병목현상이 발생할 수 있지 않을까?

위와 같은 의구심이 들어 찾아보다,
클라이언트에서 사용자가 보낸 HTTP요청을 가장 먼저 받는 건, Servlet Container임을 알게 되었다.

이번 포스팅에서는 Servlet Container의 개념과 기능에 대해 알아보며,
HTTP요청과 HTTP응답 과정 사이 속, 어느 계층에서 동작하는지 알아보도록 한다.


1. Servlet

Servlet Cotainer에 대해 알아보기 전에,
DispatcherServlet, Servlet Container, HttpServletRequest, HttpServletResponse
다양한 개념 속 이름에서 등장하는 Servlet이라는게 뭔지 정리해보고 가려 한다.

서블릿(Servlet)이란,
동적 기반 웹 페이지를 만들 때 사용되는 자바 기반의 웹 어플리케이션 프로그래밍 기술을 말한다.

브라우저가 서버에 요청을 보내면 서블릿은

  • 요청을 받고 : 클라이언트의 HTTP 요청 받기
  • 처리하며 : 요청 파라미터 읽기, 비즈니스 로직 실행, DB 조회 및 저장 etc.
  • 응답을 내려준다 : 응답 HTML, JSON, 리다이렉트 생성

Spring MVC의 역할 아닌가?

전통적인 서블릿 방식의 경우 개발자가 서블릿 클래스 내부에서
요청 파라미터 처리, 비즈니스 로직 호출, DB 접근, 응답 생성 등을 직접 구현했다.

(서블릿은 자바 웹 어플리케이션에서 HTTP 요청과 응답을 처리하기 위한 기반 기술)

Spring이 나타나면서 Spring MVC는 기존 서블릿 기반 위에서 동작하며,
DispatcherServlet을 통해 서블릿 기능을 프레임워크 구조 안으로 추상화한 것이다.

(기존 Servlet에서 직접 다루던 웹 요청 기반 처리 작업들을 여러 구성요소로 분리
개발자가 더 구조적으로 작성할 수 있게 만든 서블릿 기반 MVC 프레임워크
)

아래 표를 통해 전통적인 Servlet과 Servlet Container가 수행하던 일들을,
지금의 Spring에서 역할과 책임을 나누어 담당하게 바뀐 전반적인 내용을 담아보았다.

처리 과정담당 구성요소 1담당 구성요소 2담당 구성요소 3
HTTP 요청 최초 수신Servlet ContainerTomcat / Jetty / Undertow
요청/응답 객체 생성Servlet ContainerHttpServletRequestHttpServletResponse
Spring MVC 진입DispatcherServletFront Controller
요청 URL에 맞는 코드 찾기HandlerMappingHandlerExecutionChain
Controller 실행HandlerAdapterRequestMappingHandlerAdapterController Method
요청 파라미터 읽기ArgumentResolver@RequestParam@PathVariable
요청 Body 읽기HttpMessageConverter@RequestBodyJSON → Java 객체
객체 바인딩/검증DataBinderValidatorBindingResult
비즈니스 로직 실행ServiceDomain
DB 조회 저장RepositoryMapperDAO
반환값 처리ReturnValueHandlerHandlerMethodReturnValueHandler
HTML 렌더링ViewResolverViewJSP / Thymeleaf
JSON 응답 생성HttpMessageConverter@ResponseBodyJava 객체 → JSON
예외 처리HandlerExceptionResolver@ExceptionHandler@ControllerAdvice
응답 정리DispatcherServletHttpServletResponse
HTTP 응답 전송Servlet ContainerHTTP Response Message

2. Servlet Container

서블릿 컨테이너(Servlet Container)는 서블릿을 실행하고 관리하는 서버 환경이다.
Spring Boot 애플리케이션의 경우 보통 내장 TomcatServlet Container로 동작하며, Spring MVC의 핵심 서블릿인 DispatcherServlet을 실행하고 관리한다.

서블릿 컨테이너는 가장 먼저 클라이언트가 보낸 HTTP 요청을 수신한다.
이때 요청을 확인하고 어떤 서블릿에게 이를 위임할지 결정하는데,

@WebServlet("/login")
public class LoginServlet extends HttpServlet {
    // 로그인 요청 처리
}

@WebServlet("/users")
public class UserServlet extends HttpServlet {
    // 사용자 요청 처리
}

이는 전통적인 Servlet 구조에 해당하며,

public class LoginServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) {
        String id = request.getParameter("id");
        String password = request.getParameter("password");

        // 로그인 처리
        // 응답 생성
    }
}

한 서블릿 안에 요청 처리, 파라미터 읽기, 비즈니스 로직 호출, 응답 생성 등 여러 로직이 모두 섞여있다.

현대 Spring MVC 구조에서는 URL을 여러 서블릿에 직접 연결하지 않고,
대부분의 요청을 하나의 서블릿인 DispatcherServlet으로 보낸다.
DispatcherServlet은 Spring MVC의 단일 진입점이자 프론트 컨트롤러로 동작하며,
요청 URL과 HTTP 메서드에 맞는 Controller를 찾아 요청을 위임한다.

/login     → DispatcherServlet → LoginController
/users     → DispatcherServlet → UserController
/products  → DispatcherServlet → ProductController
/orders    → DispatcherServlet → OrderController

3. Spring MVC에서 Servlet Container의 기능

서블릿 컨테이너는 서블릿을 실행하고 관리하는 역할을 한다.
서블릿 컨테이너가 실제로 어떤 동작과 기능을 수행하는지 조금 더 자세히 알아보았다.

아래 내용은 Spring Boot 기반 Spring MVC 어플리케이션에서
Servlet Container와 Spring MVC가 HTTP 요청을 처리하며 상호작용하는 과정을 기준으로 설명한다.

0. HTTP 요청 받기

GET /users/1 HTTP/1.1
Host: example.com
Cookie: JSESSIONID=...

Spring MVC 바깥 쪽에서, 클라이언트가 보낸 HTTP 요청을 처음 받아온다.


1. 요청/응답 객체 생성

HttpServletRequest request
HttpServletResponse response

원본 HTTP 요청을 자바 객체로 감싸서 만든다.
추가로 클라이언트에게 보낼 데이터를 모아놓은 응답 객체도 생성한다.

  • HttpServletRequest
    요청 URL, HTTP method, header, cookie, query parameter, body 등 정보를 모아둔 객체 생성

  • HttpServletResponse
    상태 코드, 응답 헤더, 쿠키, 응답 body등을 담기 위한 객체 생성


2. HttpServletRequest , HttpServletResponse 전달

DispatcherServlet에게 생성한 HttpServletRequest, HttpServletResponse를 전달하고,
DispatcherServlet을 호출한다.


3. Filter 실행

서블릿(DispatcherServlet)을 호출하기 전후로 Filter가 실행될 수 있다.

이러한 Filter는 Spring MVC보다 앞단에서 동작하며 주로 다음과 같은 기능들을 수행한다.

  • 문자 인코딩 설정
  • 인증/인가 검사
  • CORS 처리
  • 요청/응답 로깅
  • XSS 방어 처리
  • 압축 처리
  • 요청/응답 래핑
  • 공통 헤더 설정
  • Spring Security 필터 체인 실행
  • 특정 URL 접근 제한 등

Spring MVC보다 앞단에서 동작하기에 모든 요청에 대해
UTF-8 인코딩을 설정하거나, 로그인하지 않은 사용자가 특정 경로에 접근하지 못하도록 검사할 수 있다.

...
import org.springframework.web.filter.OncePerRequestFilter;
...

// JWT 토큰 검사 필터 예시 코드
@RequiredArgsConstructor
public class JWTAuthFilter extends OncePerRequestFilter {
    private final JWTProvider jwtProvider;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    @NonNull HttpServletResponse response,
                                    @NonNull FilterChain filterChain)
            throws IOException, ServletException {
			...

4. 스레드 관리

서블릿 컨테이너는 클라이언트의 요청마다 스레드를 할당하여 여러 요청을 동시에 처리한다.

사용자 A 요청 → Thread-1 → DispatcherServlet → Controller
사용자 B 요청 → Thread-2 → DispatcherServlet → Controller
사용자 C 요청 → Thread-3 → DispatcherServlet → Controller

이때 서블릿 컨테이너는 보통 스레드 풀(Thread Pool) 방식을 사용한다.
요청이 들어올 때마다 매번 새 스레드를 생성하는 것이 아니라,
미리 만들어 둔 스레드를 재사용하여 요청을 처리한다.

Spring MVC에서 DispatcherServlet, Controller, Service는 대부분 싱글톤 객체로 관리된다.
따라서 여러 요청이 동시에 들어오면, 서로 다른 스레드가 같은 객체의 메서드를 동시에 실행할 수 있다.

Spring MVC 싱글톤 빈의 상태 관리와 동시성 문제

싱글톤 빈에 요청마다 달라지는 값을 인스턴스 변수에 저장하면,
여러 스레드가 같은 변수를 동시에 읽고 쓰게 된다.

@Controller
public class UserController {

  private Long currentUserId; // 여러 요청이 공유하는 상태

  @GetMapping("/users/{id}")
  public String getUser(@PathVariable Long id) {
      this.currentUserId = id;
      return "user";
  }
}

그 결과 한 사용자의 요청 데이터가 다른 사용자의 요청 데이터로 덮어써지거나
의도하지 않은 값이 응답에 사용되는 동시성 문제가 발생할 수 있다.

따라서 Controller와 Service같은 싱글톤 빈에는
사용자별 데이터, 요청별 데이터처럼 변하는 상태를 인스턴스 변수로 저장하지 않는 것이 좋다.
요청마다 필요한 값은 메서드 파라미터, 지역 변수, 세션, DB 같은 적절한 위치에 저장해야 한다.


5. 세션 관리

서블릿 컨테이너는 HttpSession 또한 관리한다.

HTTP는 기본적으로 상태를 저장하지 않는 stateless 프로토콜이다.
따라서 서버는 여러 요청이 같은 사용자로부터 온 것인지 구분하기 위해 세션을 사용할 수 있다.

사용자가 처음 서버에 접근하면 서블릿 컨테이너는 필요에 따라 HttpSession을 생성하고,
해당 세션을 식별할 수 있는 JSESSIONID를 클라이언트에게 쿠키로 전달한다.

서버 응답
Set-Cookie: JSESSIONID=abc123

이후 브라우저는 같은 서버로 요청을 보낼 때 JSESSIONID 쿠키를 함께 전송한다.
서블릿 컨테이너는 이 JSESSIONID 값을 보고 서버에 저장된 HttpSession을 찾아낸다.

Servlet Container
→ JSESSIONID 확인
→ 해당 사용자의 HttpSession 조회
→ Controller에서 사용할 수 있도록 제공

컨트롤러에서의 사용 예시

@GetMapping("/mypage")
public String myPage(HttpSession session) {
    Object user = session.getAttribute("loginUser");
    return "mypage";
}

Controller에서 HttpSession을 사용하는 것처럼 보이지만,
실제로 세션 객체의 생성, 조회, 만료 같은 기본 관리는 서블릿 컨테이너가 담당한다.


6. HTTP응답 반환

Controller에서 처리가 끝나면,
최종적으로 DispatcherServletHttpServletResponse 에 응답 관련 데이터를 담는다.

Status: 200 OK
Content-Type: application/json
Body: {"id":1,"name":"Kim"}

이후 응답은 Filter 후처리를 거쳐 다시 Servlet Container로 돌아가고
해당 응답 객체 내부 데이터를 다시 실제 HTTP 응답 메시지로 감싸서 클라이언트에게 전송한다.


7. 서블릿(DispatcherServlet) 생명 주기 관리

서블릿 컨테이너는 자신이 관리하는 서블릿들의 생명 주기를 관리한다.

전통적인 서블릿 방식에서는 여러 개의 서블릿이 URL별로 등록될 수 있고,
서블릿 컨테이너는 각각의 서블릿 객체를 생성, 초기화, 실행, 종료한다.

Spring MVC에서는 대부분의 웹 요청을 하나의 프론트 컨트롤러인 DispatcherServlet이 담당한다.
따라서 Spring MVC 구조에서는 서블릿 컨테이너가
주로DispatcherServlet의 생명 주기를 관리한다고 볼 수 있다.

서블릿 생명 주기는 대략 다음과 같다.

애플리케이션 시작
→ Servlet Container가 DispatcherServlet 생성
→ init() 호출
→ Spring MVC 초기화

요청 발생
→ service() 호출
→ DispatcherServlet이 Spring MVC 요청 처리 수행

애플리케이션 종료
→ destroy() 호출
→ DispatcherServlet 종료

Servlet ContainerDispatcherServlet을 호출한 이후,
Spring MVC 내부의 과정은 기존 포스트에 작성하였다.

profile
개발 및 IT기술에 대해 정리하고 기록합니다.

0개의 댓글