컨테이너 기본 자바 이미지 최적화 쿠버네티스 기본 헬스체크 배포 전략 · 12-Factor 📝 문제풀이
◀ 심화 CH 13 📋 목차 심화 CH 15 ▶
🐳 심화 CHAPTER 14 · 5년차 트랙

내 자바 앱을 컨테이너에 담아 쿠버네티스에 안전하게 배포하기

코드는 완성됐어요. 이제 "어떻게 실행 환경까지 통째로 포장해서, 무중단으로 배포하고, 스스로 회복하게 만드느냐"가 남았습니다. 컨테이너 이미지 최적화, JVM의 cgroup 인식, 쿠버네티스 헬스체크, 롤링·카나리 배포 — 운영과 시니어 면접의 핵심 주제예요.

🎯 이 장을 끝내면
📦
컨테이너 기본
"내 컴퓨터에선 되는데"를 끝내는 포장 기술
이미지 · 레이어 · 컨테이너, 그리고 VM과의 차이

컨테이너는 애플리케이션과 그 실행에 필요한 모든 것(JRE·라이브러리·설정)을 하나로 포장해요. 핵심 3개념을 구분해 봅시다.

🧱
이미지(Image)
읽기 전용 템플릿·레이어의 묶음
🗂️
레이어(Layer)
Dockerfile 명령마다 쌓이는 변경분·캐시 단위
🏃
컨테이너(Container)
이미지를 실행한 프로세스 인스턴스

이미지는 클래스, 컨테이너는 인스턴스라고 보면 편해요. 이미지는 여러 개의 레이어가 쌓인 구조이고, 각 레이어는 읽기 전용으로 공유·재사용됩니다. 컨테이너를 실행하면 그 위에 얇은 쓰기 가능 레이어가 하나 얹혀요.

🆚 컨테이너 vs 가상 머신(VM) VM — 하이퍼바이저 위에 게스트 OS 커널 전체를 각각 띄워요. 격리는 강하지만 무겁고 부팅이 느립니다.
컨테이너호스트 커널을 공유하고, 리눅스 namespace(격리)·cgroup(자원 제한)으로 프로세스 수준에서 분리해요. 그래서 가볍고 초 단위로 뜹니다. 핵심 한 줄: 컨테이너는 OS를 통째로 복제하지 않고 커널을 공유하는 격리된 프로세스예요. VM처럼 별도 커널을 부팅하지 않습니다.
💡
레지스트리(Registry). 만든 이미지는 레지스트리(Docker Hub, GitHub Container Registry, AWS ECR, Harbor 등)에 push하고, 배포 대상에서 pull해서 씁니다. 이미지는 name:tag로 식별하지만, 진짜 불변 식별자는 내용 해시인 digest(sha256:…)예요. 운영에선 latest 대신 고정 태그·digest로 재현성을 확보합니다.
🖼️ 그림으로 보기 — 가상 머신 vs 컨테이너
가상 머신 — 게스트 OS를 각자 App Bin/Libs Guest OS App Bin/Libs Guest OS App Bin/Libs Guest OS 하이퍼바이저 인프라(Hardware) 각 VM = OS 전체 → 무겁다·느린 부팅 컨테이너 — 커널 공유 App Bin/Libs App Bin/Libs App Bin/Libs 컨테이너 런타임 호스트 OS · 커널 (공유) 인프라(Hardware) 커널 공유·격리된 프로세스 → 가볍다·초 단위
🪶
자바 이미지 최적화
작고 빠르고 안전한 이미지 만들기
멀티스테이지 · distroless · 레이어 캐싱 · JVM의 컨테이너 인식

JDK 전체가 들어간 뚱뚱한 이미지는 빌드도 배포도 느리고, 공격 표면도 커요. 네 가지 무기로 다이어트합니다.

기법무엇을
멀티스테이지 빌드빌드 스테이지(JDK·Maven)에서 .jar만 만들고, 실행 스테이지(슬림 JRE)로 산출물만 복사빌드 도구·소스가 최종 이미지에 안 남아 크기·보안 개선
슬림 JRE / distrolessJDK 대신 JRE만, 혹은 셸·패키지매니저까지 뺀 distroless 베이스불필요한 바이너리 제거 → 크기↓, CVE 표면↓
레이어 캐싱의존성 레이어애플리케이션 코드 레이어를 분리코드만 바뀌면 의존성 레이어는 캐시 재사용 → 빌드·push↓
Jib / buildpacksDockerfile 없이 빌드 툴(Gradle/Maven)이나 buildpack이 이미지 자동 생성레이어 분리·재현성을 도구가 알아서 처리
# 1단계: 빌드 (JDK + Maven) — 산출물만 만들고 버려질 스테이지
FROM eclipse-temurin:21-jdk AS build
WORKDIR /app
COPY pom.xml .
RUN mvn -B dependency:go-offline      # 의존성 레이어 (코드보다 먼저 → 캐시)
COPY src ./src
RUN mvn -B clean package -DskipTests

