스프링 부트 정리 관련글
- 스프링 부트 살펴보기
- 독립 실행형 Servlet Application (현재 게시글)
- 독립 실행형 Spring Application
- DI와 테스트, 디자인 패턴
- 자동 구성 기반 Application
- 조건부 자동 구성
- 외부 설정을 이용한 자동 구성
- Spring JDBC 자동 구성 개발
- 스프링 부트 자세히 살펴보기
🌱 Servlet Container의 이해
스프링 부트의 핵심 철학 중 하나는 컨테이너를 의식하지 않아도 된다(Containerless)는 점이다.
하지만 컨테이너가 내부에서 어떻게 동작하는지를 이해하면, 스프링 부트가 자동으로 해주는 일들이 어떤 흐름으로 이뤄지는지 감이 잡힌다.
이번에는 직접 Servlet Container를 띄워보며, 그 구조를 코드 레벨에서 살펴보자!
1) 빈(Empty) Servlet Container 띄우기
서블릿 컨테이너의 대표 주자는 Tomcat이다.
보통은 Tomcat을 설치한 후 server.xml을 수정해서 띄우지만, Tomcat 역시 결국 Java로 작성된 프로그램이다.
즉, Tomcat이라는 클래스의 객체를 만들고 실행 메서드를 호출하면 서버를 직접 띄울 수 있다는 뜻이다.
이런 이유로 Tomcat은 Embedded Tomcat이라는 내장형 라이브러리를 제공한다.
(Spring Boot 프로젝트를 Spring Initializer로 생성했다면 이미 이 라이브러리가 포함되어 있다.)
스프링 부트는 이 내장 톰캣을 코드로 제어할 수 있도록 TomcatServletWebServerFactory라는 유틸리티 클래스를 제공한다.
package com.java.spring;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServer;
public class Application {
public static void main(String[] args) {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
WebServer webServer = factory.getWebServer();
webServer.start();
}
}
- getWebServer() → 실제 웹 서버 인스턴스 생성

→ Tomcat을 제외하고 ServletWebServerFactory로 받아도 된다.
→ ServletWebServerFactory를 구현한 클래스는 getServer라는 것을 지원해야한다. - start() → 톰캣 서블릿 컨테이너 실행

WebServer 타입의 오브젝트
물론 WebServer 인터페이스는 Tomcat뿐 아니라 Jetty, Undertow 등 다른 컨테이너도 동일한 방식으로 실행할 수 있도록 추상화되어 있다.
즉, 특정 컨테이너에 종속되지 않는 인터페이스라는 점이 핵심이다.
가져온 webserver에서 start 메소드를 실행하면, Tomcat Servlet Container가 동작한다

2) 서버 실행 확인
서버가 정상적으로 떴는지 확인하려면 간단히 요청을 보내본다.
# Windows
curl -i "http://localhost:8080"
# Mac
http -v :8080

404 Not Found 응답이 발생했지만,
Tomcat이 정상적으로 기동되어 요청을 받을 준비가 된 상태를 의미하다.
3) Servlet 등록하기
현재 서버에는 서블릿이 등록되어 있지 않아 요청을 받아도 처리할 수 있는 대상이 없다는 뜻이다.
즉, 서블릿 컨테이너가 비어 있는 상태이므로 직접 Servlet을 추가해야 요청을 처리할 수 있다.
getWebServer()는 ServletContextInitializer 타입의 파라미터를 받을 수 있는데,
이 인터페이스는 ServletContext를 구성하기 위한 것으로, 컨테이너 안에서 서블릿을 등록하는 역할을 한다.

별도로 재사용 할 일이 없기 때문에, 보통은 아래처럼 익명 클래스로 바로 구현하기도 한다.
WebServer webServer = factory.getWebServer(servletContext -> {
// Servlet 등록
});

