내장 톰캣을 라이브러리 형태로 애플리케이션에 포함하여, main() 메서드 실행만으로 웹 서버를 직접 구동할 수 있게 해주는 기능이다. 이를 통해 개발자는 별도의 WAS 설치나 복잡한 설정 없이 순수 자바 코드로 서블릿 및 스프링 MVC 애플리케이션을 편리하게 실행할 수 있다. 스프링 부트가 내장 톰캣을 활용하여 웹 애플리케이션을 실행하는 근본 원리이기도 하다
내장 톰캣을 이용할 서블릿 애플리케이션 구동
내장 톰캣은 톰캣 서버 자체를 자바 라이브러리처럼 사용하여, 개발자가 직접 코드로 톰캣을 설정하고 구동할 수 있도록 한다
HelloServlet
public class HelloServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("HelloServlet.service");
resp.getWriter().println("hello servlet!");
}
}
- 클라이언트 요청을 받아 “hello servlet!”을 응답하는 간단한 서블릿
EmbedTomcatServletMain (내장 톰캣으로 서블릿 구동)
public class EmbedTomcatServletMain {
public static void main(String[] args) throws LifecycleException {
System.out.println("EmbedTomcatServletMain.main");
// 1. 톰캣 인스턴스 생성 및 포트 설정 (8080)
Tomcat tomcat = new Tomcat();
Connector connector = new Connector();
connector.setPort(8080);
tomcat.setConnector(connector);
// 2. 톰캣 컨텍스트 추가
// 첫 번째 파라미터 (contextPath): 웹 애플리케이션의 컨텍스트 경로
// - ""(빈 문자열) 또는 "/" : 루트 컨텍스트 (http://localhost:8080/)
// - "/myApp" : 서브 컨텍스트 (http://localhost:8080/myApp/)
// 두 번째 파라미터 (docBase): 정적 리소스(HTML, CSS 등)의 기본 디렉토리
// - "/" : 임시 디렉토리 사용 (정적 파일 없을 때 사용)
Context context = tomcat.addContext("", "/");
// 3. 서블릿 등록 (서블릿 이름: "helloServlet", 서블릿 인스턴스: new HelloServlet())
tomcat.addServlet("", "helloServlet", new HelloServlet());
// 4. 서블릿 매핑 (URL 패턴 "/hello-spring"으로 "helloServlet"을 연결)
context.addServletMappingDecoded("/hello-spring", "helloServlet");
// 5. 톰캣 시작
tomcat.start();
// 톰캣은 시작되면 별도의 백그라운드 스레드에서 HTTP 요청을 처리한다.
// 하지만 main() 메서드가 종료되면 JVM 전체가 종료되어 톰캣도 함께 종료되므로,
// await() 메서드를 호출하여 main 스레드를 블로킹 상태로 유지해야 한다.
tomcat.getServer().await(); / 톰캣이 중지될 때까지 대기 (CTRL+C 등으로 종료 가능)
}
}
- 톰캣 설정: Tomcat 객체를 생성하고 Connector를 설정하여 8080 포트에서 HTTP 요청을 수신하도록 구성한다
- 서블릿 등록: tomcat.addContext()로 웹 애플리케이션의 컨텍스트(루트 경로)를 정의한 후, tomcat.addServlet()으로 HelloServlet 인스턴스를 “helloServlet”이라는 이름으로 등록한다. 마지막으로 context.addServletMappingDecoded()를 통해 hello-spring URL 패턴으로 들어오는 요청이 “helloServlet”으로 라우팅되도록 매핑한다
- 톰캣 시작: tomcat.start()를 호출하여 내장 톰캣 서버를 시작한다
- 결과: 이 main() 메서드를 실행하면 별도 톰캣 설치 없이 http://localhost:8080/hello-spring으로 접속하여 “hello servlet!”응답을 받을 수 있다
주의사항(?)
개발자가 직접 내장 톰캣을 세밀하게 다룰 일은 거의 없다. 스프링 부트가 대부분의 내장 톰캣 관련 설정을 자동화해주기 때문이다. 이 코드는 내장 톰캣이 어떤 원리로 동작하는지 개념적으로 이해하는 데 도움이 된다
내장 톰캣을 이용한 스프링 MVC 애플리케이션 구동
내장 톰캣에 스프링 컨테이너와 DispatcherServlet을 연동하면, 스프링 MVC 애플리케이션도 main() 메서드를 통해 직접 구동할 수 있다
HelloController
@RestController
public class HelloController {
@GetMapping("/hello-spring")
public String hello() {
System.out.println("HelloController.hello");
return "hello spring!";
}
}
- 스프링 컨테이너에 의해 관리되며 /hello-spring 요청을 처리하는 컨트롤러이다
HelloConfig
@Configuration
public class HelloConfig {
@Bean
public HelloController helloController() {
return new HelloController();
}
}
- HelloController를 스프링 빈으로 등록하는 설정 클래스이다
EmbedTomcatSpringMain
public class EmbedTomcatSpringMain {
public static void main(String[] args) throws LifecycleException {
System.out.println("EmbedTomcatSpringMain.main");
// 1. 톰캣 인스턴스 생성 및 포트 설정 (8080)
Tomcat tomcat = new Tomcat();
Connector connector = new Connector();
connector.setPort(8080);
tomcat.setConnector(connector);
// 2. 스프링 컨테이너 생성 및 설정 등록
AnnotationConfigWebApplicationContext appContext = new AnnotationConfigWebApplicationContext();
appContext.register(HelloConfig.class); // HelloConfig 설정 클래스를 등록
// 여기서는 아직 스프링 빈이 생성되지 않음 (설정만 등록된 상태)
// 3. DispatcherServlet 생성 및 스프링 컨테이너 연결
DispatcherServlet dispatcher = new DispatcherServlet(appContext);
// DispatcherServlet이 초기화될 때 appContext.refresh()를 호출하여
// 스프링 컨테이너를 초기화하고 빈들을 생성한다.
// 초기화 시점: 톰캣 시작 직후 첫 번째 HTTP 요청이 들어올 때
// 4. 디스패처 서블릿을 톰캣 컨텍스트에 등록 및 URL 매핑 ("/" 모든 요청 처리)
Context context = tomcat.addContext("", "/"); // 루트 컨텍스트 생성
tomcat.addServlet("", "dispatcher", dispatcher); // "dispatcher" 이름으로 DispatcherServlet 등록
context.addServletMappingDecoded("/", "dispatcher"); // 모든 요청을 "dispatcher"로 매핑
// 5. 톰캣 시작
tomcat.start();
tomcat.getServer().await(); // main 스레드가 종료되지 않도록 대기
}
}
- 톰캣 설정: 이전과 동일하게 톰캣을 생성하고 8080 포트를 설정한다
- 스프링 컨테이너 생성: AnnotationConfigWebApplicationContext를 생성하고 HelloConfig.class를 등록하여 HelloController와 같은 스프링 빈들이 컨테이너에 의해 관리되도록 한다
- DispatcherServlet 연결: DispatcherServlet을 생성하면서 위에서 만든 appContext (스프링 컨테이너)를 전달하여 연결한다. 이제 DispatcherServlet은 스프링 컨테이너의 빈들을 알고 요청을 처리할 수 있다
- DispatcherServlet 등록 및 매핑: tomcat.addContext()로 컨텍스트를 정의하고 tomcat.addServlet()으로 DispatcherServlet을 등록한다. 이때 /로 매핑하여 모든 HTTP 요청이 DispatcherServlet을 통해 처리되도록 설정한다
- 초기화 타이밍: 기본적으로 DispatcherServlet은 첫 번째 HTTP 요청이 들어올 때 초기화된다. 이때 스프링 컨테이너의 refresh()가 호출되어 빈들이 생성된다. 즉시 초기화하려면 Tomcat.addServlet() 반환값에 .setLoadOnStartup(0)을 설정한다
- 톰캣 시작: tomcat.start()를 호출하여 내장 톰캣을 구동한다
- 결과: 이 main() 메서드를 실행하면 별도 톰캣 설치나 복잡한 WAS 설정 없이 http://localhost:8080/hello-spring으로 접속하여 “hello spring!” 응답을 받을 수 있다
코드 실행 흐름
- main() 메서드 시작
- Tomcat 인스턴스 생성 및 포트 설정
- AnnotationConfigWebApplicationContext 생성 (아직 초기화 안됨)
- DispatcherServlet 생성 및 스프링 컨테이너 연결
- DispatcherServlet 톰캣에 등록 및 URL 매핑
- tomcat.start() → 톰캣 서버 시작 (백그라운드 스레드)
- 첫 번째 HTTP 요청 도착
- DispatcherServlet.init() 호출 → appContext.refresh() 실행 → 스프링 빈 생성(HelloController 등)
- DispatcherServlet이 요청을 처리하여 응답 반환
내장 톰캣을 직접 코드로 다루는 이 과정은 ServletContainerInitializer를 통한 서블릿 컨테이너 초기화는 “서블릿 컨테이너에 서블릿을 등록하고 웹 애플리케이션을 구동한다”는 목적은 동일하지만, 제어 방식에서 근본적인 차이가 있다
제어 방식의 차이
- 내장 톰캣: 개발자가 main() 메서드에서 톰캣 인스턴스를 직접 생성하고 제어한다. 톰캣은 하나의 라이브러리로 취급된다
- ServletContainerInitializer: WAS(톰캣)가 주체가 되어 WAR 파일을 배포할 때 개발자가 작성한 초기화 코드를 콜백으로 호출한다. 톰캣이 개발자 코드를 호출하는 구조이다
실무 관점
내장 톰캣을 직접 코드로 다루는 경우는 매우 드물기 때문에 대부분 스프링 부트가 이 모든 과정을 자동화하기 떄문이다. 하지만 다음과 같은 상황에서 내장 톰캣을 직접 다뤄야 할 수도 있다
- 커스텀 서블릿 컨테이너 설정이 필요한 경우 (예: 특수한 Connector 설정, SSL 인증서 프로그래밍 방식 적용)
- 스프링 부트 없이 경량 웹 애플리케이션을 만들어야 하는 경우
- 톰캣 외 다른 서블릿 컨테이너(Jetty, Undertow)로 전환해야 하는 경우
스프링 부트는 내부적으로 이와 유사한 내장 톰캣을 제어하며, application.properties (또는 yml)의 server.port, server.servlet.context-path 같은 설정들이 결국 이러한 코드로 변환되어 적용된다