java -jar app.jar 한 줄로 서버가 뜨는 원리 — 내장 서버와 실행 가능 JAR

전통적 Java 웹 애플리케이션은 WAR 파일을 외부 Tomcat에 배포했다. tomcat9, catalina.sh start, deploy/ 디렉토리 — 배포가 복잡하고 환경마다 다르다. Spring Boot는 이것을 java -jar app.jar 한 줄로 끝낸다 — Tomcat이 JAR 안에 내장(embedded)되어 있기 때문이다.

이 글은 내장 서버의 작동 원리, 실행 가능 JAR 구조, layered JAR(컨테이너 최적화), 그리고 Spring Boot 4.0에서 Virtual Thread와 내장 서버의 결합을 풀어간다. (Spring Boot Reference - Embedded Web Servers)

내장 서버 — Tomcat이 JAR 안에

Spring Boot는 웹 애플리케이션을 실행할 때 외부 서버 없이 JAR 안에 포함된 서버를 직접 시작한다.

flowchart TD
    JAR["app.jar"] --> MAIN["main()"]
    MAIN --> SA["SpringApplication.run()"]
    SA --> WEB["WebServerFactory<br/>(Tomcat / Netty / Jetty)"]
    WEB --> EMBED["내장 Tomcat 11 시작<br/>(별도 설치 불필요)"]
    EMBED --> DISP["DispatcherServlet 등록"]
    DISP --> READY["HTTP 요청 처리 준비 완료<br/>(http://localhost:8080)"]
서버 Starter 용도
Tomcat 11 (기본) spring-boot-starter-web 서블릿 기반 REST API
Netty spring-boot-starter-webflux Reactive (WebFlux)
Jetty 12.1 spring-boot-starter-web + jetty exclusion 대체 서블릿 컨테이너
Undertow spring-boot-starter-web + undertow 고성능 서블릿 컨테이너

서버 변경

<!-- Maven: Tomcat 제외 + Jetty 추가 -->
<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-jetty</artifactId>
</dependency>

Tomcat이 기본이지만, 성능 특성이 다른 서버(Jetty, Undertow)로 변경할 수 있다. 대부분의 애플리케이션에서는 Tomcat으로 충분하다 — Tomcat 11은 Spring 7의 Servlet 6.1 기반이다.

비유: 전통적 WAR 배포는 빈 건물을 먼저 세우고(Tomcat 설치) 가구를 들여놓는(WAR 배포) 방식이었다. Spring Boot는 컨테이너 하우스 같다 — 건물과 가구가 하나로 조립되어 완성된 상태로 배달된다. 어디든 가져다 놓기만 하면 즉시 사용할 수 있다.

내부 메커니즘: java -jar app.jar가 실행되면 무슨 일이 일어나는가?

  1. JVM이 MANIFEST.MFMain-Class(JarLauncher)를 실행
  2. JarLauncher가 BOOT-INF/lib/의 모든 JAR를 커스텀 ClassLoader로 로드
  3. Start-Class(사용자의 main() 메서드)를 호출
  4. SpringApplication.run()WebApplicationContext를 생성
  5. ServletWebServerFactory bean(TomcatServletWebServerFactory)이 내장 Tomcat 인스턴스를 생성
  6. DispatcherServlet을 Tomcat에 등록하고 8080 포트로 리슨 시작

이 전체 과정이 단일 Java 프로세스 안에서 수백 밀리초 만에 완료된다.

실행 가능 JAR (fat JAR) 구조

myapp.jar
├── BOOT-INF/
│   ├── classes/                    # 애플리케이션 코드
│   │   └── com/example/...
│   ├── lib/                        # 모든 의존성 JAR
│   │   ├── spring-boot-4.0.0.jar
│   │   ├── spring-framework-7.0.5.jar
│   │   ├── tomcat-embed-core-11.0.x.jar
│   │   └── ... (수십~수백 개)
│   └── classpath.idx              # 클래스패스 인덱스
├── org/springframework/boot/loader/  # JAR 로더
└── META-INF/
    └── MANIFEST.MF
        Main-Class: org.springframework.boot.loader.launch.JarLauncher
        Start-Class: com.example.MyApp

java -jar myapp.jar를 실행하면:

  1. JarLauncher가 JAR 내부의 BOOT-INF/lib/에 있는 의존성을 클래스패스에 추가
  2. Start-Class(com.example.MyApp)의 main()을 호출
  3. SpringApplication.run()이 내장 Tomcat을 시작

Layered JAR — Docker 최적화

Spring Boot 3.2+부터 layered JAR가 기본으로 활성화됐다. JAR를 논리적 계층으로 분리하여 Docker 이미지의 캐시 효율을 극대화한다.

# Spring Boot 4 — JAR에서 계층 추출
java -jar myapp.jar --list-layers
dependencies
spring-boot-loader
snapshot-dependencies
application
# Dockerfile — 계층별 COPY로 캐시 최적화
FROM eclipse-temurin:25-jre
WORKDIR /app
# 의존성 계층 (변경 빈도 낮음 → 캐시 적중률 높음)
COPY dependencies/ ./
# 애플리케이션 계층 (변경 빈도 높음)
COPY application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]