# 2단계: 실행 (슬림 JRE) — build 스테이지에서 jar만 복사
FROM eclipse-temurin:21-jre AS runtime
WORKDIR /app
COPY --from=build /app/target/app.jar app.jar
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0"
ENTRYPOINT ["sh","-c","java $JAVA_OPTS -jar app.jar"]
⚠️
레이어 캐싱의 순서가 곧 속도. Dockerfile은 위에서부터 레이어를 쌓고, 바뀐 레이어부터 아래는 전부 다시 빌드해요. 그래서 자주 안 바뀌는 의존성을 먼저 복사·설치하고, 자주 바뀌는 소스 코드는 뒤에 둡니다. 순서가 반대면 코드 한 줄 고칠 때마다 의존성 전체를 다시 받아 캐시가 무용지물이 돼요.
🧠 JVM의 컨테이너 인식(container-aware) — 아주 중요 예전 JVM은 컨테이너에 메모리 512MB 제한을 걸어도 호스트 전체 RAM을 보고 힙을 잡아 OOMKilled로 죽곤 했어요. 지금은 JDK가 cgroup의 메모리·CPU 제한을 자동 인식(JDK 10+, 8u191+)합니다.
힙은 -Xmx로 하드코딩하기보다 -XX:MaxRAMPercentage컨테이너 메모리의 비율(예: 75%)을 지정하는 게 유연해요. 컨테이너 limit을 바꿔도 힙이 자동으로 따라 조정되니까요. 면접 포인트: "왜 컨테이너에서 -Xmx를 고정하면 위험한가?" → limit을 바꿔도 힙이 안 따라오거나, 힙+메타스페이스+스택+네이티브가 limit을 넘겨 OOMKill될 수 있어요. 비율 기반이 안전합니다.
🧭
CPU도 인식해요. Runtime.availableProcessors()는 cgroup의 CPU 할당량을 반영해 값을 돌려줘요. 이 값이 ForkJoinPool 공통 풀·GC 스레드 수·커넥션 풀 기본값에 영향을 주니, CPU limit을 지나치게 낮게 잡으면 병렬성이 함께 줄어듭니다.
🖼️ 그림으로 보기 — 이미지 레이어 쌓기
이미지 = 레이어의 쌓임 (아래=베이스, 위=앱) 쓰기 가능 레이어(컨테이너) 앱 JAR 레이어 의존성 레이어 JRE 레이어 베이스 이미지(OS) 아래 레이어(베이스·JRE·의존성)는 잘 안 변함 → 캐시 재사용·공유 앱 JAR: 자주 변함 → 이 레이어만 다시 빌드·push 실행 순간: 얇은 '쓰기' 레이어가 이미지 위에 얹힘 = 컨테이너
☸️
쿠버네티스 기본
컨테이너를 "선언"하면 알아서 굴러가게
Pod · Deployment · Service · ConfigMap/Secret · HPA

쿠버네티스(K8s)의 철학은 선언적(declarative)이에요. "이런 상태였으면 좋겠다"를 YAML로 선언하면, 컨트롤러가 현재 상태를 목표 상태로 계속 수렴시켜요(reconciliation loop). 핵심 오브젝트를 봅시다.

오브젝트역할핵심
Pod컨테이너를 담는 최소 배포 단위(보통 1컨테이너). 고유 IP를 가짐Pod는 일회용 — 죽으면 새 IP로 다시 태어남
DeploymentReplicaSet을 통해 원하는 복제본 수를 유지하고 롤링 업데이트/롤백 관리선언한 replica 수를 스스로 맞춤(self-healing)
Service수시로 바뀌는 Pod 앞에 고정 가상 IP·DNS 이름을 주고 부하 분산Pod IP가 변해도 안정된 접점 제공
ConfigMap설정값(환경변수·프로퍼티)을 이미지 밖으로 분리같은 이미지를 dev/prod에 재사용
Secret비밀번호·토큰 등 민감 정보 보관(base64 인코딩·접근 제어)코드·이미지에 비밀을 넣지 않기
HPACPU·메모리·커스텀 지표에 따라 Pod 수를 자동 증감(오토스케일)부하에 맞춰 replica 자동 조절
💡
Deployment → ReplicaSet → Pod 계층. 우리가 다루는 건 보통 Deployment예요. Deployment가 새 버전을 배포하면 새 ReplicaSet을 만들고, 옛 ReplicaSet의 Pod를 줄이면서 새 ReplicaSet의 Pod를 늘리는 식으로 롤링 업데이트를 수행합니다. 롤백은 이전 ReplicaSet으로 되돌리는 것이라 빨라요.
Pod Deployment Service ReplicaSet ConfigMap Secret HPA Ingress
🩺
헬스체크
"살아 있냐"와 "받을 준비 됐냐"는 다르다
liveness vs readiness vs startup 프로브

쿠버네티스는 세 종류의 프로브(probe)로 Pod 상태를 확인해요. 이 셋을 헷갈리면 불필요한 재시작이나 준비 안 된 Pod로의 트래픽 유입 같은 사고가 납니다.

