최근 Spring 4 + Java 8 기반의 레거시 시스템을 Spring Boot 3.4 + Java 17로 마이그레이션 했습니다.
초기에는 단순한 라이브러리 버전 업그레이드로 접근했습니다. 하지만 마이그레이션을 진행할수록, 단순한 프레임워크 교체가 아닌 애플리케이션 아키텍처의 재설계였으며, 인프라에 종속되었던 시스템 제어권을 애플리케이션으로 가져오는 전환점이었습니다.
이번 글에서는 마이그레이션 과정에서 경험한 기술적 변화 5가지를 정리했습니다.
- 서블릿 제어권: WAS(Tomcat) 중심에서 Application 중심으로
- 배포 방식: 환경 종속적인 WAR에서 독립적인 JAR로
- 빌드 도구: 정적인 Maven에서 유연한 Gradle로
- 설정 관리: 파편화된 XML에서 중앙 집중형 YAML로
- 런타임: Java 8(Throughput)에서 Java 17(Latency)로
한눈에 보기
프레임워크, 런타임, 빌드 도구 등 인프라 변경 사항입니다.
Spring Boot 3.x로 넘어오며 Jakarta EE 스펙 변경이 강제되었고, 이는 Tomcat 10 이상을 요구합니다.
| 구분 | 변경 전 (Legacy) | 변경 후 (Modern) | 핵심 변경 사유 |
| 프레임워크 | Spring 4 | Spring Boot 3.4.1 | 자동 설정(Auto-Config), 모니터링(Actuator) 확보 |
| 서블릿 제어권 | WAS (Tomcat) | Application (JVM) | 배포 불확실성 제거 |
| Java | Java 8 (Parallel GC) | Java 17 (G1GC) | Stop The World 단축, Record/Switch 등 문법 활용 |
| Spec | Java EE (javax.*) | Jakarta EE (jakarta.*) | Tomcat 10+ 및 Spring Boot 3.x 필수 스펙 대응 |
| 빌드 도구 | Maven (pom.xml) | Gradle (build.gradle) | 유연한 빌드 스크립트, 증분 빌드 속도 향상 |
| 배포 방식 | WAR (External Tomcat) | JAR (Embedded Tomcat) | 컨테이너 의존성 제거, 독립 실행 환경 구축 |
| 설정 관리 | web.xml + XMLs | Java Config + .yml | 설정 중앙 집중화 및 프로그래매틱 제어, 가독성 |
1. 서블릿 제어권: WAS Container → Application
레거시 Spring을 Spring Boot로 바꿈으로써 가장 큰 변화는 프로세스 실행의 주체(Main Context)가 달라졌다는 점입니다.
서블릿 컨테이너(Tomcat)가 주인이고 애플리케이션이 손님이던 관계를, 애플리케이션이 주인이 되어 컨테이너를 부품처럼 사용하는 구조로 역전시켰습니다.
Spring 4 (Legacy): Container-Driven
기존 레거시 환경에서는 외장 톰캣(WAS)이 실행의 주체였습니다.
- 흐름: OS → Tomcat 프로세스 → web.xml 파싱 → Spring Context 로딩
- 문제: 환경 파편화. 운영 서버(Tomcat) 설정과 로컬 개발 환경 설정이 물리적으로 분리되어 있습니다.
"내 로컬에선 되는데 운영에선 안 되는" 이슈가 발생하며, 이는 배포 신뢰성을 떨어뜨리는 주원인입니다.
Spring Boot: Application-Driven
Spring Boot는 애플리케이션(Application)이 제어권을 가집니다.
- 흐름: OS → JVM(Main) → Spring Boot 실행 → 내장 톰캣(Embedded Tomcat) 호출
- 설계: web.xml을 제거하고, ServletWebServerApplicationContext가 톰캣의 생명주기를 관리합니다.
서블릿(DispatcherServlet) 등록 또한 ServletContextInitializer를 통해 자바 코드로 명시적 제어합니다. - 결과: 인프라에 종속되지 않습니다. WAS가 라이브러리로 패키징 되므로, 로컬과 운영 환경이 100% 동일함을 보장합니다. 인프라 종속성을 제거하고 '어디서든 실행 가능한(Self-contained)' 단일 아티팩트를 확보했습니다.
서블릿 컨테이너 구동 방식의 변화

