웹 애플리케이션에서 브라우저의 요청이 컨트롤러까지 도달하기 위해서는 여러 계층을 거친다.
정적 리소스를 처리하는 웹 서버, 동적 요청을 실행하는 WAS, 자바 HTTP 요청 처리 컴포넌트인 서블릿, 그리고 스프링 MVC의 핵심 서블릿인 DispatcherServlet이 이 흐름 안에서 각각 역할을 가진다.
이 글에서는 웹 서버, WAS, 서블릿, 톰캣, Spring MVC의 관계를 요청 흐름 중심으로 정리한다.
웹 서버, WAS, 서블릿 기본 개념
웹 요청 처리 구조를 이해하려면 먼저 웹 서버, WAS, 서블릿의 역할을 구분해야 한다.
웹 서버
웹 서버는 정적 리소스를 처리한다.
정적 리소스에는 HTML, CSS, JavaScript, 이미지 파일 등이 포함된다.
대표적인 웹 서버로는 Nginx, Apache HTTP Server가 있다.
웹 서버는 클라이언트, 보통 브라우저의 요청을 받아 정적 파일을 그대로 응답한다. 만약 서버에서 계산이 필요하거나 데이터베이스 조회가 필요한 동적 요청이 들어오면 해당 요청을 WAS로 전달한다.
즉, 웹 서버의 핵심 역할은 정적 리소스 응답과 동적 요청 전달이다.
WAS
WAS는 Web Application Server의 약자이다.
WAS는 동적 컨텐츠를 처리한다. 서버에서 계산하거나, DB를 조회하거나, 비즈니스 로직을 실행해야 하는 요청이 WAS에서 처리된다.
자바 기반 웹 애플리케이션에서는 WAS가 자바 웹 애플리케이션 실행 환경을 제공하며, 서블릿 컨테이너를 포함한다.
대표적인 WAS 또는 서블릿 컨테이너로는 Tomcat, Jetty, JBoss가 있다.
WAS도 정적 리소스를 처리할 수 있다. 하지만 보통 운영 환경에서는 정적 리소스 처리는 웹 서버가 맡고, 동적 요청 처리는 WAS가 맡도록 역할을 분리한다.
서블릿
서블릿은 자바로 작성된 HTTP 요청 처리 컴포넌트이다.
일반적으로 HttpServlet 클래스를 상속하고, doGet(), doPost() 같은 메서드를 오버라이드하여 요청을 처리한다.
doGet()은 HTTP GET 요청을 처리한다. 클라이언트가 URL을 통해 정보를 요청하면 서블릿 컨테이너가 doGet()을 호출한다.
doPost()는 HTTP POST 요청을 처리한다. 클라이언트가 폼 데이터를 전송하거나, 서버에 데이터를 저장 또는 변경하라고 요청할 때 doPost()가 호출된다.
서블릿 객체의 생명주기는 톰캣 같은 서블릿 컨테이너가 관리한다.
서블릿은 멀티스레드 방식으로 동작하며, 자바 기반 동적 웹 프로그래밍의 핵심이다.
요청당 스레드 모델
WAS는 보통 요청당 스레드 모델을 사용한다.
즉, 클라이언트 요청이 들어오면 WAS는 스레드 풀에서 스레드를 하나 가져와 해당 요청을 처리한다.
요청당 스레드를 사용하는 이유
웹 서버는 여러 사용자의 요청을 동시에 처리해야 한다.
요청마다 스레드를 할당하면 한 사용자의 요청이 오래 걸리더라도 다른 사용자의 요청은 별도의 스레드에서 독립적으로 처리될 수 있다.
따라서 하나의 요청이 지연되어도 전체 서비스가 멈추지 않는다.
또한 서버는 CPU와 메모리 같은 자원을 최대한 활용해야 한다. 요청마다 스레드를 할당하면 시스템이 허용하는 범위 안에서 가능한 많은 요청을 동시에 처리할 수 있다.
다만 요청마다 스레드를 새로 생성하면 비용이 크다. 그래서 WAS는 보통 미리 만들어둔 스레드 풀을 사용한다.
스레드 풀을 사용하면 스레드 생성과 소멸 비용을 줄이고, 동시에 실행되는 스레드 수를 제한하여 서버가 과부하되는 것을 막을 수 있다.
각 요청은 별도의 스레드에서 처리되므로, 한 요청의 장애나 지연이 다른 요청에 직접 영향을 주지 않는다.
클라이언트당 스레드가 아닌 이유
HTTP는 비연결성, 무상태 프로토콜이다.
웹에서는 한 클라이언트가 여러 요청을 순차적으로 보내거나 동시에 보낼 수 있다.
예를 들어 한 사용자가 여러 탭에서 웹사이트를 열거나, 하나의 페이지에서 이미지, CSS, JavaScript 같은 여러 리소스를 병렬로 요청하는 경우가 많다.
클라이언트마다 스레드를 하나만 할당하면 한 사용자가 여러 요청을 보내도 하나의 스레드만 사용하게 된다. 그러면 병목이 생긴다.
반대로 요청마다 스레드를 할당하면 한 클라이언트가 여러 요청을 보내도 각각 독립적으로 빠르게 처리할 수 있다.
즉, 웹 서버는 클라이언트 단위보다 요청 단위로 실행 흐름을 나누는 것이 동시성 처리에 더 적합하다.
톰캣의 역할과 구조
톰캣은 WAS이자 서블릿 컨테이너이다.
정확히는 Java EE 또는 Jakarta EE의 서블릿과 JSP 사양을 구현한 서버이다.
톰캣은 동적 컨텐츠 처리를 담당하며, 정적 리소스도 직접 처리할 수 있다.
톰캣의 주요 구성 요소
톰캣은 크게 Server, Service, Engine, Host, Connector 같은 컴포넌트로 구성된다.
요청 흐름은 대략 다음과 같다.
Connector가 HTTP 요청을 수신한다.- 요청 정보를 기반으로
Request객체를 생성한다. - 서블릿 컨테이너가 적절한 서블릿을 찾고 실행한다.
- 처리 결과를
Response객체에 담는다. - 클라이언트에게 HTTP 응답을 반환한다.
정적 리소스 요청은 톰캣이 직접 응답할 수 있고, 동적 요청은 서블릿이나 JSP로 처리된다.
스프링 MVC 애플리케이션에서는 이 서블릿이 보통 DispatcherServlet이다.
내장 톰캣과 외장 톰캣
Spring Boot에서는 기본적으로 내장 톰캣을 사용한다.
내장 톰캣은 애플리케이션 안에 톰캣이 포함되어 있고, JAR 파일을 실행하면 톰캣도 함께 실행되는 방식이다.
배포가 간단하고, 별도 서버 설치 없이 애플리케이션을 실행할 수 있다는 장점이 있다.
외장 톰캣은 톰캣을 별도로 설치하고, 애플리케이션을 WAR 파일로 빌드하여 톰캣에 배포하는 방식이다.
외장 톰캣은 서버 설정을 더 세밀하게 제어할 수 있고, 여러 도메인이나 여러 애플리케이션을 하나의 톰캣에서 운영하는 구조에 적합하다.
반면 Spring Boot의 내장 톰캣은 독립 실행형 애플리케이션 배포에 적합하다.
Spring MVC와 서블릿 통합 구조
Spring MVC는 서블릿 위에서 동작한다.
Spring MVC의 핵심 서블릿은 DispatcherServlet이다.
DispatcherServlet은 모든 HTTP 요청을 먼저 받아서 적절한 컨트롤러에 위임하는 역할을 한다.
이 구조는 Front Controller 패턴을 구현한 것이다.
DispatcherServlet의 역할
DispatcherServlet은 스프링 MVC 요청 처리의 중심이다.
클라이언트 요청을 받은 뒤, 어떤 컨트롤러가 이 요청을 처리해야 하는지 찾고, 해당 컨트롤러를 실행한 뒤, 뷰 렌더링 또는 응답 반환까지 전체 흐름을 조율한다.
즉, 컨트롤러는 실제 요청을 처리하는 역할을 하고, DispatcherServlet은 요청 처리 흐름 전체를 관리하는 역할을 한다.
Spring MVC 요청 처리 흐름
Spring MVC에서 일반적인 요청 처리 흐름은 다음과 같다.
- 브라우저가 HTTP 요청을 보낸다.
- 톰캣이 요청을 수신한다.
- 서블릿 컨테이너가
DispatcherServlet에 요청을 전달한다. DispatcherServlet이HandlerMapping을 통해 요청을 처리할 컨트롤러를 찾는다.HandlerAdapter가 컨트롤러 메서드를 호출한다.- 컨트롤러는 필요한 경우 Service, DAO, DB 계층을 호출한다.
- 컨트롤러가 View 이름 또는 응답 데이터를 반환한다.
- View를 사용하는 경우
ViewResolver가 View 객체를 찾는다. - View가 렌더링된다.
DispatcherServlet이 최종 응답을 클라이언트에게 반환한다.
요청 흐름을 간단히 쓰면 다음과 같다.
Client
→ Web Server
→ WAS(Tomcat)
→ DispatcherServlet
→ HandlerMapping
→ Controller
→ Service/DAO
→ ViewResolver
→ View
→ Client
REST API처럼 JSON을 반환하는 경우에는 ViewResolver와 View 렌더링 대신, HttpMessageConverter가 객체를 JSON 같은 응답 본문으로 변환한다.
서블릿과 스프링의 통합
전통적인 방식에서는 web.xml에 DispatcherServlet을 직접 등록했다.
또는 @WebServlet 같은 어노테이션 방식으로 서블릿을 등록할 수도 있다.
Spring Boot에서는 SpringApplication.run() 시점에 자동 설정을 통해 DispatcherServlet이 자동 등록된다.
따라서 개발자가 직접 web.xml을 작성하지 않아도 스프링 MVC 요청 처리 구조가 동작한다.
요청 흐름 확인 방법
실제로 요청이 어떤 방식으로 처리되는지 확인하려면 로깅을 보면 된다.
org.springframework.web.servlet 로그 레벨을 DEBUG 또는 TRACE로 설정하면 DispatcherServlet의 상세 요청 흐름을 확인할 수 있다.
Spring Boot에서는 다음 설정으로 요청 파라미터와 헤더 같은 세부 정보를 더 볼 수 있다.
spring.mvc.log-request-details=true
단순 컨트롤러를 하나 작성한 뒤 요청을 보내면, 로그를 통해 DispatcherServlet, HandlerMapping, Controller, ViewResolver, View 순서의 흐름을 확인할 수 있다.
서블릿 생명주기
서블릿은 컨테이너가 생명주기를 관리한다.
대표적인 생명주기 메서드는 init(), service(), destroy()이다.
init()
init()은 서블릿 인스턴스가 처음 생성될 때 단 한 번 호출된다.
서버가 시작될 때 미리 로드되거나, 해당 서블릿에 대한 첫 요청이 들어왔을 때 실행된다.
주요 역할은 서블릿이 동작하는 데 필요한 리소스를 준비하는 것이다. 예를 들어 DB 연결, 설정 값, 초기화 객체 등을 준비할 수 있다.
초기화 작업이 실패하면 ServletException이나 UnavailableException을 던질 수 있다.
init()은 여러 요청이 와도 한 번만 실행된다. 초기화가 끝난 뒤에는 하나의 서블릿 인스턴스가 여러 요청을 멀티스레드로 처리한다.
service()
service()는 클라이언트의 HTTP 요청이 들어올 때마다 호출된다.
요청을 받아 적절한 작업을 수행하고 응답을 생성한다.
HttpServlet의 경우 service()가 내부적으로 HTTP 메서드에 따라 doGet(), doPost() 등으로 분기한다.
service()는 ServletRequest와 ServletResponse 객체를 파라미터로 받아 요청 데이터와 응답 데이터를 처리한다.
각 요청마다 WAS가 스레드를 할당해 service()를 실행한다.
이때 서블릿 인스턴스는 하나만 생성되고, 여러 스레드가 동시에 service()를 호출할 수 있다.
따라서 서블릿에서 인스턴스 변수를 사용할 때는 동기화 문제에 주의해야 한다.
destroy()
destroy()는 서블릿이 메모리에서 제거될 때 단 한 번 호출된다.
서버 종료, 애플리케이션 재배포 등의 상황에서 호출될 수 있다.
주요 역할은 리소스 정리이다. DB 연결 해제, 파일 닫기, 백그라운드 스레드 종료 같은 작업을 수행한다.
영속적으로 저장해야 할 데이터가 있다면 파일이나 DB에 기록할 수 있다.
destroy()가 호출된 뒤 해당 서블릿 인스턴스는 GC 대상이 된다.
서버는 일반적으로 기존 요청 처리가 끝난 뒤 destroy()를 호출하려고 시도한다. 다만 장시간 실행되는 작업이 있다면 서버의 grace period 안에 완료되지 않을 수도 있다.
DispatcherServlet 내부 동작 순서
DispatcherServlet의 내부 동작은 대략 다음 순서로 볼 수 있다.
doService()
→ doDispatch()
→ getHandler()
→ getHandlerAdapter()
→ handle()
→ processDispatchResult()
→ render()
핵심은 doDispatch()이다.
doDispatch() 안에서 핸들러를 찾고, 핸들러 어댑터를 찾고, 실제 컨트롤러를 실행하고, 그 결과를 처리한다.
WAR 배포
전통적인 외장 톰캣 배포에서는 애플리케이션을 WAR 파일로 빌드하여 톰캣에 업로드한다.
Maven 기준으로는 pom.xml에 다음과 같이 설정할 수 있다.
<packaging>war</packaging>
Spring Boot에서는 내장 톰캣을 사용하는 JAR 배포가 일반적이지만, 필요한 경우 외장 톰캣 배포를 위해 WAR 형태로 패키징할 수 있다.
요약
웹 서버는 정적 리소스를 처리하고, WAS는 동적 컨텐츠를 처리한다.
서블릿은 자바 기반 HTTP 요청 처리 컴포넌트이며, 톰캣은 서블릿 컨테이너이자 WAS이다.
Spring MVC는 서블릿 구조 위에서 동작하며, DispatcherServlet이 모든 요청의 진입점 역할을 한다.
전체 흐름은 다음과 같다.
사용자 요청
→ 톰캣
→ DispatcherServlet
→ HandlerMapping
→ Controller
→ Service/DAO
→ ViewResolver 또는 HttpMessageConverter
→ 사용자 응답
즉, Spring MVC의 컨트롤러는 독립적으로 바로 호출되는 것이 아니라, 톰캣의 서블릿 컨테이너와 DispatcherServlet을 거쳐 실행된다.