servletContext.addServlet() 메서드는 두 개의 인자를 받는다:
- 첫 번째 인자 : 서블릿의 이름(String servletName)
- 두 번째 인자 : Servlet 타입 객체
💡 HttpServlet을 사용하는 이유
여기서 두 번째 인자로 들어가는 Servlet은 인터페이스다.
직접 구현하려면 init(), service(), destroy() 등 여러 메서드를 모두 오버라이드해야 한다.
public interface Servlet {
void init(ServletConfig config) throws ServletException;
void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
void destroy();
}
이걸 매번 직접 구현하기엔 너무 번거롭기 때문에 자바 표준에서는 공통 기능을 미리 구현해둔 어댑터 클래스인 HttpServlet을 제공한다.
이 클래스는 GenericServlet을 상속받고 HTTP 요청을 처리할 때 필요한 메서드(doGet, doPost, service 등)를 미리 구현해둔 상태다.

그래서 우리는 단순히 HttpServlet을 상속받아서 필요한 메서드만 오버라이드(@Override) 하면 된다.
이때 별도의 클래스를 만들지 않고 익명 클래스로 바로 등록할 수도 있다.
Serlvet을 실제로 등록하기
WebServer webServer = factory.getWebServer(servletContext -> {
servletContext.addServlet("hello", new HttpServlet() {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setStatus(200);
resp.setContentType("text/plain");
resp.getWriter().println("Hello Servlet");
}
}).addMapping("/hello");
});
이제 /hello로 요청이 들어오면 service() 메소드가 실행된다.
curl "<http://localhost:8080/hello>"
# Hello Servlet

4) 요청 파라미터로 동적 응답 만들
요청 파라미터를 읽어 동적으로 응답할 수도 있다.
예를 들어 req.getParameter()를 이용하면 쿼리스트링으로 넘어온 값을 읽을 수 있다.
resp.getWriter().println("Hello " + req.getParameter("name"));
curl -i "http://localhost:8080/hello?name=Spring"
# Hello Spring

🐣Front Controller 패턴의 등장
초기 Servlet 구조에서는 요청 URL마다 개별 Servlet을 등록해야 했다.
서블릿이 많아지면서 중복 코드가 늘고, 요청 분기 로직이 복잡해졌다.
이를 해결하기 위한 첫 시도가 바로 Front Controller 패턴이다.

Front Controller는 모든 요청을 한 곳에서 받고, 공통 처리를 수행한 후 필요한 컨트롤러로 위임한다.
💡 주요 역할
- 공통 로직 처리 (인증, 보안, 다국어 등)
- URL → Controller 맵핑
- 요청 파라미터 바인딩
간단한 Front Controller 구현
WebServer webServer = factory.getWebServer(servletContext -> {
HelloController helloController = new HelloController();
servletContext.addServlet("frontcontroller", new HttpServlet() {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 공통 처리 (예: 인증, 로깅 등)
if (req.getRequestURI().equals("/hello") && req.getMethod().equals("GET")) {
String name = req.getParameter("name");
String returnValue = helloController.hello(name);
resp.setStatus(HttpStatus.OK.value());
resp.setContentType(MediaType.TEXT_PLAIN_VALUE);
resp.getWriter().println(returnValue);
} else {
resp.setStatus(HttpStatus.NOT_FOUND.value());
}
}
}).addMapping("/*");
});
public class HelloController {
public String hello(String name) {
return "Hello " + name;
}
}
이제 Front Controller는 모든 요청을 한 곳에서 받고 URI와 HTTP Method를 기준으로 로직을 분기해준다.
만약 HTTP Method를 POST로 호출하면 아래와 같이 오류가 발생한다.

