Spring Boot 3 핵심 정리 시리즈 15편. Docker로 애플리케이션을 표준 컨테이너에 담아 어디든 똑같이 옮기는 법, Compose로 여러 컨테이너를 한 번에 관리하는 법, 그리고 Kubernetes로 컨테이너 항만을 자동 운영하는 법까지 — Spring Boot Maven 플러그인의 자동 이미지 빌드, Actuator와 K8s Probe 연동까지 짐을 표준 컨테이너에 담는 비유로 친절하게 풀어쓴 15편.
이 글은 Spring Boot 3 핵심 정리 시리즈의 15편입니다. 14편까지 따라오셨다면 이미 캐싱과 이벤트로 단일 애플리케이션의 성능과 구조를 다듬는 단계예요. 이번 15편에서는 그 애플리케이션을 어떻게 다른 환경으로 옮기고, 여러 인스턴스로 띄우고, 자동으로 운영할 것인가 — 즉 배포의 영역을 다룹니다.
핵심 도구는 세 가지 — Docker, Docker Compose, Kubernetes(K8s) 입니다. 이 셋의 관계는 단순해요. Docker는 애플리케이션 한 개를 표준 컨테이너에 담는 도구, Compose는 컨테이너 여러 개를 한 번에 묶는 도구, Kubernetes는 그 컨테이너들을 여러 서버에 걸쳐 자동으로 배포·확장·복구하는 항만 관리자입니다.
왜 컨테이너 배포가 처음엔 어렵게 느껴질까요
이유는 세 가지예요.
첫째, "내 로컬에서는 잘 되는데 서버에서는 안 된다" 는 고질병이 사라지지 않습니다. OS 버전, JVM 버전, 환경 변수, 포트 충돌 — 어디서 무엇이 다른지 일일이 추적하기 어려워요. 컨테이너는 이 문제 자체를 사라지게 하는 도구인데, 처음엔 "왜 이렇게까지 해야 하나" 싶을 수 있습니다.
둘째, 이미지(Image)와 컨테이너(Container)의 차이가 헷갈려요. "이미지를 다운받고 컨테이너로 실행한다"는 문장이 처음엔 잘 안 들어옵니다. 이름도 비슷비슷하고, 둘 다 "그 안에 애플리케이션이 들어 있다"는 식으로 설명되니까요.
셋째, Kubernetes는 추상도가 한 단계 더 올라가요. Pod·Deployment·Service·Ingress·ConfigMap·Secret — 명사가 너무 많고, 이게 왜 다 따로 있어야 하는지가 안 보입니다. 한 번에 다 보면 어지러워요.
해결법은 한 가지 비유예요. 컨테이너는 "표준 화물 컨테이너", Kubernetes는 "컨테이너 항만 관리자" 입니다. 화물선 시대에 컨테이너 표준이 만들어지면서 어느 항구·어느 트럭·어느 기차에서도 똑같이 옮길 수 있게 된 그 혁명을 소프트웨어 배포에 그대로 적용한 거예요. 이 비유 한 가지만 잡고 가면 흐름이 자연스럽게 따라옵니다.
Docker — 표준 컨테이너 비유
회사에서 거대한 짐을 외국으로 보낸다고 해 봅시다. 짐의 크기와 모양이 다 제각각이라면 트럭·배·기차마다 매번 다시 포장해야 하고, 받는 쪽에서도 매번 다른 방식으로 푸는 절차를 거쳐야 해요. 그런데 모든 짐을 표준 화물 컨테이너에 담아 보내면, 운반 수단이 무엇이든 컨테이너 단위로 그대로 옮길 수 있습니다.
Docker가 정확히 그 일을 합니다. 애플리케이션과 그 실행 환경(OS 라이브러리, JVM, 설정 파일) 전부를 하나의 컨테이너 이미지로 묶어서, 어떤 호스트에서도 동일하게 실행되도록 보장해요. "내 로컬에서는 잘 되는데 서버에서는 안 된다"는 문제가 근본적으로 사라집니다.
현대 클라우드 네이티브 환경에서 Docker는 사실상의 표준(de facto standard) 이 됐어요. 그리고 Spring Boot는 한 발 더 나아가, Maven/Gradle 플러그인으로 Dockerfile 없이도 최적화된 Docker 이미지를 자동 생성하는 기능을 내장하고 있습니다.
이미지 vs 컨테이너 — 설계도와 그 설계도로 지은 집
Docker를 처음 배울 때 가장 많이 헷갈리는 게 이미지(Image)와 컨테이너(Container)의 차이예요. 두 단어를 단순한 비유로 풀어 봅시다.
- 이미지 = 설계도(blueprint) — 컨테이너를 만들기 위한 읽기 전용 템플릿이에요. 애플리케이션 코드, 런타임, 라이브러리, 설정 파일이 모두 포함된 불변의 스냅샷입니다.
- 컨테이너 = 그 설계도로 지은 집 — 이미지를 기반으로 실제로 실행되는 인스턴스예요. 격리된 환경에서 동작하며, 호스트 커널을 공유하니 가상 머신보다 훨씬 가볍고 빠르게 시작됩니다.
같은 설계도(이미지)로 여러 채의 집(컨테이너)을 동시에 지을 수 있어요. 이게 컨테이너의 강력함입니다.
이미지 (Image)
├── Layer 1: Base OS (Ubuntu/Alpine)
├── Layer 2: JRE 17
├── Layer 3: Application JAR
└── Layer 4: 설정 파일
컨테이너 (Container) = 이미지 + 실행 레이어(쓰기 가능)
여기서 시험 함정이 하나 있어요. 컨테이너 안에서 만든 데이터는 컨테이너가 삭제되면 사라집니다. 영속화하고 싶다면 Docker Volume이나 외부 저장소(데이터베이스 컨테이너 등)를 써야 해요. 이걸 모르고 컨테이너 안에 SQLite 파일을 두면 재시작 한 번에 데이터가 통째로 날아갑니다.
Docker 핵심 용어 정리
| 용어 | 역할 |
|---|---|
| Docker Engine | 컨테이너를 실행하는 런타임 |
| Docker Hub | 공개 이미지 저장소 (docker.io) |
| Docker Registry | 이미지를 저장·배포하는 저장소 (공개·사설) |
| Docker Network | 컨테이너 간 통신을 위한 가상 네트워크 |
| Docker Volume | 컨테이너 외부에 데이터를 영속 저장하는 공간 |
이 다섯 가지만 머리에 들어가면 Docker 명령어를 따라가는 데 큰 지장이 없습니다.
Spring Boot Docker 이미지 빌드 — Maven 플러그인이 정답
Spring Boot 2.3 이후로는 Dockerfile을 직접 작성하지 않아도 Maven/Gradle 플러그인으로 최적화된 OCI 이미지를 자동 생성할 수 있어요. 내부적으로 Cloud Native Buildpacks를 사용하니, 보안 패치도 자동으로 받아갑니다.
<!-- pom.xml — spring-boot-maven-plugin 설정 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<!-- 이미지 이름 커스터마이징 -->
<name>myrepo/${project.artifactId}:${project.version}</name>
<!-- 빌드팩으로 생성되는 환경 변수 설정 -->
<env>
<BP_JVM_VERSION>17</BP_JVM_VERSION>
</env>
</image>
<!-- Docker Hub 등에 바로 푸시할 경우 -->
<docker>
<publishRegistry>
<username>${docker.username}</username>
<password>${docker.password}</password>
<url>https://index.docker.io/v1/</url>
</publishRegistry>
</docker>
</configuration>
</plugin>
# 이미지 빌드 명령
./mvnw clean package spring-boot:build-image
# 빌드하면서 바로 Docker Hub에 푸시
./mvnw clean package spring-boot:build-image -Dspring-boot.build-image.publish=true
# Gradle의 경우
./gradlew bootBuildImage
여기서 정말 중요한 시험 함정 — Buildpack은 멀티 플랫폼 이미지 생성에 강점이 있어 Apple Silicon(M1/M2) Mac에서도 자연스럽게 호환됩니다. Dockerfile 직접 작성 방식보다 훨씬 적은 노력으로 안전한 이미지를 얻을 수 있어요.
전통적인 Dockerfile 방식
빌드팩보다 세밀한 제어가 필요할 때 사용해요. 멀티 스테이지 빌드와 레이어 캐싱을 활용하면 빌드 시간과 이미지 크기를 모두 줄일 수 있습니다.
# 멀티 스테이지 빌드 — 빌드 환경과 실행 환경 분리
FROM eclipse-temurin:17-jdk-alpine AS builder
WORKDIR /app
COPY .mvn/ .mvn
COPY mvnw pom.xml ./
RUN ./mvnw dependency:go-offline # 의존성 사전 다운로드 (레이어 캐싱 활용)
COPY src ./src
RUN ./mvnw clean package -DskipTests
# 실행 이미지 — JRE만 포함하여 이미지 크기 최소화
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
# 보안 — root가 아닌 별도 사용자로 실행
RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring
# Spring Boot 레이어드 JAR 활용 (레이어별 복사로 캐싱 최적화)
COPY --from=builder /app/target/extracted/dependencies/ ./
COPY --from=builder /app/target/extracted/spring-boot-loader/ ./
COPY --from=builder /app/target/extracted/snapshot-dependencies/ ./
COPY --from=builder /app/target/extracted/application/ ./
# 헬스체크 설정
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD wget -q -O /dev/null http://localhost:8080/actuator/health || exit 1
EXPOSE 8080
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
레이어드 JAR 추출은 Spring Boot 2.3+에서 제공하는 기능이에요.
# 레이어 목록 확인
java -Djarmode=layertools -jar target/myapp.jar list
# 레이어 추출 (Dockerfile에서 사용)
java -Djarmode=layertools -jar target/myapp.jar extract --destination target/extracted
빌드 방식 비교표
| 항목 | Buildpack (Maven Plugin) | Dockerfile |
|---|---|---|
| Dockerfile 필요 | 불필요 | 필요 |
| 이미지 최적화 | 자동 (레이어 캐싱) | 수동 설정 필요 |
| 보안 패치 | 자동 (base image 업데이트) | 수동 관리 |
| 커스터마이징 | 제한적 | 완전한 제어 |
| 빌드 속도 | 비교적 느림 | 빠름 (캐시 활용 시) |
| 권장 환경 | 일반적인 Spring Boot 앱 | 복잡한 커스텀 요구사항 |
판단 기준 — 특별한 이유가 없다면 Buildpack, 베이스 이미지 변경이나 OS 패키지 추가가 필요하면 Dockerfile.
Docker 핵심 명령어 — 일상 운영의 80%
여기서는 일상에서 가장 많이 쓰는 명령어만 모았어요. 이 정도만 알면 컨테이너 운영의 80% 이상을 커버합니다.
이미지 관리
# 이미지 목록 조회
docker images
# 이미지 상세 정보 (메타데이터, 레이어, 아키텍처 등)
docker image inspect myapp:0.0.1-SNAPSHOT
# 이미지 다운로드
docker pull mysql:8.0
# 이미지 삭제
docker rmi myapp:0.0.1-SNAPSHOT
# 사용하지 않는 이미지 일괄 삭제
docker image prune -a
컨테이너 실행
# 주요 옵션과 함께 실행
docker run \
-d \ # 백그라운드(detached) 실행
--name gateway \ # 컨테이너 이름 지정
-p 8080:8080 \ # 호스트포트:컨테이너포트 매핑
-e SPRING_PROFILES_ACTIVE=prod \ # 환경 변수 설정
-v /host/data:/container/data \ # 볼륨 마운트
--network my-network \ # 특정 네트워크에 연결
myapp:0.0.1-SNAPSHOT
# Apple Silicon (M1/M2) Mac에서 플랫폼 지정
docker run --platform linux/amd64 \
-d --name gateway -p 8080:8080 \
myapp:0.0.1-SNAPSHOT
# 실행 중인 컨테이너 목록
docker ps
# 모든 컨테이너 (중지된 것 포함)
docker ps -a
# 컨테이너 중지·시작·재시작
docker stop gateway
docker start gateway
docker restart gateway
# 컨테이너 삭제
docker stop gateway && docker rm gateway
# 실시간 로그 (-f: follow)
docker logs -f gateway
# 실행 중인 컨테이너에 셸 접속
docker exec -it gateway sh
# 컨테이너 리소스 사용량 모니터링
docker stats
여기서 시험 함정이 하나 있어요. 컨테이너에서 다른 컨테이너에 접근할 때 localhost는 자기 자신을 가리킵니다. 같은 Docker 네트워크에 속한 다른 컨테이너에 접근하려면 컨테이너 이름(또는 서비스 이름) 으로 접속해야 해요.
# 잘못된 설정 — 컨테이너에서 localhost는 자기 자신
SPRING_DATASOURCE_URL: jdbc:mysql://localhost:3306/restdb
# 올바른 설정 — 같은 네트워크에서 서비스 이름으로 접근
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/restdb
이 함정은 Compose 환경에서 더 자주 등장합니다.
Docker Compose — 여러 컨테이너를 한 번에
docker run 명령어가 길어지면 관리가 어려워져요. 데이터베이스·인증 서버·게이트웨이·앱 컨테이너를 매번 따로 띄우고 네트워크를 연결하는 일도 번거롭습니다.
Docker Compose는 여러 컨테이너로 구성된 애플리케이션을 단일 YAML 파일(compose.yaml 또는 docker-compose.yml) 로 정의하고 관리하는 도구예요. 복잡한 docker run 명령어 대신 선언적 방식으로 전체 스택을 관리할 수 있습니다.
> 참고로 docker-compose (v1, Python 기반)는 구버전이고, 현재 표준은 Docker Desktop에 내장된 docker compose (v2, Go 기반)입니다. 명령어가 한 단어 차이(하이픈 vs 공백)예요.
compose.yaml 기본 구조
# compose.yaml
services:
# MySQL 데이터베이스 서비스
mysql:
image: mysql:8.0
container_name: mysql_db
platform: linux/amd64 # Apple Silicon Mac에서 필요
environment:
MYSQL_ROOT_PASSWORD: root_password
MYSQL_DATABASE: restdb
MYSQL_USER: restadmin
MYSQL_PASSWORD: password
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql # 데이터 영속화
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
# Spring Boot REST MVC 서비스
rest-mvc:
image: myapp-mvc:0.0.1-SNAPSHOT
container_name: rest_mvc
platform: linux/amd64
depends_on:
mysql:
condition: service_healthy # MySQL 헬스체크 통과 후 시작
ports:
- "8081:8081"
environment:
# 12-Factor App 원칙에 따라 환경 변수로 설정 주입
- SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/restdb
- SPRING_DATASOURCE_USERNAME=restadmin
- SPRING_DATASOURCE_PASSWORD=password
- SPRING_PROFILES_ACTIVE=localdocker
# Spring Authorization Server
auth-server:
image: myapp-auth-server:0.0.1-SNAPSHOT
container_name: auth_server
ports:
- "9000:9000"
# Spring Cloud Gateway
gateway:
image: myapp-gateway:0.0.1-SNAPSHOT
container_name: gateway
depends_on:
- rest-mvc
- auth-server
ports:
- "8080:8080" # 외부에 노출되는 단일 진입점
volumes:
mysql_data: # 명명된 볼륨 — 컨테이너 삭제 후에도 데이터 유지
여기서 시험 함정이 하나 있어요. depends_on만으로는 서비스 준비 상태를 보장할 수 없습니다. depends_on은 컨테이너 시작 순서만 보장해요. 데이터베이스가 시작은 됐지만 아직 연결을 받을 준비가 안 됐을 수 있어요.
# 잘못된 예 — MySQL이 시작되긴 했지만 준비 안 됐을 수 있음
depends_on:
- mysql
# 올바른 예 — healthcheck와 함께 사용
depends_on:
mysql:
condition: service_healthy # 헬스체크 통과 후 시작
condition: service_healthy를 함께 써야 진짜 안전합니다.
Compose 핵심 명령어
# 전체 스택 시작 (백그라운드)
docker compose up -d
# 특정 서비스만 시작
docker compose up -d mysql
# 서비스 중지 (컨테이너는 유지)
docker compose stop
# 서비스 중지 + 컨테이너 제거 (볼륨은 유지)
docker compose down
# 서비스 중지 + 컨테이너 + 볼륨 모두 제거
docker compose down -v
# 서비스 상태 확인
docker compose ps
# 로그 확인 (모든 서비스)
docker compose logs -f
# 서비스 스케일 조정 (특정 서비스를 n개로 실행)
docker compose up -d --scale rest-mvc=3
docker compose stop과 docker compose down의 차이가 시험에 자주 나와요. stop은 중지만(재시작 가능), down은 중지 + 삭제입니다.
환경 변수 분리 — 12-Factor App
12-Factor App 원칙에 따라 설정은 코드와 분리해야 해요. 환경 변수를 통해 같은 이미지를 여러 환경(dev·staging·prod)에 배포할 수 있습니다. 이 원칙은 16편에서 마이크로서비스 맥락에서 다시 자세히 풀어 갑니다.
# .env 파일 (Git에 커밋하지 않을 것)
MYSQL_ROOT_PASSWORD=my_secret_password
MYSQL_DATABASE=restdb
SPRING_PROFILES_ACTIVE=prod
# compose.yaml에서 .env 파일 참조
services:
rest-mvc:
env_file:
- .env
environment:
SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE:-dev} # 기본값: dev
# application.properties에서 환경 변수 읽기
spring.datasource.url=${SPRING_DATASOURCE_URL:jdbc:mysql://localhost:3306/restdb}
spring.datasource.username=${SPRING_DATASOURCE_USERNAME:restadmin}
spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:password}
Kubernetes — 컨테이너 항만 관리자
Docker Compose는 단일 호스트(서버 한 대)를 위한 도구예요. 그런데 프로덕션에서는 서버 여러 대에 걸쳐 수십 개·수백 개의 컨테이너를 운영해야 합니다. 한 대가 다운되면 자동으로 다른 대로 옮겨야 하고, 트래픽이 몰리면 자동으로 인스턴스를 늘려야 하고, 새 버전을 배포할 때 무중단으로 전환해야 해요.
Kubernetes(K8s) 가 그 일을 합니다. 회사 비유로 풀면 — "컨테이너 항만 관리자" 예요. 화물 컨테이너가 어느 부두에 내려앉을지, 어디로 옮길지, 빈 부두를 어떻게 채울지, 망가진 컨테이너를 어떻게 교체할지 — 이 모든 걸 자동으로 처리합니다. 구글이 내부 시스템(Borg)을 기반으로 개발했고, 현재 CNCF(Cloud Native Computing Foundation)에서 관리해요.
자세한 사양과 튜토리얼은 Kubernetes 공식 문서에서 확인할 수 있습니다.
Kubernetes 핵심 구성
Cluster
├── Control Plane (마스터 노드)
│ ├── API Server: kubectl 명령어를 받는 진입점
│ ├── etcd: 클러스터 상태를 저장하는 분산 키-값 저장소
│ ├── Scheduler: Pod를 적절한 노드에 배치
│ └── Controller Manager: 원하는 상태 유지 감시
│
└── Worker Nodes (워커 노드)
├── kubelet: 노드에서 컨테이너 실행 에이전트
├── kube-proxy: 네트워크 라우팅
└── Container Runtime: Docker, containerd 등
용어가 많지만 실무에서 매일 만지는 건 워커 노드 위에 떠 있는 오브젝트(Object) 들이에요. 명사 4개만 잡고 가면 80%가 끝납니다 — Pod, Deployment, Service, ConfigMap/Secret.
K8s 핵심 오브젝트 4종 — 명사 4개로 이해하기
Pod — 최소 배포 단위
Pod은 하나 이상의 컨테이너를 감싸는 K8s의 최소 실행 단위예요. 같은 Pod의 컨테이너들은 네트워크와 스토리지를 공유합니다. 회사 비유로 — "한 사무실에 함께 들어간 동료들" 같은 개념이에요. 같은 사무실이니 책상은 따로지만 공기·전기·인터넷은 함께 씁니다.
직접 Pod을 만들기보다는 보통 Deployment를 통해 관리해요. Pod 직접 생성 예시는 학습용입니다.
# Pod 직접 생성 (학습 목적)
apiVersion: v1
kind: Pod
metadata:
name: spring-app-pod
labels:
app: spring-app
spec:
containers:
- name: spring-app
image: myrepo/myapp-mvc:0.0.1-SNAPSHOT
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: "kubernetes"
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
여기서 시험 함정이 하나 있어요. Resource Limit을 설정하지 않으면 하나의 Pod이 노드 전체 리소스를 소비할 수 있습니다. 한 컨테이너가 메모리 누수를 일으키면 같은 노드의 다른 Pod까지 모두 영향을 받아요. 항상 requests와 limits를 함께 적어 주세요.
Deployment — Pod 복제와 롤링 업데이트
Deployment는 원하는 개수의 Pod 복제본(replica)을 유지하고, 무중단 롤링 업데이트를 지원합니다. 프로덕션에서 가장 많이 쓰는 오브젝트예요.
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-mvc-deployment
labels:
app: spring-mvc
spec:
replicas: 3 # 3개의 Pod 복제본 유지
selector:
matchLabels:
app: spring-mvc
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1 # 업데이트 중 최대 1개 Pod 중단 허용
maxSurge: 1 # 업데이트 중 최대 1개 추가 Pod 허용
template:
metadata:
labels:
app: spring-mvc
spec:
containers:
- name: spring-mvc
image: myrepo/myapp-mvc:0.0.1-SNAPSHOT
ports:
- containerPort: 8081
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8081
initialDelaySeconds: 30
periodSeconds: 10
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8081
initialDelaySeconds: 60
periodSeconds: 30
env:
- name: SPRING_DATASOURCE_URL
valueFrom:
secretKeyRef:
name: db-secret
key: datasource-url
replicas: 3은 "이 앱을 항상 3개 인스턴스로 유지해 주세요"라는 선언이에요. 한 개가 다운되면 K8s가 자동으로 새 Pod을 띄워 줍니다.
Service — Pod에 안정적인 접근
Pod의 IP는 재시작될 때마다 바뀌어요. Service는 그 위에 고정된 엔드포인트를 제공하고, 여러 Pod 간 로드밸런싱도 자동으로 수행합니다.
Service 타입은 세 가지가 있어요. 각자의 자리가 다릅니다.
| 타입 | 접근 범위 | 사용 사례 |
|---|---|---|
| ClusterIP | 클러스터 내부만 | 서비스 간 내부 통신 (기본값) |
| NodePort | 노드 IP + 지정 포트 | 개발/테스트 외부 접근 |
| LoadBalancer | 외부 로드밸런서 | 프로덕션 외부 노출 |
| ExternalName | 외부 DNS 이름 매핑 | 외부 서비스 추상화 |
# ClusterIP — 클러스터 내부 통신용 (기본값)
apiVersion: v1
kind: Service
metadata:
name: spring-mvc-service
spec:
selector:
app: spring-mvc
ports:
- protocol: TCP
port: 8081 # Service가 노출하는 포트
targetPort: 8081 # Pod 내 컨테이너 포트
type: ClusterIP
---
# LoadBalancer — 클라우드 로드밸런서 생성 (프로덕션)
apiVersion: v1
kind: Service
metadata:
name: gateway-loadbalancer
spec:
selector:
app: gateway
ports:
- port: 80
targetPort: 8080
type: LoadBalancer
ConfigMap & Secret — 설정 관리
12-Factor App 원칙에 따라 설정은 코드와 분리해야 한다고 했죠. K8s에서 그 자리는 ConfigMap(일반 설정) 과 Secret(민감 데이터) 입니다.
# ConfigMap — 일반 설정 데이터
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
SPRING_PROFILES_ACTIVE: "kubernetes"
LOG_LEVEL: "INFO"
---
# Secret — 민감한 데이터 (Base64 인코딩)
apiVersion: v1
kind: Secret
metadata:
name: db-secret
type: Opaque
data:
datasource-url: amRiYzpteXNxbDovL215c3FsOjMzMDYvcmVzdGRi
username: cmVzdGFkbWlu
password: cGFzc3dvcmQ=
# Secret 생성 (명령줄 — Base64 자동 처리)
kubectl create secret generic db-secret \
--from-literal=datasource-url='jdbc:mysql://mysql:3306/restdb' \
--from-literal=username='restadmin' \
--from-literal=password='password'
여기서 정말 중요한 시험 함정 — 비밀번호를 ConfigMap에 저장하지 마세요. ConfigMap은 암호화되지 않습니다. 비밀번호·토큰·API 키 같은 민감한 데이터는 반드시 Secret에 넣어야 해요.
Ingress — 외부 HTTP/HTTPS 라우팅
Service만으로는 외부 도메인 라우팅이 부족해요. Ingress는 외부 HTTP/HTTPS 트래픽을 클러스터 내부 Service로 라우팅하는 게이트웨이 역할을 합니다.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: api.myapp.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: gateway-service
port:
number: 8080
kubectl 핵심 명령어 — 일상 운영의 80%
# 클러스터 정보
kubectl cluster-info
kubectl get nodes
# 리소스 조회
kubectl get pods
kubectl get pods -o wide # IP, 노드 등 상세 정보
kubectl get deployments
kubectl get services
kubectl get all # 모든 리소스 조회
# 리소스 상세 정보
kubectl describe pod spring-mvc-abc123
kubectl describe deployment spring-mvc-deployment
# 리소스 생성/적용 — 선언적 방식 (권장)
kubectl apply -f deployment.yaml
kubectl apply -f . # 현재 디렉터리의 모든 yaml 적용
# 리소스 삭제
kubectl delete -f deployment.yaml
# Pod 로그
kubectl logs spring-mvc-abc123
kubectl logs -f spring-mvc-abc123 # 실시간
kubectl logs spring-mvc-abc123 --previous # 이전 컨테이너
# Pod 셸 접속
kubectl exec -it spring-mvc-abc123 -- sh
# 포트 포워딩 (로컬 개발/디버깅)
kubectl port-forward service/spring-mvc-service 8081:8081
# Deployment 스케일링
kubectl scale deployment spring-mvc-deployment --replicas=5
# 롤링 업데이트 이미지 변경
kubectl set image deployment/spring-mvc-deployment spring-mvc=myrepo/myapp-mvc:0.0.2
# 롤아웃 상태 확인
kubectl rollout status deployment/spring-mvc-deployment
# 이전 버전으로 롤백
kubectl rollout undo deployment/spring-mvc-deployment
여기서 시험 함정이 하나 있어요. 명령형(kubectl create)보다 선언형(kubectl apply -f)을 권장합니다. 선언형은 매니페스트 파일에 "원하는 상태"를 적어 두고 K8s가 그 상태를 유지하도록 하는 방식이에요. Git으로 인프라 변경 이력을 추적할 수 있고, 같은 명령을 여러 번 실행해도 결과가 같은 멱등성을 가집니다.
Spring Boot Actuator + K8s Probe 연동 — 환상의 짝궁
12편에서 다룬 Spring Boot Actuator는 K8s의 헬스체크 Probe와 그대로 맞물립니다. 별다른 설정 없이도 자연스럽게 통합돼요.
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
# application.properties
management.endpoint.health.probes.enabled=true
management.health.livenessState.enabled=true
management.health.readinessState.enabled=true
# Actuator 엔드포인트 노출
management.endpoints.web.exposure.include=health,info,metrics
# Kubernetes Deployment에서 Probe 설정
containers:
- name: spring-app
livenessProbe:
httpGet:
path: /actuator/health/liveness # Spring Boot 제공
port: 8080
initialDelaySeconds: 60 # JVM 시작 시간 고려
periodSeconds: 30
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health/readiness # Spring Boot 제공
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
liveness와 readiness의 차이가 시험에 자주 나와요.
- liveness = "이 Pod이 살아 있나" — 실패하면 Pod 재시작
- readiness = "이 Pod이 트래픽을 받을 준비가 됐나" — 실패하면 Service에서 트래픽 차단
JVM 시작 시간이 비교적 긴 편이라 initialDelaySeconds는 liveness 60초, readiness 30초 정도로 넉넉히 잡는 게 안전합니다.
Docker vs Compose vs Kubernetes 비교
| 항목 | Docker | Docker Compose | Kubernetes |
|---|---|---|---|
| 관리 범위 | 단일 컨테이너 | 단일 호스트의 다중 컨테이너 | 다중 호스트의 다중 컨테이너 |
| 사용 사례 | 개발, 빌드 | 로컬 개발, 소규모 배포 | 프로덕션, 대규모 배포 |
| 자동 스케일링 | 없음 | 수동 (--scale) | 자동 (HPA) |
| 자동 복구 | 없음 | 없음 | 있음 |
| 로드밸런싱 | 없음 | 없음 (외부 도구 필요) | 내장 |
| 학습 곡선 | 낮음 | 낮음~중간 | 높음 |
| 설정 파일 | Dockerfile | compose.yaml | yaml (다수) |
판단 기준 — 개발 = Docker, 로컬 멀티 컨테이너 = Compose, 프로덕션 클러스터 = Kubernetes.
자주 만나는 함정 5가지
1. Apple Silicon 아키텍처 불일치
Intel 기반으로 빌드된 이미지를 M1/M2 Mac에서 실행하면 오류가 나거나 성능이 크게 떨어져요.
# 특정 플랫폼 지정
docker run --platform linux/amd64 myapp:latest
# 멀티 아키텍처 빌드
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest .
Spring Boot Buildpack은 자동으로 멀티 아키텍처 이미지를 만들어 주니 이 함정이 거의 없어요.
2. 컨테이너 이름 충돌
같은 이름의 컨테이너가 이미 존재하면 오류가 발생합니다.
# 오류: "The container name '/gateway' is already in use"
# 해결 — 기존 컨테이너 중지 후 삭제
docker stop gateway && docker rm gateway
# 또는 --rm 옵션으로 종료 시 자동 삭제
docker run --rm --name gateway -p 8080:8080 myapp:latest
3. localhost 함정
이미 위에서 언급한 — 컨테이너 안에서 localhost는 자기 자신이에요. 다른 컨테이너로 접근하려면 컨테이너 이름·서비스 이름을 사용합니다.
4. 민감 정보를 Dockerfile에 하드코딩
# 절대 금지 — 이미지 레이어에 비밀번호가 영구 저장됨
ENV DB_PASSWORD=my_secret_password
# 올바른 방법 — 런타임 환경 변수로 주입
# docker run -e DB_PASSWORD=my_secret_password ...
K8s에서는 Secret을 통해 안전하게 주입할 수 있어요.
5. 에러 응답에 스택 트레이스 노출
이건 7번째 함정처럼 모든 환경에서 공통이에요. 컨테이너든 일반 배포든, 에러 응답에 내부 스택 트레이스가 나가면 보안 위험이 됩니다. 17편에서 자세히 풀어 갑니다.
시험 직전 한 번 더 — 자주 헷갈리는 함정 모음
여기까지가 15편 컨테이너·배포의 핵심이에요. 시험 직전 또는 실무에서 헷갈릴 때 다시 펼쳐 볼 수 있게 압축 노트로 마무리할게요.
- Docker = 표준 컨테이너 비유 — OS·JVM·앱·설정을 한 박스로 묶어 어디든 그대로 옮기기
- 이미지 = 설계도, 컨테이너 = 그 설계도로 지은 집, 같은 이미지로 여러 컨테이너 실행 가능
- 컨테이너 안 데이터는 삭제 시 사라짐 — 영속화는 Volume 또는 외부 저장소
- Spring Boot 2.3+ —
./mvnw spring-boot:build-image로 Dockerfile 없이 자동 이미지 빌드 - Buildpack 권장(멀티 아키텍처·자동 보안 패치), Dockerfile은 세밀 제어 필요할 때
- 컨테이너에서
localhost는 자기 자신 — 다른 컨테이너 접근 시 컨테이너 이름·서비스 이름 사용 - Compose의
depends_on은 시작 순서만 보장 — 준비 상태는condition: service_healthy와 함께 docker compose stop= 중지만,docker compose down= 중지 + 삭제,down -v는 볼륨까지 삭제- 12-Factor App — 설정은 환경 변수로 외부화,
.env파일·SPRING_*환경 변수 - Kubernetes = 컨테이너 항만 관리자 — 다중 호스트의 자동 배포·스케일·복구
- K8s 명사 4개 — Pod / Deployment / Service / ConfigMap·Secret
- Pod = 최소 배포 단위, Deployment가 Pod 복제본 + 롤링 업데이트 관리
- Service 3종 — ClusterIP(내부 기본), NodePort(개발 외부), LoadBalancer(프로덕션 외부)
- 비밀번호는 Secret, 일반 설정은 ConfigMap — ConfigMap은 암호화 X
kubectl apply -f(선언형) >kubectl create(명령형) — Git으로 인프라 이력 관리- Actuator + K8s Probe —
/actuator/health/liveness(재시작) +/actuator/health/readiness(트래픽) - liveness 실패 = Pod 재시작, readiness 실패 = 서비스에서 트래픽 차단
- Resource limits 항상 설정 — 한 Pod이 노드 전체 자원 소비 방지
- Apple Silicon —
--platform linux/amd64또는 Buildpack 자동 멀티 아키텍처 - 민감 정보를 Dockerfile
ENV에 박지 말 것 — 런타임 환경 변수 또는 Secret으로 주입
Docker의 자세한 사양은 Docker 공식 문서에서, Kubernetes의 깊은 활용은 Kubernetes 공식 문서에서 확인할 수 있어요.
시리즈 다른 편
같은 시리즈의 다른 글들도 같은 친절 톤으로 묶어 정리되어 있어요.
- 1편 — Spring Boot 입문
- 2편 — Spring MVC REST · MockMVC
- 3편 — Spring Data JPA · 검증
- 4편 — MySQL · Flyway · TestContainers
- 5편 — CSV 업로드 · 페이징 · 동적 쿼리
- 6편 — JPA 관계 매핑 심화
- 7편 — Spring Security · OAuth 2.0 · JWT
- 8편 — RestTemplate · RestClient
- 9편 — Reactive Programming · WebFlux 입문
- 10편 — WebFlux 심화 · MongoDB · WebClient
- 11편 — Cloud Gateway · Maven/Gradle · Buildpack
- 12편 — OpenAPI · Spring AI
- 13편 — Actuator · 관측성
- 14편 — Spring Cache · 이벤트
- 15편 — Docker · Compose · Kubernetes (현재 글)
- 16편 — 마이크로서비스 · Apache Kafka
- 17편 — Spring Professional · 베스트 프랙티스 (완)