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가 실행되면 무슨 일이 일어나는가?
- JVM이
MANIFEST.MF의Main-Class(JarLauncher)를 실행 - JarLauncher가
BOOT-INF/lib/의 모든 JAR를 커스텀 ClassLoader로 로드 Start-Class(사용자의main()메서드)를 호출SpringApplication.run()이WebApplicationContext를 생성ServletWebServerFactorybean(TomcatServletWebServerFactory)이 내장 Tomcat 인스턴스를 생성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를 실행하면:
JarLauncher가 JAR 내부의BOOT-INF/lib/에 있는 의존성을 클래스패스에 추가Start-Class(com.example.MyApp)의main()을 호출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를 만든다.
생각해 볼 문제
spring-boot-starter-web(Tomcat)과spring-boot-starter-webflux(Netty)를 동시에 의존성에 넣으면 Spring Boot는 어느 서버를 사용하는가?- fat JAR 대신 exploded directory(
java -cp classes:lib/* com.example.App)로 실행하면 어떤 장단점이 있는가? - Virtual Thread 활성화 시
server.tomcat.threads.max설정이 어떻게 달라지는가? - Spring Boot 4.0에서 JSP를 꼭 써야 하는 레거시 애플리케이션은 어떻게 마이그레이션하는가?
- layered JAR에서
snapshot-dependencies계층이 분리된 이유는?
참고
- Spring Boot 4.0 Reference - Embedded Web Servers - 접근 2026-07-10
- Spring Boot 4.0 Reference - Virtual Threads - 접근 2026-07-10
- Spring Boot 4.0 Reference - Layered JARs - 접근 2026-07-10
- Spring Boot 4.0 Release - 접근 2026-07-10
서버 포트와 컨텍스트 경로
# 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
- Spring Boot 4.0과 이전 버전(Spring Boot 3.x)의 가장 큰 차이점은 무엇인가? 마이그레이션 시 주의할 점은?
- 이 글에서 다룬 개념을 실제 프로젝트에 적용할 때 가장 먼저 고려해야 할 것은?
HTTPS와 SSL 설정
Spring Boot는 HTTPS를 간단히 활성화할 수 있다:
PKCS12 또는 JKS 키스토어를 준비하고 server.ssl.* 프로퍼티로 설정한다. 개발 환경에서는 자체 서명 인증서로 테스트하고, 운영에서는 Let's Encrypt 등 CA 인증서를 사용한다. HTTP를 HTTPS로 리다이렉트하려면 추가 필터 구성이 필요하다.
- Spring Boot 4.0 Reference - 접근 2026-07-10
'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 |