layered JAR의 이점: 코드를 수정해도 의존성 계층이 캐시에 남아 Docker 빌드가 빠르다. CI/CD에서 이미지 빌드 시간을 크게 단축한다.

Virtual Thread와 내장 서버 — Spring Boot 4.0

# Spring Boot 4 / Java 25 — application.yml
spring:
  threads:
    virtual:
      enabled: true   # Tomcat이 Virtual Thread로 요청 처리

이 설정 한 줄로:

  • Tomcat의 요청 처리 스레드가 platform thread → virtual thread로 전환
  • 각 HTTP 요청이 별도의 virtual thread에서 처리
  • I/O 대기(DB, 외부 API) 시 platform thread를 반납 → 더 많은 동시 요청 처리
  • 동기 코드(@Transactional, JDBC, RestClient)를 그대로 쓰면서 reactive 수준의 동시성

Spring Boot 3.2+에서 도입, 4.0에서 안정화. spring.threads.virtual.enabled=true만 설정하면 Tomcat, RestClient, @Async가 모두 virtual thread를 사용한다. 기존 동기 코드를 변경하지 않고 처리량을 크게 높일 수 있다.

Virtual Thread 전환 시 주의

  • synchronized 블록 안의 I/O → pinning 발생 (Java 21~24). ReentrantLock으로 대체 권장.
  • ThreadLocal 남용 → 메모리 폭발. ScopedValue(Java 25 finalized) 권장.
  • 기존 스레드 풀 설정(server.tomcat.threads.max)이 virtual thread 환경에서 다르게 동작.

Spring Boot 4.0 — JSP 제거

Spring Boot 4.0에서 JSP 지원이 제거됐다. (Spring Boot 4.0 Release)

항목 Spring Boot 3 Spring Boot 4
JSP 지원 (WAR 배포 시) 제거
템플릿 엔진 Thymeleaf, JSP Thymeleaf, FreeMarker (JSP 제외)
서블릿 컨테이너 Tomcat 10 Tomcat 11 (Servlet 6.1)

JSP는 레거시 기술로, REST API + 프론트엔드 SPA(React, Vue) 시대에 거의 사용되지 않는다. Spring Boot 4는 이 의존성을 제거하여 더 가벼운 JAR를 만든다.

실습 — 내장 서버 시작 패턴

// Spring Boot 4 / Java 25 — EmbeddedServerDemo.java
// Spring 없이 간단한 HTTP 서버 시뮬레이션
import java.io.*;
import java.net.*;

public class EmbeddedServerDemo {
    public static void main(String[] args) throws Exception {
        // Spring Boot가 내부적으로 하는 일 시뮬레이션
        System.out.println("내장 서버 시작 (포트 8080)");
        try (var server = new ServerSocket(8080)) {
            System.out.println("서버 준비 완료: http://localhost:8080");

            // 첫 번째 요청 처리
            try (var client = server.accept();
                 var out = client.getOutputStream()) {
                String body = "{\"status\": \"running\", \"server\": \"embedded\"}";
                String response = "HTTP/1.1 200 OK\r\n" +
                    "Content-Type: application/json\r\n" +
                    "Content-Length: " + body.length() + "\r\n" +
                    "\r\n" + body;
                out.write(response.getBytes());
                System.out.println("응답 전송 완료");
            }
        }
    }
}
java EmbeddedServerDemo.java &
curl http://localhost:8080
kill %1
내장 서버 시작 (포트 8080)
서버 준비 완료: http://localhost:8080
{"status": "running", "server": "embedded"}
응답 전송 완료

확인할 것: 단일 Java 프로세스가 HTTP 서버 역할을 한다. Spring Boot가 내장 Tomcat을 시작하여 이와 같은 요청 처리를 자동화한다 — 단, Tomcat은 스레드 풀, 서블릿 매핑, 필터 체인 등 훨씬 더 많은 기능을 제공한다.

WAR 배포에서 JAR 배포로 — Spring Boot의 혁신

항목 WAR (전통) JAR (Spring Boot)
서버 설치 외부 Tomcat 설치 필요 불필요 (내장)
배포 방식 WAR 파일을 webapps/에 복사 java -jar app.jar
버전 관리 서버와 앱 버전 분리 하나의 JAR에 통합
환경 일관성 서버마다 설정 다름 JAR 하나 = 환경
CI/CD WAR 빌드 + 서버 배포 JAR 빌드 + 실행
롤백 서버 재설정 + WAR 교체 이전 JAR로 교체

Spring Boot의 내장 서버는 단순한 편의가 아니라 배포 모델의 근본적 변화다. "서버를 설치하고 WAR를 배포"에서 "실행 가능한 JAR를 실행"으로. 컨테이너(Docker)와 완벽하게 부합한다.

