Spring 개발일지

[팀프로젝트] MSA 기반 티켓팅 프로그램 - Dockerfile 및 CI workflow 작성

김둘리 2026. 5. 27. 11:08

ECS Fargate 배포를 위한 Docker 이미지 빌드 파일 작성과 GitHub Actions CI 파이프라인을 구성했다.

1. 왜 Docker를 사용하나?

기존 → 각자 IntelliJ에서 직접 실행
→ "내 컴에서는 되는데?" 문제 발생
→ 환경마다 다른 결과

Docker → 어느 컴퓨터에서도 동일한 환경
→ 배포 시 EC2, ECS에 이미지로 올림
→ 환경 차이 없음

2. Dockerfile - 멀티 스테이지 빌드

단순히 jar를 복사하는 방식 대신 멀티 스테이지 빌드를 적용했다.

# ===== Stage 1: Build =====
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app

COPY gradlew .
COPY gradle gradle
COPY build.gradle settings.gradle ./
RUN chmod +x gradlew

ARG GITHUB_USER
RUN --mount=type=secret,id=github_token \
    GITHUB_TOKEN="$(cat /run/secrets/github_token)" \
    GITHUB_USER=$GITHUB_USER \
    ./gradlew dependencies --no-daemon || true

COPY src src
RUN ./gradlew clean bootJar --no-daemon -x test

RUN java -Djarmode=layertools -jar build/libs/*.jar extract

# ===== Stage 2: Runtime =====
FROM eclipse-temurin:21-jre-alpine

RUN apk add --no-cache curl
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app

COPY --from=builder --chown=appuser:appgroup /app/dependencies/ ./
COPY --from=builder --chown=appuser:appgroup /app/spring-boot-loader/ ./
COPY --from=builder --chown=appuser:appgroup /app/snapshot-dependencies/ ./
COPY --from=builder --chown=appuser:appgroup /app/application/ ./

USER appuser
EXPOSE 8080

ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS org.springframework.boot.loader.launch.JarLauncher"]

3. 멀티 스테이지 빌드를 선택한 이유

단일 스테이지
→ JDK, 빌드 도구 등이 최종 이미지에 포함
→ 이미지 크기 커짐

멀티 스테이지
→ Stage 1: JDK로 빌드
→ Stage 2: JRE만으로 실행
→ 최종 이미지에 빌드 도구 미포함
→ 이미지 크기 감소 + 보안 강화

4. Layered Jar 적용

Spring Boot의 Layered Jar를 활용해서 Docker 캐시를 최적화했다.

RUN java -Djarmode=layertools -jar build/libs/*.jar extract
dependencies/      → 거의 변하지 않음 (캐시 재사용)
spring-boot-loader/ → 거의 변하지 않음 (캐시 재사용)
snapshot-dependencies/ → 가끔 변함
application/       → 코드 변경마다 변함

코드만 바뀌면 application 레이어만 새로 빌드하면 돼서 빌드 속도가 빨라진다.


5. 보안 고려사항

비루트 사용자 실행

RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

root로 실행하면 컨테이너 탈출 시 호스트 시스템에 위험하다. 비루트 사용자로 실행해서 보안을 강화했다.

GitHub Token 보안

처음에는 이렇게 했다.

# 취약한 방식 - ENV로 승격하면 레이어에 노출됨
ARG GITHUB_TOKEN
ENV GITHUB_TOKEN=$GITHUB_TOKEN

코드래빗 리뷰로 보안 취약점을 발견했다.

ARG → ENV로 승격하면 docker history로 토큰이 노출될 수 있음

BuildKit secret 마운트 방식으로 변경했다.

RUN --mount=type=secret,id=github_token \
    GITHUB_TOKEN="$(cat /run/secrets/github_token)" \
    ./gradlew dependencies --no-daemon || true

빌드 시에만 시크릿을 마운트하고 최종 이미지에는 남지 않는다.


6. JVM 컨테이너 메모리 설정

ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
UseContainerSupport → Docker 컨테이너의 메모리 제한을 JVM이 인식
MaxRAMPercentage=75.0 → 컨테이너 메모리의 75%를 JVM Heap으로 사용

컨테이너 환경에서 JVM이 호스트 메모리를 기준으로 동작하면 OOM이 발생할 수 있다.


7. Rest Docs 빌드 문제 해결

도커 빌드 시 테스트가 강제 실행되는 문제가 있었다.

// 문제 - bootJar가 asciidoctor에, asciidoctor가 test에 의존
bootJar {
    dependsOn asciidoctor // asciidoctor → test 강제 실행
}

build.gradle에서 bootJar의 asciidoctor 의존성을 제거했다.

// 해결
bootJar {
    // Rest Docs는 개발 환경에서만 사용
}

8. GitHub Actions CI workflow

PR을 올릴 때 자동으로 테스트가 실행되도록 구성했다.

name: CI

on:
  pull_request:
    branches:
      - dev

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'

      - name: Build & Test
        env:
          GITHUB_USER: ${{ github.actor }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: ./gradlew test --no-daemon

      - name: Upload test report (on failure)
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: test-report
          path: build/reports/tests/
          retention-days: 7

테스트 실패 시 리포트를 아티팩트로 업로드해서 실패 원인을 쉽게 확인할 수 있다.


마무리

Docker와 CI를 적용하면서 얻은 것들이다.

  • 멀티 스테이지 빌드로 이미지 경량화
  • Layered Jar로 빌드 속도 최적화
  • 비루트 사용자와 BuildKit secret으로 보안 강화
  • CI로 PR 시 자동 테스트로 코드 품질 보장

배포 환경을 표준화하면 팀 전체가 동일한 환경에서 개발할 수 있다.