이 구조에서 중요한 점은 Front Controller 안에서 실제 비즈니스 로직이 동작하는 것이 아니라
HelloController의 hello() 메서드로 작업을 위임하고 그 메서드가 처리한 결과값을 리턴받아 message body에 담아 응답을 생성한다는 점이다.
즉 Front Controller는 요청을 직접 처리하는 주체가 아니라
요청을 적절한 컨트롤러로 연결하고 응답을 만들어 반환하는 조정자(Dispatcher) 역할을 한다.
Mapping과 Binding
Front Controller 구조에서는 두 가지 핵심 작업이 이루어진다.
| 개념 | 설명 |
| Mapping | 들어온 요청(URL, HTTP Method)을 분석해 어떤 로직을 실행할지 결정 |
| Binding | 요청 파라미터를 컨트롤러 메서드의 자바 타입으로 변환 |
예를 들어 /hello?name=Spring 이라는 요청이 들어오면
→ Mapping: /hello → HelloController.hello()
→ Binding: name → "Spring"
이런 식으로 연결된다.
Binding 덕분에 컨트롤러 메서드는 더 이상 HttpServletRequest에 직접 의존하지 않고 순수 자바 타입으로 데이터를 받을 수 있게 된다.
이 개념이 나중에 Spring MVC의 @RequestParam, @ModelAttribute로 발전하게 된다.
DispatcherServlet으로의 발전
Front Controller 패턴은 이후 스프링 MVC의 핵심 구조로 발전했다.
스프링에서는 이 역할을 DispatcherServlet이 맡는다.
DispatcherServlet은 다음과 같은 일을 한다:
| 역할 | 설명 |
| 요청 분기 (Handler Mapping) | 어떤 URL이 어떤 컨트롤러로 가야 하는지 결정 |
| 바인딩 (Handler Adapter) | 요청 파라미터를 메서드 인자 타입에 맞게 변환 |
| 공통 처리 (Interceptor, ExceptionResolver 등) | 로깅, 인증, 예외 처리 같은 공통 로직 수행 |
| 뷰 렌더링 (ViewResolver) | 컨트롤러가 반환한 데이터를 화면(View)으로 변환 |
즉, 우리가 직접 구현했던 Front Controller가 점점 세분화되어 매핑, 바인딩, 예외 처리, 뷰 렌더링이 각각 모듈로 나뉘고
그 중심에서 이를 조율하는 객체가 DispatcherServlet이 된 것이다.
핵심 개념 정리
| 개념 | 설명 |
| Servlet Container | HTTP 요청을 받아 Servlet에게 전달하고, 응답을 만들어 클라이언트에 돌려주는 실행 환경 |
| ServletContextInitializer | 서블릿 컨텍스트를 구성하기 위한 인터페이스 (서블릿/필터 등록 등) |
| HttpServlet | Servlet 인터페이스의 기본 구현체로, 필요한 메서드만 오버라이드 가능 |
| Front Controller | 모든 요청을 한 곳에서 받아 공통 로직 처리 후 실제 컨트롤러로 위임 |
| Mapping | 요청 URL → 처리 대상 서블릿(또는 컨트롤러) 결정 |
| Binding | 요청 파라미터를 로직에 맞는 자바 객체나 타입으로 변환하는 과정 |
🔍정리
- TomcatServletWebServerFactory로 톰캣 인스턴스 생성
- ServletContextInitializer를 이용해 서블릿 등록
- HttpServlet을 상속받아 요청 처리 구현
- Front Controller를 통해 공통 처리와 로직 위임 분리
이 과정을 거치면 스프링 부트 없이도 직접 서블릿 컨테이너를 띄우고 동작시키는 독립 실행형 애플리케이션을 만들 수 있다.
스프링 부트가 내부에서 하는 일을 코드로 직접 체험해보는 셈이다.
'Devlog > 공부 아카이브' 카테고리의 다른 글
| [스프링 부트 정리] 자동 구성 기반 Application (0) | 2025.10.06 |
|---|---|
| [스프링 부트 정리] DI와 테스트, 디자인 패턴 (0) | 2025.10.06 |
| [스프링 부트 정리] 독립 실행형 Spring Application (1) | 2025.10.06 |
| [스프링 부트 정리] 스프링 부트 살펴보기 (0) | 2025.10.04 |
| 면접준비 (4) | 2025.08.23 |