프로젝트를 진행하면서 개발 단계에서는 DB, 캐시, 애플리케이션을 각각 따로 띄워 테스트하지만
운영 단계에서는 여러 서비스가 하나의 통합된 애플리케이션처럼 동작해야 합니다.
이때 여러 컨테이너를 한 번에 관리할 수 있는 도구가 바로 Docker Compose입니다.
이번 글에서는 제가 진행 중인 Spring Boot + PostgreSQL 프로젝트를 기준으로
Docker Compose를 활용해 (앱 + DB)를 한 번에 구동하고,
멀티스테이지 빌드로 빌드 속도와 이미지 크기를 최적화한 과정을 정리했습니다.
1. Docker Compose란 무엇인가
Docker Compose는 여러 컨테이너를 하나의 애플리케이션 단위로 묶어 관리할 수 있게 해주는 도구입니다.
기존에는 docker run 명령을 여러 번 실행해야 했지만
Compose는 docker-compose.yml에 서비스 정의를 모두 모아두고 한 명령으로 실행합니다.
`docker compose up` 한 줄이면
DB, 백엔드, 캐시, 네트워크, 볼륨까지 모두 자동으로 구성됩니다.
제가 사용한 Compose 구성은 배포용이 아닌 로컬 개발 환경용으로
PostgreSQL(DB)과 Spring Boot 애플리케이션(Backend)을 함께 실행합니다.
version: "3.9"
services:
db:
image: postgres:16
container_name: webtest-db
environment:
POSTGRES_DB: ${DB_NAME:-webtest}
POSTGRES_USER: ${DB_USER:-dev}
POSTGRES_PASSWORD: ${DB_PASSWORD:-dev1234}
ports:
- "55432:5432"
volumes:
- db-data:/var/lib/postgresql/data
app:
build:
context: .
dockerfile: Dockerfile
depends_on:
- db
environment:
SPRING_PROFILES_ACTIVE: local
DB_HOST: db
DB_PORT: 5432
volumes:
db-data:
이 설정에서 눈여겨볼 부분은 다음과 같습니다.
- 환경변수 기본값: ${VAR:-default} 문법을 이용해 .env 파일이 없어도 기본값이 적용됩니다.
- 서비스 간 네트워크 연결: DB_HOST=db로 지정하면 Compose가 자동으로 내부 DNS를 설정해 앱이 DB에 db:5432로 접속할 수 있습니다.
- 데이터 영속화: volumes로 정의된 db-data는 컨테이너를 재생성해도 데이터가 유지됩니다.
- depends_on: 앱이 DB보다 나중에 시작되도록 보장하지만, DB “준비 완료”까지 기다리지는 않습니다.
2. Compose 내부 동작 흐름
`docker compose up` 명령을 실행하면 내부적으로 아래와 같은 순서로 동작합니다.
- .env 파일 로딩
Compose는 현재 디렉토리의 .env를 자동으로 읽습니다.
환경변수가 지정되지 않았을 경우 ${VAR:-default} 문법의 기본값을 사용합니다. - 서비스 그래프 구성
depends_on 규칙에 따라 서비스 간 의존 관계를 구성합니다.
예를 들어 app → db 관계가 있다면, DB 컨테이너가 먼저 실행됩니다. - 리소스 생성
네트워크, 볼륨 등 Compose 수준의 리소스를 생성합니다. - 이미지 빌드 및 Pull
- db: postgres:16 이미지를 pull
- app: Dockerfile 기반으로 직접 빌드
- 컨테이너 실행
- DB가 먼저 실행되어 초기화 스크립트를 처리합니다.
- 앱 컨테이너가 실행되어 DB와 연결을 시도합니다.
- Compose의 내부 네트워크를 통해 db라는 서비스명으로 연결이 이뤄집니다.
3. Dockerfile 멀티스테이지 빌드
멀티스테이지 빌드는 한 Dockerfile 안에서 빌드 단계와 실행 단계를 분리하는 기법입니다.
빌드에는 JDK, Gradle 등의 도구가 필요하지만 실제 런타임에는 JRE만 있으면 충분합니다.
아래는 제가 사용 중인 실제 Dockerfile 예시입니다.
# Build stage
FROM gradle:8.10-jdk17 AS builder
WORKDIR /app
COPY gradlew ./gradlew
COPY gradle ./gradle
COPY settings.gradle build.gradle ./
COPY gradle.properties ./gradle.properties
RUN chmod +x ./gradlew
# Gradle 캐시를 BuildKit 캐시로 연결
RUN --mount=type=cache,target=/home/gradle/.gradle \
./gradlew --no-daemon dependencies || true
COPY src ./src
RUN --mount=type=cache,target=/home/gradle/.gradle \
./gradlew --no-daemon clean bootJar -x test
# Runtime stage
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=builder /app/build/libs/*-SNAPSHOT.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","app.jar"]
이 Dockerfile은 두 가지 특징이 있습니다.
- Gradle 캐시와 BuildKit 캐시의 결합
- Gradle은 내부적으로 ~/.gradle에 의존성(jar)과 빌드 결과를 캐싱합니다.
- BuildKit은 Docker 빌드 엔진의 캐시로, 특정 디렉토리를 캐시 마운트(--mount=type=cache)할 수 있습니다.
- 즉 Gradle 캐시가 “무엇을” 캐시하는지 정의하고, BuildKit은 “어디에/어떻게” 그 캐시를 유지할지 담당합니다.
- 둘을 함께 사용하면 컨테이너 빌드 환경에서도 의존성 다운로드 시간을 대폭 단축할 수 있습니다.
- 멀티스테이지로 이미지 경량화
- 첫 번째 스테이지에서는 JDK와 Gradle을 이용해 JAR를 빌드하고,
- 두 번째 스테이지에서는 JAR 파일만 JRE 이미지로 복사합니다.
- 그 결과, 최종 이미지는 약 200MB 내외로 작고, 보안 취약점도 줄어듭니다.
4. ENTRYPOINT와 그레이스풀 셧다운
`ENTRYPOINT ["java","-jar","app.jar"]` 설정은 컨테이너가 시작될 때 실행할 명령을 지정합니다.
이 방식은 exec form으로 작성되어 PID 1 프로세스로 실행되며
SIGTERM, SIGINT 같은 종료 시그널을 정확히 전달받습니다.
Spring Boot는 종료 시그널을 받으면 내부 톰캣 서버가
새 요청 수신을 중단하고 진행 중인 요청을 마무리한 뒤 정상 종료하는
그레이스풀 셧다운을 수행합니다.
이 덕분에 운영 중에도 컨테이너 재배포나 업데이트 시 트랜잭션이 끊기지 않고 안정적으로 마무리됩니다.
정리하며
Compose와 멀티스테이지 빌드는 단순히 컨테이너를 묶는 기술이 아닙니다.
로컬 개발 환경에서 운영 환경과 최대한 동일한 조건을 재현하고
빌드 과정의 속도, 안정성, 일관성을 확보하는 핵심 도구입니다.
현재 프로젝트에서는
- Docker Compose로 DB+Spring 환경을 한 번에 띄우고
- BuildKit 캐시와 Gradle 캐시를 함께 사용하여 빌드 시간을 절반 이하로 줄였으며
- 멀티스테이지 빌드로 이미지 크기를 최소화했습니다.
이 과정 덕분에 팀원 모두가 동일한 개발 환경을 유지하면서
코드 변경 → 빌드 → 테스트 → 실행까지의 흐름을 완전히 자동화할 수 있었습니다.
'프로젝트 > 웹 성능 테스트' 카테고리의 다른 글
| DB 마이그레이션 (0) | 2025.10.25 |
|---|---|
| Git Flow 전략 (1) | 2025.10.24 |
| 도커: 이론2(용어 정리) (0) | 2025.10.18 |
| Docker 적용: 이론 (0) | 2025.10.12 |
| GHCR로 백엔드 도커 이미지 자동 빌드 & 푸시 (2) | 2025.09.07 |