Theme:

예전에는 톰캣을 따로 설치하고 WAR 파일을 배포했는데, 스프링부트에서는 java -jar로 바로 실행됩니다. 톰캣이 내장되어 있다는 게 정확히 어떤 의미이고, 어떻게 동작하는 걸까요?

개념 정의

**임베디드 서버(Embedded Server)**는 웹 서버(톰캣, Jetty, Undertow 등)를 애플리케이션의 라이브러리로 포함하여, 별도의 서버 설치 없이 main() 메서드로 직접 서버를 시작하는 방식입니다. 스프링부트는 기본으로 내장 톰캣을 사용합니다.

왜 필요한가

전통적인 배포 방식과 비교해봅시다.

PLAINTEXT
[전통적 방식]
1. 톰캣 서버 설치 (버전 관리 필요)
2. 톰캣 설정 파일 수정 (server.xml 등)
3. WAR 파일 빌드
4. WAR를 톰캣의 webapps/ 디렉토리에 배포
5. 톰캣 재시작

[임베디드 방식]
1. JAR 파일 빌드 (톰캣 포함)
2. java -jar app.jar 실행

임베디드 방식의 장점은 명확합니다.

  • 서버 설치와 관리가 필요 없습니다
  • 앱과 서버의 버전이 함께 관리됩니다
  • Docker 컨테이너화가 쉽습니다
  • 개발 환경과 운영 환경이 동일합니다

내부 동작

부팅 과정

PLAINTEXT
SpringApplication.run()
  └── createApplicationContext()
      └── ServletWebServerApplicationContext 생성
          └── onRefresh()
              └── createWebServer()
                  └── ServletWebServerFactory.getWebServer() 호출
                      └── TomcatServletWebServerFactory
                          └── Tomcat 인스턴스 생성
                              └── Connector 설정 (포트, 프로토콜)
                              └── Engine → Host → Context 설정
                              └── DispatcherServlet 등록
                              └── tomcat.start() 호출

ServletWebServerFactory

JAVA
// 스프링부트 내부 (개념적)
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);
    }
}

스레드 모델

PLAINTEXT
[요청 처리 흐름]
클라이언트 → Acceptor Thread (연결 수락)
           → Poller Thread (I/O 이벤트 감지, NIO)
           → Worker Thread Pool (실제 요청 처리)
              └── DispatcherServlet → Controller → Service → ...

톰캣은 기본적으로 스레드 풀 기반으로 요청을 처리합니다. 각 HTTP 요청은 하나의 워커 스레드에서 처리되며, 스레드가 모두 사용 중이면 대기열(accept-count)에 들어갑니다.

코드 예제

기본 톰캣 설정

YAML
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 요청 수 제한

스레드 풀 설정 가이드

PLAINTEXT
[요청 처리 용량]
동시 처리 가능: max-connections (8192)
    └── 실제 처리: threads.max (200)
        └── 대기열: accept-count (100)
            └── 거부: Connection Refused

총 수용 가능 연결 = max-connections + accept-count

스레드 수를 무작정 늘리면 안 됩니다.

  • 스레드 하나당 약 512KB~1MB의 스택 메모리 소모
  • 200 스레드 × 1MB = 200MB
  • 컨텍스트 스위칭 비용도 증가

프로그래밍 방식으로 톰캣 커스터마이징

JAVA
@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 설정

YAML
server:
  port: 8443
  ssl:
    key-store: classpath:keystore.p12
    key-store-password: changeit
    key-store-type: PKCS12
    key-alias: myapp

HTTP와 HTTPS 동시 사용

JAVA
@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로 전환:

XML
<!-- 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>
GROOVY
// 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

YAML
server:
  shutdown: graceful                    # 우아한 종료 활성화

spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s     # 종료 대기 시간

Graceful Shutdown이 활성화되면:

  1. 새 요청 수락을 중단합니다
  2. 진행 중인 요청이 완료될 때까지 대기합니다
  3. 타임아웃 내에 완료되지 않으면 강제 종료합니다
JAVA
// 종료 이벤트 감지
@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을 설정해야 합니다
댓글 로딩 중...