요약 — 이 글의 결론

  • 내장 서버는 JAR 안에 Tomcat/Jetty/Netty를 포함한다. 외부 서버 설치 없이 java -jar app.jar로 실행. Spring Boot 4.0은 Tomcat 11(Servlet 6.1)을 기본으로 사용한다.
  • 실행 가능 JAR(fat JAR)는 모든 의존성을 하나의 JAR로 묶는다. JarLauncher가 클래스패스를 설정하고 애플리케이션을 시작한다.
  • Layered JAR로 Docker 캐시를 최적화한다. 의존성 계층과 애플리케이션 계층을 분리하여, 코드 변경 시 의존성 다운로드를 건너뛴다.
  • spring.threads.virtual.enabled=true로 Virtual Thread를 활성화한다. 동기 코드를 변경하지 않고 동시 처리량을 크게 높인다. Spring Boot 4.0에서 안정화.
  • Spring Boot 4.0은 JSP를 제거했다. REST API + 프론트엔드 SPA 시대에 더 가벼운 JAR를 만든다.

생각해 볼 문제

  1. spring-boot-starter-web(Tomcat)과 spring-boot-starter-webflux(Netty)를 동시에 의존성에 넣으면 Spring Boot는 어느 서버를 사용하는가?
  2. fat JAR 대신 exploded directory(java -cp classes:lib/* com.example.App)로 실행하면 어떤 장단점이 있는가?
  3. Virtual Thread 활성화 시 server.tomcat.threads.max 설정이 어떻게 달라지는가?
  4. Spring Boot 4.0에서 JSP를 꼭 써야 하는 레거시 애플리케이션은 어떻게 마이그레이션하는가?
  5. layered JAR에서 snapshot-dependencies 계층이 분리된 이유는?

참고

서버 포트와 컨텍스트 경로

# Spring Boot 4 / Java 25 — application.yml
server:
  port: 8080                    # HTTP 포트
  servlet:
    context-path: /api/v1       # 컨텍스트 경로
  ssl:
    enabled: true
    key-store: classpath:keystore.p12
    key-store-password: ${SSL_KEYSTORE_PASSWORD}
    key-store-type: PKCS12
  compression:
    enabled: true               # HTTP 응답 압축
  http2:
    enabled: true               # HTTP/2 지원

Tomcat 스레드 설정

server:
  tomcat:
    threads:
      max: 200                  # 최대 스레드 수
      min-spare: 10             # 최소 대기 스레드
    max-connections: 8192       # 최대 연결 수
    accept-count: 100           # 수락 대기 큐
    connection-timeout: 20s     # 연결 타임아웃

Virtual Thread 활성화 시 threads.max는 무시된다 — 각 요청이 별도의 virtual thread에서 처리되므로 스레드 풀 크기가 의미 없다. 대신 max-connections로 동시 연결 수를 제어한다.

HTTP/2 활성화

Spring Boot는 HTTP/2를 기본적으로 지원한다 — server.http2.enabled=true 설정만으로 활성화. Tomcat 11은 h2c(cleartext HTTP/2)와 h2(TLS HTTP/2)를 모두 지원한다.

서버 선택 가이드

서버 장점 단점 적합한 용도
Tomcat 11 커뮤니티 큼, 안정적 다소 무거움 일반적 REST API
Jetty 12.1 가벼움, 빠른 시작 생태계 작음 임베디드, 경량
Undertow 고성능, 저메모리 설정 복잡 고동시 실시간
Netty (WebFlux) non-blocking, reactive 동기 코드 안 됨 Reactive

90%의 애플리케이션은 Tomcat으로 충분하다. 특별한 성능 요구가 있을 때만 대체 서버를 검토한다.

환경 변수로 배포

같은 JAR을 환경별로 다르게 실행:

SPRING_PROFILES_ACTIVE=prod java -jar app.jar
  1. Spring Boot 4.0과 이전 버전(Spring Boot 3.x)의 가장 큰 차이점은 무엇인가? 마이그레이션 시 주의할 점은?
  2. 이 글에서 다룬 개념을 실제 프로젝트에 적용할 때 가장 먼저 고려해야 할 것은?

HTTPS와 SSL 설정

Spring Boot는 HTTPS를 간단히 활성화할 수 있다:

PKCS12 또는 JKS 키스토어를 준비하고 server.ssl.* 프로퍼티로 설정한다. 개발 환경에서는 자체 서명 인증서로 테스트하고, 운영에서는 Let's Encrypt 등 CA 인증서를 사용한다. HTTP를 HTTPS로 리다이렉트하려면 추가 필터 구성이 필요하다.

'Develop Artifacts > Spring Boot' 카테고리의 다른 글

SpringBoot - 06. testing  (0) 2026.07.12
SpringBoot - 05. spring-data-jpa  (0) 2026.07.12
SpringBoot - 4. rest api  (0) 2026.07.12
SpringBoot - 02. starter-config  (0) 2026.07.12
SpringBoot - 01. auto configuration  (0) 2026.07.12