여러분이 윈도우(Windows) 노트북에서 Java 17을 설치하고 스프링 부트(Spring Boot) 서버를 완벽하게 개발했습니다. "완벽해! 이제 배포하자!"라며 리눅스(Linux) 운영 서버에 .jar 파일을 올리고 실행했는데, 갑자기 "Unsupported Class Version Error"가 뜨며 서버가 뻗어버립니다. 알고 보니 리눅스 서버에는 Java 11이 깔려있었던 것입니다. 부랴부랴 Java 17로 업데이트했더니 이번엔 파일 경로(Path) 문제로 이미지가 엑스박스가 뜹니다. 개발자의 로컬 환경, 테스트 서버 환경, 라이브 운영 서버 환경이 조금씩 달라서 생기는 이 지긋지긋한 '환경 불일치 문제'는 지난 수십 년간 수많은 개발자의 밤을 지새우게 만들었습니다. 이 고통을 완벽하게 끝내기 위해 "애플리케이션(jar)뿐만 아니라, 자바 버전, 운영체제(OS), 라이브러리 세팅까지 통째로 하나의 박스(Container)에 꽉꽉 압축해서 배포하자!"는 혁명적인 기술이 탄생했습니다. 바로 Docker(도커)입니다.
이번에는 "클라우드 네이티브의 절대 표준!" Spring Boot 애플리케이션을 도커라이징(Dockerizing)하는 최적의 Dockerfile 작성 비법에 대하여 아주 치밀하게 딥다이브 해보겠습니다.

🐳 1. Dockerfile: 컨테이너를 찍어내는 붕어빵 틀
도커의 핵심은 Dockerfile이라는 텍스트 문서입니다. 이 문서에 "어떤 OS를 깔고, 어떤 자바를 깔고, 내 코드를 어디에 복사할지"를 적어두면, 도커 엔진이 이 문서를 읽고 '도커 이미지(Image)'라는 압축 캡슐을 만들어냅니다. 스프링 부트를 띄우기 위한 가장 스탠다드한 Dockerfile을 살펴보겠습니다.