- Legacy Spring (좌): Tomcat이 최상위 컨테이너로 존재하고, 그 안에 WAR가 배포되어 Spring이 '수동적'으로 로딩되는 구조
- Spring Boot(우): Spring Boot 애플리케이션(JAR)이 최상위 프로세스이며, 내부 라이브러리로 포함된 내장 Tomcat을 '능동적'으로 구동하는 구조
2. 배포 방식: WAR → JAR
특정 WAS 버전과 서버 환경에 맞춰야 했던 종속적인 배포 방식(WAR)을 버리고, 실행에 필요한 모든 의존성을 하나로 패키징하는 독립적인 배포 방식(Executable JAR)으로 전환했습니다.
WAR (Web Application Archive)
기존 방식은 애플리케이션이 서블릿 표준 스펙(WEB-INF 구조)을 따르는 아카이브에 불과했습니다.
- 흐름: WAR 빌드 → 운영 서버 접속 → Tomcat 설치/설정 확인 → WAR 업로드 → Tomcat 재기동
- 한계: 실행을 위해서는 반드시 외부에 구성된 서블릿 컨테이너가 필요합니다. 이는 애플리케이션의 생명주기가 호스트 OS에 설치된 Tomcat 버전에 강하게 결합됨을 의미합니다.
- 문제: 서버마다 Tomcat 버전이 조금만 달라도 런타임 에러가 발생합니다. 스케일 아웃 시에도 'Tomcat이 설치된 이미지'를 먼저 프로비저닝해야 하므로 운영 복잡도가 높습니다.
JAR (Executable JAR)
Spring Boot의 Executable JAR는 애플리케이션 구동에 필요한 모든 것(코드+ 라이브러리 + WAS)을 파일 하나에 담습니다.
- 흐름: JAR 빌드 → java -jar app.jar 실행
- 설계: 단순한 압축 파일이 아닙니다. org.springframework.boot.loader.JarLauncher가 메인 클래스로 동작하며, BOOT-INF/lib 내부에 있는 JAR 파일들을 읽어들여 클래스패스를 구성하고 애플리케이션을 구동합니다.
- 결과: "Build Once, Run Anywhere"
- 환경 격리: 인프라에 Java(JRE)만 설치되어 있다면 어디서든 동일하게 실행됩니다.
- 컨테이너 친화적: Docker 이미지로 빌드할 때 복잡한 레이어 구성 없이 JAR 파일 하나만 복사하면 되므로,
Kubernetes 환경에서의 배포 파이프라인도 단순해졌습니다.
| 구분 | WAR | JAR |
| 의존성 | 외부 Tomcat 필수 | 내장 Tomcat 포함 |
| 실행 | WAS가 WAR를 로드 | JVM이 JAR를 직접 실행 (JarLauncher) |
| 확장성 | 서버 프로비저닝 복잡 | 컨테이너/클라우드 배포 최적화 |
| 핵심 가치 | 표준 스펙 준수 | 운영 편의성과 이식성(Portability) |
3. 빌드 도구: Maven → Gradle
pom.xml의 장황한 XML 태그 관리 비용을 줄이고 빌드 효율을 높이기 위해 Gradle로 전환했습니
Maven - 관리 비용과 성능의 한계
기존 프로젝트는 pom.xml 기반의 Maven을 사용했습니다.
- 한계: XML은 선언적이고 정적입니다. 동적인 빌드 로직(예: 환경별 리소스 처리, 복잡한 플러그인 설정)을 구현하려면 XML 설정이 기하급수적으로 늘어나 가독성을 해칩니다.
- 문제: 느린 피드백 루프. Maven은 증분 빌드(Incremental Build) 지원이 미흡합니다. 작은 코드 수정에도 전체 모듈을 다시 검증하고 패키징하는 경우가 많아, CI/CD 파이프라인과 로컬 테스트 시간이 불필요하게 길어졌습니다.
- 코드 예시
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.example</groupId> <artifactId>core-module</artifactId>
<version>${project.version}</version> </dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Gradle - 빌드 캐시와 유연성 확보
Gradle(Kotlin DSL)은 단순한 문법 변화가 아닌, 빌드 프로세스의 효율화입니다.
- 설계:
- 증분 빌드 & 빌드 캐시: 입력(Input)이 변경되지 않은 태스크는 실행을 건너뛰고, 이전에 빌드된 결과를 재사용(Cache)합니다. 이를 통해 빌드 시간을 최대 50~70% 단축했습니다.
- 정교한 의존성 제어: compile 범위가 뭉뚱그려져 있던 Maven과 달리, Gradle은 implementation과 api를 명확히 구분합니다. 내부 구현체가 외부에 노출되는 것을 막아 모듈 간 결합도를 낮췄습니다.
- 코드 예시
// 가독성과 컴파일 타임 체크가 가능한 설정
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
// 의존성이 전파되지 않도록 캡슐화 (컴파일 속도 향상)
implementation(project(":core-module"))
testImplementation("org.junit.jupiter:junit-jupiter")
}
| 구분 | Maven (Legacy) | Gradle (Modern) |
| 설정 언어 | XML (정적, 장황함) | Groovy/Kotlin DSL (동적, 간결함) |
| 빌드 속도 | 전체 빌드 위주 | 증분 빌드 & 빌드 캐시 (고성능) |
| 의존성 | 전이 의존성 제어 어려움 | implementation으로 의존성 캡슐화 |
| 확장성 | 플러그인 작성 복잡 | 스크립트로 로직 즉시 구현 가능 |
4. 설정 관리: XML → YML
web.xml과 여러 경로에 흩어져 있던 XML 설정 파일들을 제거하고, YAML 기반의 계층형 설정과 Profile 전략을 도입하여 환경별(Dev/Prod) 구성의 명확성과 안전성을 확보했습니다.
XML - 낮은 가독성과 응집도
기존 환경은 설정이 물리적으로 파편화되어 있었습니다.
- 구조: web.xml(서블릿), dispatcher-servlet.xml(MVC), context-security.xml(보안) 등으로 파일이 쪼개져 있어 전체 흐름을 파악하기 어렵습니다.
- 문제:
- 수평 구조: .properties 파일은 계층 구조를 표현할 수 없어, server.port, server.tomcat.threads.max처럼 중복된 접두어를 반복해야 합니다.
- 환경 분리 난이도: 로컬과 운영 설정을 분리하기 위해 Maven Profile을 사용하여 빌드 시점에 파일을 교체하거나, 주석으로 토글링하는 위험한 방식을 사용했습니다.
YAML & Profile - 계층화 및 환경 격리
Spring Boot의 application.yml과 Profile 기능을 통해 설정을 중앙 집중화하고 구조화했습니다.
- 설계:
- 계층형 구조(Hierarchy): YAML을 사용하여 관련 설정을 직관적으로 그룹화했습니다.
- Multi-Profile 전략: application-dev.yml, application-prod.yml로 파일을 명확히 분리하고, 실행 시점(-Dspring.profiles.active=prod)에 활성 프로필을 결정합니다. 빌드 아티팩트(JAR) 수정 없이 실행 옵션만으로 환경을 전환합니다.
- 구현 포인트 (Type-Safe Configuration): 단순 키-값(@Value) 참조를 넘어, @ConfigurationProperties를 도입했습니다. 설정값을 자바 객체(POJO)로 매핑하여 컴파일 시점에 타입 체크를 수행하고, IDE의 자동 완성 지원을 받습니다.
# application.yml (공통 설정 및 그룹화)
server:
port: 8080
shutdown: graceful
spring:
profiles:
active: dev # 기본값
datasource:
hikari:
maximum-pool-size: 10
| 구분 | Legacy (XML/Properties) | Modern (YAML/Profile) |
| 형식 | Flat (단순 Key-Value) | Hierarchical (계층 구조) |
| 환경 분리 | 빌드 시점 교체 / 주석 처리 | Runtime 시점 Profile 활성화 |
| 안전성 | 문자열 하드코딩 (@Value) | 객체 매핑 (@ConfigurationProperties) |
| 가독성 | 파일 파편화로 파악 어려움 | 도메인별 응집도 높음 |
5. 런타임 환경: Java 8 → Java 17
Spring Boot 3.0의 베이스라인 상향으로 인한 강제 업데이트가 아닙니다.
런타임 환경 업그레이드 핵심은 'GC 알고리즘 교체를 통한 '응답 지연(Latency)의 예측 가능성 확보'입니다.
5.1 GC 진화: 처리량(Throughput)에서 지연 시간(Latency)으로
가장 큰 변화는 JVM의 메모리 관리 방식입니다.


