Java를 Docker에 넣었더니 10배 느려졌습니다. 여기 해결책이 있습니다.
간과한 하나의 JVM 설정이 성능을 망칠 뻔했고, 간단한 조정으로 앱을 구했습니다.

Docker가 소프트웨어 개발에서 가장 화제가 되는 용어가 되었을 때, 저는 이를 시도해보고 싶어 하는 개발자 중 한 명이었습니다. 그 약속은 거부할 수 없었습니다: 가벼운 컨테이너, 더 빠른 배포, 환경 간 이식 가능한 앱. 마법처럼 들렸죠.
그래서 당연히 우리 팀이 Java 애플리케이션을 컨테이너화하기로 결정했을 때, 저는 완전히 동의했습니다. 우리는 배포가 더 깔끔하고, 빠르고, 확장 가능해질 것이라고 생각했습니다. 하지만 성능을 향상시키는 대신, Docker는 우리 앱을 느리게 만들었습니다. 어떤 경우에는 Docker 내부에서 실행할 때 베어메탈 서버에서 직접 실행하는 것과 비교하여 응답 시간이 10배나 악화되었습니다.
좌절스럽고, 심지어 당황스러웠습니다. Docker로 전환하라고 팀을 설득했는데 모든 데모가 끔찍하게 지연되는 것을 지켜봐야 한다고 상상해보세요. 한때는 Docker가 Java 앱에 잘못된 도구인지 의문을 제기하기도 했습니다. 하지만 진실은 Docker가 문제가 아니었습니다.
문제는 하나의 간과된 세부 사항이었습니다. 우리가 이를 수정하자 모든 것이 바뀌었습니다.
제가 겪은 이야기를 들려드리겠습니다.
고통: Docker가 Java를 비참하게 만들었을 때
우리 설정은 간단했습니다:
- Java 11 기반 마이크로서비스
- Spring Boot가 백본
- 스테이징 서버에서 원활하게 실행
우리가 이것들을 Docker 컨테이너로 감쌌을 때, "그냥 작동할" 것으로 예상했습니다. 하지만 우리가 본 것은 충격적이었습니다:
- 이전에는 200ms에 완료되던 API 호출이 이제 2-3초가 걸렸습니다.
- 트래픽이 적을 때도 CPU 사용률이 급증했습니다.
- 가비지 컬렉션이 예측 불가능해져서 무작위 지연 스파이크가 발생했습니다.
팀원들의 첫 반응은 일반적인 비난 게임이었습니다:
- "Docker는 프로덕션 준비가 안 됐어."
- "Java와 컨테이너는 잘 어울리지 않아."
- "VM을 고수했어야 했어."
하지만 마음속 깊이, 저는 이것이 Docker의 잘못이 아니라는 것을 알고 있었습니다. 뭔가 잘못 구성되었습니다.
범인 찾기
우리는 소매를 걷어붙이고 디버깅을 시작했습니다. 우리가 시도한 것들:
- 리소스 할당 확인 — 아마도 Docker가 컨테이너에 충분한 CPU나 메모리를 제공하지 않았을 것입니다. 제한을 조정했지만 큰 차이는 없었습니다.
- 베이스 이미지 전환 —
openjdk:11-jre이미지가 비대할 것이라고 생각했습니다. 슬림 버전, Alpine 이미지, 심지어 커스텀 빌드까지 시도했습니다. 여전히 느렸습니다. - 앱 프로파일링 — VisualVM과 같은 도구는 컨테이너 내부에서 가비지 컬렉션이 이상하게 동작하고 있음을 보여주었습니다. JVM이 실제보다 훨씬 더 많은 메모리와 CPU를 가지고 있다고 생각하는 것 같았습니다.
그리고 그때 깨달았습니다: Docker 내부의 JVM은 컨테이너의 리소스 제한을 자동으로 알지 못합니다.
근본 원인: JVM vs. Docker의 리소스 제한
우리가 놓친 핵심 세부 사항은 다음과 같습니다:
기본적으로 JVM은 호스트 머신의 리소스(총 CPU, 메모리)를 기반으로 자체 최적화를 시도합니다. 하지만 Docker에서는 컨테이너가 항상 실제 제한을 노출하지 않습니다.
따라서 호스트에 16GB RAM이 있지만 컨테이너가 2GB만 받는 경우, JVM은 기꺼이 모든 16GB를 사용할 수 있다고 가정할 수 있습니다. 그 불일치는 과도한 할당, 공격적인 가비지 컬렉션 및 성능 재앙으로 이어집니다.
우리의 경우, JVM은 Docker가 허용하는 것보다 더 많은 메모리와 CPU 코어를 가지고 있다고 생각했습니다. 결과는? 스래싱 가비지 컬렉션과 느린 응답 시간.
해결책: 하루를 구한 하나의 JVM 옵션
해결책은 아름답게 간단했습니다. 최신 Java 버전(JDK 10부터 시작)은 컨테이너 인식을 더 잘 처리하지만, 이전 버전이나 잘못 구성된 이미지를 사용하는 경우 JVM에 컨테이너 제한을 준수하도록 명시적으로 알려야 합니다.
마법의 옵션은 다음과 같습니다:
-XX:+UseContainerSupport
이 플래그는 JVM을 컨테이너 인식하게 만듭니다. Java가 Docker에서 설정한 실제 CPU 및 메모리 제한을 준수하도록 보장합니다.
이전 Java 버전의 경우 다음을 추가해야 할 수도 있습니다:
-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap
이를 활성화한 후 성능 차이는 밤과 낮이었습니다.
- 가비지 컬렉션이 안정화되었습니다.
- 지연이 정상 수준으로 돌아왔습니다.
- 우리 서비스는 Docker 내부에서 외부와 마찬가지로 원활하게 실행되었습니다.
보너스 최적화: 적절한 메모리 크기 조정
JVM이 컨테이너 제한을 준수하기 시작하면서 메모리 할당을 더욱 세밀하게 조정했습니다. 기본값에 맡기는 대신 명시적으로 설정했습니다:
-Xmx512m -Xms512m
이렇게 하면 JVM이 컨테이너가 처리할 수 있는 것 이상으로 자동 확장하려고 시도하지 않았습니다. Xms와 Xmx를 동일한 값으로 설정하면 힙의 불필요한 크기 조정을 줄이는 데 도움이 되었습니다.
또한 Docker가 할당한 CPU 수를 기반으로 스레드 풀을 조정했습니다. 이로써 이전에 본 끔찍한 CPU 스파이크가 제거되었습니다.
이를 통해 배운 것
이 전체 경험은 귀중한 교훈을 가르쳐주었습니다: 컨테이너는 Java 성능의 게임 규칙을 바꿉니다. 앱을 Docker화하고 마법을 기대하는 것만으로는 충분하지 않습니다. JVM과 Docker가 어떻게 상호 작용하는지 이해해야 합니다.
몇 가지 핵심 요점:
- JVM이 컨테이너 인식인지 항상 확인하세요. JDK 11+ 일반적으로 그렇지만 확인하세요.
- Docker Compose 또는 Kubernetes에서 리소스 제한을 설정하세요. 그렇지 않으면 Java는 전체 호스트를 가지고 있다고 가정합니다.
- JVM 튜닝을 잊지 마세요.
Xmx및Xms와 같은 플래그는 여전히 친구입니다. - 컨테이너에서 다르게 모니터링하세요. VisualVM, JFR 또는 Prometheus와 같은 도구는 베어메탈 설정에서 볼 수 없는 놀라움을 드러낼 수 있습니다.
마지막 생각
Docker가 우리의 Java 앱을 10배 느리게 만든 것이 아닙니다. JVM 컨테이너 인식 부족이 그렇게 했습니다.
하나의 JVM 옵션을 전환하고 메모리를 적절하게 조정하자, Docker는 항상 의도했던 게임 체인저가 되었습니다. 배포가 더 원활해지고 확장이 쉬워졌으며 컨테이너화를 더 이상 두려워하지 않게 되었습니다.
따라서 Java 앱이 Docker에서 느리게 느껴진다면 도구를 탓하지 마세요. JVM 플래그를 살펴보세요. 그 작은 세부 사항이 몇 주간의 골칫거리를 절약할 수 있습니다.
태그: #Java #Docker #JVM #Microservices #Performance #DevOps #Programming #Containers
출처: We Put Java in Docker. It Got 10x Slower. Here's the Fix. - Rajeshwari P, Medium (2025년 9월 18일)

