내장 서버 — 스프링부트에 톰캣이 내장되어 있다는 게 무슨 뜻일까
예전에는 톰캣을 따로 설치하고 WAR 파일을 배포했는데, 스프링부트에서는
java -jar로 바로 실행됩니다. 톰캣이 내장되어 있다는 게 정확히 어떤 의미이고, 어떻게 동작하는 걸까요?
개념 정의
**임베디드 서버(Embedded Server)**는 웹 서버(톰캣, Jetty, Undertow 등)를 애플리케이션의 라이브러리로 포함하여, 별도의 서버 설치 없이 main() 메서드로 직접 서버를 시작하는 방식입니다. 스프링부트는 기본으로 내장 톰캣을 사용합니다.
왜 필요한가
전통적인 배포 방식과 비교해봅시다.
[전통적 방식]
1. 톰캣 서버 설치 (버전 관리 필요)
2. 톰캣 설정 파일 수정 (server.xml 등)
3. WAR 파일 빌드
4. WAR를 톰캣의 webapps/ 디렉토리에 배포
5. 톰캣 재시작
[임베디드 방식]
1. JAR 파일 빌드 (톰캣 포함)
2. java -jar app.jar 실행
임베디드 방식의 장점은 명확합니다.
- 서버 설치와 관리가 필요 없습니다
- 앱과 서버의 버전이 함께 관리됩니다
- Docker 컨테이너화가 쉽습니다
- 개발 환경과 운영 환경이 동일합니다
내부 동작
부팅 과정
SpringApplication.run()
└── createApplicationContext()
└── ServletWebServerApplicationContext 생성
└── onRefresh()
└── createWebServer()
└── ServletWebServerFactory.getWebServer() 호출
└── TomcatServletWebServerFactory
└── Tomcat 인스턴스 생성
└── Connector 설정 (포트, 프로토콜)
└── Engine → Host → Context 설정
└── DispatcherServlet 등록
└── tomcat.start() 호출
ServletWebServerFactory
// 스프링부트 내부 (개념적)
public class TomcatServletWebServerFactory implements ServletWebServerFactory {
@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
Tomcat tomcat = new Tomcat();
// Connector 설정 (HTTP 포트)
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
connector.setPort(this.port); // 기본 8080
tomcat.setConnector(connector);
// Context 설정
Context context = tomcat.addContext("", System.getProperty("java.io.tmpdir"));
// 서블릿 초기화 (DispatcherServlet 등록)
for (ServletContextInitializer initializer : initializers) {
initializer.onStartup(context.getServletContext());
}
tomcat.start();
return new TomcatWebServer(tomcat);
}
}
스레드 모델
[요청 처리 흐름]
클라이언트 → Acceptor Thread (연결 수락)
→ Poller Thread (I/O 이벤트 감지, NIO)
→ Worker Thread Pool (실제 요청 처리)
└── DispatcherServlet → Controller → Service → ...
톰캣은 기본적으로 스레드 풀 기반으로 요청을 처리합니다. 각 HTTP 요청은 하나의 워커 스레드에서 처리되며, 스레드가 모두 사용 중이면 대기열(accept-count)에 들어갑니다.
코드 예제
기본 톰캣 설정
server:
port: 8080 # 서버 포트
servlet:
context-path: /api # 컨텍스트 패스
tomcat:
threads:
max: 200 # 최대 워커 스레드 수 (기본 200)
min-spare: 10 # 최소 유지 스레드 수 (기본 10)
max-connections: 8192 # 최대 동시 연결 수 (기본 8192)
accept-count: 100 # 대기열 크기 (기본 100)
connection-timeout: 20000 # 연결 타임아웃 (ms)
keep-alive-timeout: 20000 # Keep-Alive 타임아웃 (ms)
max-keep-alive-requests: 100 # Keep-Alive 요청 수 제한
스레드 풀 설정 가이드
[요청 처리 용량]
동시 처리 가능: max-connections (8192)
└── 실제 처리: threads.max (200)
└── 대기열: accept-count (100)
└── 거부: Connection Refused
총 수용 가능 연결 = max-connections + accept-count
스레드 수를 무작정 늘리면 안 됩니다.
- 스레드 하나당 약 512KB~1MB의 스택 메모리 소모
- 200 스레드 × 1MB = 200MB
- 컨텍스트 스위칭 비용도 증가
프로그래밍 방식으로 톰캣 커스터마이징
@Component
public class TomcatCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
@Override
public void customize(TomcatServletWebServerFactory factory) {
factory.setPort(9090);
factory.addConnectorCustomizers(connector -> {
Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();
protocol.setMaxThreads(300);
protocol.setMinSpareThreads(20);
protocol.setConnectionTimeout(30000);
// 압축 설정
connector.setProperty("compression", "on");
connector.setProperty("compressibleMimeType",
"text/html,text/xml,text/plain,application/json");
connector.setProperty("compressionMinSize", "1024");
});
}
}
HTTPS 설정
server:
port: 8443
ssl:
key-store: classpath:keystore.p12
key-store-password: changeit
key-store-type: PKCS12
key-alias: myapp
HTTP와 HTTPS 동시 사용
@Configuration
public class HttpsConfig {
@Bean
public ServletWebServerFactory servletContainer() {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
// 추가 HTTP 커넥터 (HTTPS는 application.yml에서 설정)
Connector httpConnector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL);
httpConnector.setPort(8080);
factory.addAdditionalTomcatConnectors(httpConnector);
return factory;
}
}
다른 서버로 전환
Undertow로 전환:
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
// build.gradle
implementation('org.springframework.boot:spring-boot-starter-web') {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
}
implementation 'org.springframework.boot:spring-boot-starter-undertow'
서버 비교:
| 서버 | 특징 | 적합한 경우 |
|---|---|---|
| Tomcat | 안정적, 커뮤니티 크고 레퍼런스 많음 | 대부분의 경우 (기본값) |
| Undertow | 가볍고 빠름, Non-blocking I/O | 경량 서비스, 높은 동시성 |
| Jetty | 유연한 설정, WebSocket 지원 우수 | WebSocket 중심 앱 |
Graceful Shutdown
server:
shutdown: graceful # 우아한 종료 활성화
spring:
lifecycle:
timeout-per-shutdown-phase: 30s # 종료 대기 시간
Graceful Shutdown이 활성화되면:
- 새 요청 수락을 중단합니다
- 진행 중인 요청이 완료될 때까지 대기합니다
- 타임아웃 내에 완료되지 않으면 강제 종료합니다
// 종료 이벤트 감지
@Component
public class ShutdownListener {
@EventListener(ContextClosedEvent.class)
public void onShutdown() {
log.info("애플리케이션 종료 중... 리소스 정리");
}
}
정리
- 스프링부트 내장 서버는 톰캣을 라이브러리로 포함하여
java -jar로 실행합니다 - 기본 톰캣 설정은 max-threads 200, max-connections 8192입니다
- 스레드 수는 무작정 늘리지 말고, CPU 코어 수와 I/O 비율을 고려해야 합니다
WebServerFactoryCustomizer로 프로그래밍 방식의 세밀한 설정이 가능합니다- Undertow나 Jetty로 전환은 의존성만 교체하면 됩니다
- 운영에서는 반드시 Graceful Shutdown을 설정해야 합니다
댓글 로딩 중...