# 1. Base Image: 이 컨테이너의 바탕이 될 운영체제와 자바 버전을 지정합니다.
# 🌟 실무 꿀팁: 무거운 우분투(Ubuntu) 대신, 초경량화된 리눅스인 alpine 버전을 써야 이미지 용량이 획기적으로 줄어듭니다!
FROM openjdk:17-jdk-alpine
# 2. 컨테이너 내부에서 우리가 작업할 폴더 위치를 지정합니다.
WORKDIR /app
# 3. 프로젝트를 빌드해서 나온 결과물(.jar)을 컨테이너 안의 /app 폴더로 복사(COPY)합니다.
# build/libs/*.jar 는 보통 gradle build를 하면 나오는 파일 경로입니다.
COPY build/libs/*-SNAPSHOT.jar app.jar
# 4. 컨테이너가 켜졌을 때 스프링 부트를 기동시키는 실행 명령어입니다.
# -Djava.security.egd 옵션은 리눅스 환경에서 톰캣 기동 속도가 느려지는 버그를 막아줍니다.
ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "app.jar"]
🧅 2. 실무 고도화: 레이어드 빌드(Layered Build) 최적화
위의 코드는 작동은 잘하지만, 실무 관점에서는 약간의 아쉬움이 있습니다. 스프링 부트의 .jar 파일은 보통 50MB~100MB 정도인데, 이 중 99%는 변하지 않는 라이브러리(Spring, Hibernate 등)이고, 개발자가 짠 순수 코드는 1MB도 안 됩니다. 그런데 소스 코드 한 줄 고칠 때마다 매번 저 100MB짜리를 통째로 도커 이미지로 다시 구워내야(Build) 하니 빌드 시간도 오래 걸리고 네트워크 트래픽도 낭비됩니다.
스프링 부트 2.3부터는 레이어드(Layered) 빌드라는 엄청난 기능을 지원합니다. .jar 파일을 압축 해제해서 [라이브러리 계층], [스프링 부트 로더 계층], [내가 짠 소스코드 계층]으로 쪼개서 도커에 담습니다. 이렇게 하면 라이브러리는 안 변했으니 도커가 캐시(Cache)를 재사용하고, 내가 수정한 1MB짜리 소스코드 계층만 휙! 하고 갈아 끼워서 빌드 속도를 수십 배 단축시킬 수 있습니다!
# 🌟 스프링 공식 문서에서 권장하는 최적화된 Multi-stage 빌드 Dockerfile
FROM openjdk:17-jdk-alpine as builder
WORKDIR /app
COPY build/libs/*.jar app.jar
# .jar 파일을 분해(layertools)하여 레이어별로 쪼갭니다.
RUN java -Djarmode=layertools -jar app.jar extract
FROM openjdk:17-jdk-alpine
WORKDIR /app
# 쪼개진 레이어들을 순서대로 복사합니다. (잘 안 변하는 라이브러리부터 복사하는 것이 캐싱의 핵심!)
COPY --from=builder /app/dependencies/ ./
COPY --from=builder /app/spring-boot-loader/ ./
COPY --from=builder /app/snapshot-dependencies/ ./
COPY --from=builder /app/application/ ./
# .jar 통짜 실행이 아니라, org.springframework.boot.loader.JarLauncher 클래스를 직접 기동시킵니다.
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
🎯 3. 마무리 및 다음 단계
지금까지 "내 로컬에선 잘 되는데 서버에선 안 돼요!"라는 변명을 영원히 종식시키고, 무거운 우분투 대신 Alpine 리눅스를 사용하며, Layered 분해를 통해 빌드 속도를 극한으로 깎아내는 완벽한 실무용 Dockerfile 튜닝 비법에 대해 아주 치밀하게 다루어 보았습니다. 이제 여러분이 만든 도커 이미지는 어떤 클라우드(AWS EC2, Kubernetes)에 던져놔도 100% 동일하게 작동합니다.
서비스를 도커로 띄워서 한 달 정도 잘 운영하고 있었는데, 어느 날 새벽에 알람이 미친 듯이 울립니다. 서버의 메모리(RAM) 사용량이 99%를 찍더니 서버가 죽어버렸습니다(OOM, OutOfMemory Error). 서버를 껐다 켜면 다시 돌아오지만, 일주일 뒤에 또 메모리가 차오르며 죽어버립니다. 코드 어딘가에서 메모리가 줄줄 새고 있는(Memory Leak) 것입니다! 눈으로 수만 줄의 코드를 읽으며 범인을 찾는 것은 불가능합니다. 이어지는 27단계 포스팅에서는 "서버가 죽기 직전 뱉어내는 최후의 유언장!" 자바 힙 덤프(Heap Dump)를 추출하고 Eclipse MAT을 활용해 메모리 누수(Memory Leak)의 범인 객체를 과학적으로 추적하는 트러블슈팅 실무에 대해 아주 뼈 때리게 파헤쳐 보겠습니다!
'Framework > Spring Boot' 카테고리의 다른 글
| [Spring Boot] JMeter 부하 테스트: 트래픽 폭탄을 쏘고 병목을 부숴라 (0) | 2026.07.27 |
|---|---|
| [Spring Boot] 힙 덤프(Heap Dump) 분석: 메모리 누수(OOM)의 범인을 체포하라 (0) | 2026.07.27 |
| [Spring Boot] 무중단 배포 전략: Nginx와 블루/그린(Blue/Green) 아키텍처 (0) | 2026.07.27 |
| [Spring Boot] API 문서화의 끝판왕: Swagger(Springdoc)와 REST Docs의 대결 (0) | 2026.07.27 |
| [Spring Boot] Apache Kafka 연동: MSA의 대동맥, 이벤트 주도 비동기 통신 (0) | 2026.07.27 |