Java 8 (Parallel GC): "일단 멈추고 대청소"
- 특징: 물리적으로 연속된 메모리(Young/Old)를 통째로 관리합니다. 처리량(Throughput)은 우수하지만, 메모리가 찰 때까지 기다렸다가 한 번에 치우기 때문에 STW(Stop-The-World) 시간이 깁니다.
- 문제: 3D 데이터나 대용량 트래픽 처리 시, 간헐적으로 서버가 멈추는 '랙(Lag)' 현상의 주원인이 될 수 있습니다.
Java 17 (G1GC): "쪼개서 자주 청소"
- 특징: 메모리를 잘게 쪼갠 Region(바둑판) 단위로 관리합니다. 전체를 뒤지는 대신, 가비지가 많이 찬 Region만 우선적으로 청소합니다.
- 이점: 목표 중단 시간(MaxGCPauseMillis)을 설정하여 STW 시간을 통제할 수 있습니다. 응답 속도가 중요한 실시간 서비스나 API 서버에 최적화된 구조입니다.
(GC 상세 내용 및 GC 별 비교는 추후 별도 포스팅으로 다룰 예정입니다.)
5.2 언어적 생산성과 관측성 (Observability)
단순한 문법적 편의를 넘어, 코드의 안전성과 운영 디버깅 효율도 개선됐습니다.
- Record (Java 14+): DTO 작성 시 반복되는 보일러플레이트(Getter, Equals, HashCode)를 제거하고, 데이터 불변성(Immutable)을 언어 차원에서 보장합니다.
- Switch Expression(Java 14+): 값을 반환하는 간결한 Switch 문으로 로직 분기가 명확해졌습니다.
- Text Blocks (Java 15+): JSON, SQL 등 구조화된 텍스트를 이스케이프(\n, \") 없이 원형 그대로 표현하여 가독성을 높였습니다.
- JFR (Java Flight Recorder): 과거 상용 기능이었던 JFR이 기본 탑재되었습니다. 운영 환경에서도 오버헤드(1% 미만) 없이 CPU/메모리 프로파일링이 가능해져 장애 대응 능력이 강화되었습니다.
(참고: Spring Actuator)
(JFR 사용 방법은 추후 별도 포스팅으로 다룰 예정입니다.)
| 구분 | Java 8 (Legacy) | Java 17 (Modern) |
| 기본 GC | Parallel GC (처리량 중심) | G1GC (지연 시간/응답성 중심) |
| 메모리 | STW 시간이 힙 크기에 비례 | Region 단위 청소로 STW 최소화 |
| DTO 작성 | Class + Getter(), Setter() 등 반복 | Record (네이티브 불변 객체) |
| 문자열 | 이스케이프 문자 남발 ("SELECT...") | Text Block ("""...""") |
| 모니터링 | 외부 툴 의존 | JFR 내장 (상시 프로파일링) |
6. 마치며
이번 마이그레이션을 통해 단순한 최신 기술 도입을 넘어, 시스템의 '제어권'과 '안정성'을 확보했습니다.
- 실행 제어 (Application): 외부에 설치된 톰캣에 의존하던 구조를 버리고, 애플리케이션이 스스로 웹 서버를 구동하도록 변경하여 런타임 통제권을 가졌습니다.
- 배포 단위 (JAR): 서로 다른 개발/운영 환경에서 깨지기 쉬운 WAR 구조에서, 어디서든 실행 가능한 불변의 JAR로 전환하여 배포 신뢰성을 확보했습니다.
- 빌드 효율 (Gradle): 느리고 정적인 Maven에서, 캐시 기반의 고속 빌드가 가능한 Gradle로 교체하여 개발 속도를 높였습니다.
- 설정 관리 (YAML): 흩어져 있던 XML 설정을 프로필 기반의 YAML로 통합하여, 환경 격리와 타입 안전성을 챙겼습니다.
- 런타임 성능 (Java 17): 레거시 Parallel GC를 G1GC로 교체하여, 하드웨어 스펙 상향 없이도 응답 지연(Latency) 문제를 구조적으로 개선했습니다.
'개발 노트 > 실무 프로젝트' 카테고리의 다른 글
| 3D 모델 요청/응답 API 설계 (0) | 2025.12.22 |
|---|---|
| 대용량 파일 업로드의 정합성 보장 전략: Atomic Rename과 SeaweedFS 활용 (0) | 2025.11.27 |
| SRP를 만족하는 모듈화 리팩토링 과정 (0) | 2025.11.16 |
| Gzip, Zstandard, Brotli 압축 비교 - 3D 모델 파일 (0) | 2025.10.07 |
| 전 세계 공간정보를 관리하는 자료구조 구현기 - Implicit Tiling (7) | 2025.07.20 |