프로브질문실패하면
liveness"이 컨테이너 살아 있나?(데드락·행 안 걸렸나)"컨테이너를 재시작
readiness"지금 트래픽 받을 준비 됐나?(DB·캐시 워밍업 끝났나)"재시작 안 함. Service 엔드포인트에서 제외(트래픽 차단)
startup"기동이 끝났나?(느린 부팅 앱 보호)"성공 전까지 liveness/readiness를 유예. 실패 지속 시 재시작
🎯 liveness와 readiness를 왜 나눌까 liveness 실패는 "복구 불가능하니 재시작하라"는 뜻이에요. 여기에 DB 연결 여부 같은 일시적 의존성을 넣으면, DB가 잠깐 흔들릴 때 멀쩡한 앱이 무한 재시작 루프에 빠져요.
readiness 실패는 "죽인 건 아니고 지금은 트래픽 빼라"는 뜻이에요. DB가 회복되면 다시 트래픽을 받습니다. 규칙: 재시작으로 고쳐질 문제만 liveness에, 일시적으로 못 받는 상태는 readiness에 담으세요.
🧭
스프링 부트 액추에이터 연동. Spring Boot Actuator는 /actuator/health/liveness·/actuator/health/readiness 엔드포인트를 제공해요(Kubernetes 환경에서 자동 활성화, 또는 management.endpoint.health.probes.enabled=true). K8s 프로브의 httpGet.path를 여기에 맞추면, 애플리케이션의 내부 상태(DB·메시지큐 등)를 반영한 정확한 헬스체크가 됩니다.
🖼️ 그림으로 보기 — liveness vs readiness
🏃 Pod / 컨테이너 🫀 liveness — 살아있나? 행·데드락 감지 ❌ 실패 → 컨테이너 재시작 🚦 readiness — 준비됐나? DB·워밍업 완료? ❌ 실패 → 트래픽만 차단 (재시작 안 함) 재시작으로 고쳐질 것만 liveness · 일시적 불가는 readiness
🚀
배포 전략 · 12-Factor
무중단으로, 위험은 잘게 나눠서
롤링 · 블루/그린 · 카나리 · 12-Factor · CI/CD
전략방식특징
롤링(Rolling)기존 Pod를 조금씩 새 버전으로 교체(K8s Deployment 기본)추가 자원 적음. 배포 중 두 버전 공존, 롤백은 다소 느림
블루/그린새 버전(그린)을 통째로 띄우고, 준비되면 트래픽을 한 번에 스위치즉시 전환·즉시 롤백. 하지만 2배 자원 필요
카나리(Canary)새 버전에 소수 트래픽(예: 5%)만 먼저 흘려 지표 확인 후 점진 확대실제 트래픽으로 위험을 잘게 검증. 문제 시 영향 최소
💡
카나리의 핵심은 "관찰". 소수 트래픽을 흘리는 것 자체가 목적이 아니라, 그 5%에서 에러율·지연·비즈니스 지표를 보고 확대할지 롤백할지 판단하는 게 본질이에요. 그래서 카나리는 관찰성(메트릭·트레이싱)과 짝을 이룹니다(다음 장 주제).
📜 12-Factor App — 컨테이너 배포의 헌법 ③ 설정(Config) — 환경마다 달라지는 값(DB URL·키)은 코드가 아니라 환경변수로. 같은 이미지를 dev/prod에 재사용.
⑥ 프로세스(Processes) — 앱은 무상태(stateless). 세션·파일을 로컬에 두지 말고 외부 저장소(Redis·DB·S3)에. 그래야 아무 Pod로 스케일·교체 가능.
⑪ 로그(Logs) — 로그 파일을 직접 관리하지 말고 표준출력(stdout) 스트림으로 흘려보내면 플랫폼이 수집. 왜 중요? 컨테이너는 언제든 죽고 새로 뜨는 일회용이에요. 상태·설정·로그를 컨테이너 바깥에 두어야 자유롭게 스케일·재배포·이동할 수 있습니다.
🧭
CI/CD 파이프라인. CI(지속적 통합)는 push마다 빌드·테스트·이미지 생성·레지스트리 push를 자동화하고, CD(지속적 배포/전달)는 그 이미지를 K8s에 배포해요. GitOps(예: Argo CD)는 Git의 매니페스트를 단일 진실로 삼아, 클러스터 상태를 Git과 계속 동기화합니다. 배포가 버튼 한 번·되돌리기 한 번이 되는 게 목표예요.
🧠 이 장 핵심 요약
📝
문제풀이 · 점검
시니어 관점으로 점검
면접·운영에서 실제로 묻는 결의 문제예요. 틀려도 바로 해설이 나옵니다.
🧪
개념을 "안다"와 "설명할 수 있다"는 달라요. 네 유형(객관식 · O/X · 빈칸 · 코드결과)으로 점검해보세요.
심화 CH 15 🔭 관찰성과 트러블슈팅 — 메트릭·로그·트레이싱, 스레드 덤프, 실전 장애 대응 (제작 예정)