[BASIC #5] 실전 프로젝트: Spring Boot 앱 CI/CD 파이프라인 구축하기

2026. 2. 24. 16:44·CI&CD/GitHub Actions

0. 들어가며

지금까지 GitHub Actions의 이론, 기본 문법, 변수와 컨텍스트까지 모두 배웠다. 이번 포스팅에서는 배운 내용을 종합하여 실제 Spring Boot 애플리케이션의 CI/CD 파이프라인을 구축해보겠다.

  1. 코드 push 시 자동 빌드 및 테스트
  2. 테스트 통과 시 Docker 이미지 빌드
  3. Docker Hub에 이미지 push
  4. 배포 서버에 자동 배포

1. 프로젝트 준비

1.1. Spring Boot 애플리케이션

간단한 Spring Boot 애플리케이션을 준비한다.

프로젝트 구조:

spring-boot-ci-cd/
├── .github/
│   └── workflows/
│       └── ci-cd-pipeline.yml
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/
│   │   │       └── example/
│   │   │           └── demo/
│   │   │               └── DemoApplication.java
│   │   └── resources/
│   │       └── application.yml
│   └── test/
│       └── java/
│           └── com/
│               └── example/
│                   └── demo/
│                       └── DemoApplicationTests.java
├── Dockerfile
├── pom.xml
└── README.md

1.2. Dockerfile 작성

FROM openjdk:17-jdk-slim AS builder
WORKDIR /build
COPY mvnw .
COPY .mvn .mvn
COPY pom.xml .
RUN ./mvnw dependency:go-offline

COPY src ./src
RUN ./mvnw package -DskipTests

FROM openjdk:17-jre-slim
WORKDIR /app
COPY --from=builder /build/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

1.3. GitHub Secrets 설정

파이프라인에 필요한 Secrets을 등록한다.

Secret 이름  설명
DOCKER_USERNAME Docker Hub 사용자명
DOCKER_PASSWORD Docker Hub 비밀번호 또는 토큰
DEPLOY_HOST 배포 서버 IP 또는 도메인
DEPLOY_USERNAME 배포 서버 SSH 사용자명
DEPLOY_SSH_KEY 배포 서버 SSH 개인키

2. CI/CD 파이프라인 전체 구조

2.1. 파이프라인 설계

[Push / PR] → [Build & Test] → [Docker Build] → [Docker Push] → [Deploy]
      ↓              ↓               ↓                ↓              ↓
   Trigger      Run tests       Create image     Push to Hub    Deploy to server

2.2. 전체 워크플로우 파일

.github/workflows/ci-cd-pipeline.yml

name: Spring Boot CI/CD Pipeline

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]
  workflow_dispatch:
    inputs:
      environment:
        description: '배포 환경'
        required: true
        default: 'dev'
        type: choice
        options:
          - dev
          - prod

env:
  DOCKER_IMAGE: ${{ secrets.DOCKER_USERNAME }}/spring-boot-app
  JAVA_VERSION: '17'

jobs:
  # 1. 테스트 Job
  test:
    name: Test
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up JDK ${{ env.JAVA_VERSION }}
        uses: actions/setup-java@v4
        with:
          java-version: ${{ env.JAVA_VERSION }}
          distribution: 'temurin'
          cache: maven

      - name: Run tests
        run: ./mvnw test

      - name: Generate test report
        if: always()
        uses: dorny/test-reporter@v1
        with:
          name: Maven Tests
          path: target/surefire-reports/*.xml
          reporter: java-junit
          fail-on-error: false

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: test-results
          path: target/surefire-reports/

  # 2. 빌드 Job (test에 의존)
  build:
    name: Build
    needs: test
    runs-on: ubuntu-latest
    if: github.event_name != 'pull_request' || github.event.action != 'closed'

    outputs:
      version: ${{ steps.get-version.outputs.version }}
      image-tag: ${{ steps.meta.outputs.tags }}

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up JDK ${{ env.JAVA_VERSION }}
        uses: actions/setup-java@v4
        with:
          java-version: ${{ env.JAVA_VERSION }}
          distribution: 'temurin'
          cache: maven

      - name: Build with Maven
        run: ./mvnw package -DskipTests

      - name: Get version from pom.xml
        id: get-version
        run: |
          VERSION=$(./mvnw help:evaluate -Dexpression=project.version -q -DforceStdout)
          echo "version=$VERSION" >> $GITHUB_OUTPUT
          echo "Project version: $VERSION"

      - name: Upload JAR artifact
        uses: actions/upload-artifact@v3
        with:
          name: app-jar
          path: target/*.jar

      - name: Docker meta
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.DOCKER_IMAGE }}
          tags: |
            type=raw,value=${{ steps.get-version.outputs.version }}
            type=sha,prefix=,format=short
            type=ref,event=branch
            type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}

  # 3. Docker Build & Push Job
  docker:
    name: Docker Build & Push
    needs: build
    runs-on: ubuntu-latest
    if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ needs.build.outputs.image-tag }}
          cache-from: type=registry,ref=${{ env.DOCKER_IMAGE }}:buildcache
          cache-to: type=registry,ref=${{ env.DOCKER_IMAGE }}:buildcache,mode=max
          build-args: |
            VERSION=${{ needs.build.outputs.version }}

      - name: Image digest
        run: echo "Image pushed with digest ${{ steps.build-push.outputs.digest }}"

  # 4. 배포 Job (환경별)
  deploy:
    name: Deploy
    needs: [build, docker]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    environment: ${{ github.event.inputs.environment || 'prod' }}

    steps:
      - name: Deploy to Server
        uses: appleboy/ssh-action@v1.0.0
        with:
          host: ${{ secrets.DEPLOY_HOST }}
          username: ${{ secrets.DEPLOY_USERNAME }}
          key: ${{ secrets.DEPLOY_SSH_KEY }}
          port: 22
          script: |
            # 배포 스크립트
            docker pull ${{ env.DOCKER_IMAGE }}:latest
            docker stop spring-app || true
            docker rm spring-app || true
            docker run -d \\
              --name spring-app \\
              -p 8080:8080 \\
              --restart unless-stopped \\
              ${{ env.DOCKER_IMAGE }}:latest
            docker system prune -f

      - name: Health check
        run: |
          sleep 10
          curl -f <http://$>{{ secrets.DEPLOY_HOST }}:8080/actuator/health || exit 1

      - name: Send Slack notification
        if: always()
        uses: slackapi/slack-github-action@v1
        with:
          payload: |
            {
              "text": "배포 결과: ${{ job.status }}\\n환경: ${{ github.event.inputs.environment || 'prod' }}\\n버전: ${{ needs.build.outputs.version }}"
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

3. 단계별 상세 설명

3.1. Test Job 분석

test:
  name: Test
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-java@v4
      with:
        java-version: '17'
        distribution: 'temurin'
        cache: maven  # Maven 의존성 캐싱 (빌드 속도 향상)

    - run: ./mvnw test

    - uses: dorny/test-reporter@v1
      if: always()  # 테스트 성공/실패 관계없이 리포트 생성
      with:
        name: Maven Tests
        path: target/surefire-reports/*.xml
        reporter: java-junit

핵심 포인트:

  • cache: maven: Maven 의존성을 캐시하여 다음 실행 시 빌드 속도 향상
  • if: always(): 테스트 실패해도 리포트는 생성하도록 설정
  • test-reporter: 테스트 결과를 UI에서 보기 좋게 표시

3.2. Build Job 분석

build:
  needs: test
  outputs:
    version: ${{ steps.get-version.outputs.version }}
    image-tag: ${{ steps.meta.outputs.tags }}

  steps:
    - run: ./mvnw package -DskipTests  # 테스트는 이미 했으므로 건너뜀

    - id: get-version
      run: |
        VERSION=$(./mvnw help:evaluate -Dexpression=project.version -q -DforceStdout)
        echo "version=$VERSION" >> $GITHUB_OUTPUT

    - uses: docker/metadata-action@v5
      id: meta
      with:
        images: ${{ env.DOCKER_IMAGE }}
        tags: |
          type=raw,value=${{ steps.get-version.outputs.version }}
          type=sha,prefix=,format=short
          type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}

핵심 포인트:

  • outputs: 다음 Job에서 사용할 값들을 정의
  • metadata-action: Docker 태그를 자동 생성 (버전, 커밋 SHA, latest 등)

3.3. Docker Job 분석

docker:
  needs: build
  if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'

  steps:
    - uses: docker/setup-buildx-action@v3  # Buildx 설정 (멀티 플랫폼 빌드 가능)

    - uses: docker/login-action@v3
      with:
        username: ${{ secrets.DOCKER_USERNAME }}
        password: ${{ secrets.DOCKER_PASSWORD }}

    - uses: docker/build-push-action@v5
      with:
        context: .
        push: true
        tags: ${{ needs.build.outputs.image-tag }}
        cache-from: type=registry,ref=${{ env.DOCKER_IMAGE }}:buildcache
        cache-to: type=registry,ref=${{ env.DOCKER_IMAGE }}:buildcache,mode=max

핵심 포인트:

  • setup-buildx-action: Docker Buildx 설정 (멀티 아키텍처 빌드 가능)
  • cache-from/to: 레이어 캐싱으로 빌드 속도 최적화
  • push: true: 빌드 후 자동으로 Docker Hub에 push

3.4. Deploy Job 분석

deploy:
  needs: [build, docker]
  environment: ${{ github.event.inputs.environment || 'prod' }}

  steps:
    - uses: appleboy/ssh-action@v1.0.0
      with:
        host: ${{ secrets.DEPLOY_HOST }}
        username: ${{ secrets.DEPLOY_USERNAME }}
        key: ${{ secrets.DEPLOY_SSH_KEY }}
        script: |
          docker pull ${{ env.DOCKER_IMAGE }}:latest
          docker stop spring-app || true
          docker rm spring-app || true
          docker run -d --name spring-app -p 8080:8080 ${{ env.DOCKER_IMAGE }}:latest

    - run: |
        sleep 10
        curl -f <http://$>{{ secrets.DEPLOY_HOST }}:8080/actuator/health || exit 1

핵심 포인트:

  • environment: GitHub 환경 설정과 연동 (승인 절차, 보안)
  • appleboy/ssh-action: SSH로 서버에 접속하여 명령 실행
  • Health check: 배포 후 애플리케이션 정상 동작 확인

4. 환경별 배포 설정

4.1. GitHub Environments 설정

저장소 Settings > Environments에서 환경을 설정할 수 있다.

Environments:
  - dev
    - Required reviewers: (선택)
    - Wait timer: (선택)
    - Deployment branches: develop
  - prod
    - Required reviewers: 2명
    - Wait timer: 10분
    - Deployment branches: main

4.2. 환경별 워크플로우 확장

name: Environment-specific Deployment

on:
  push:
    branches: [ develop, main ]

jobs:
  deploy-dev:
    if: github.ref == 'refs/heads/develop'
    runs-on: ubuntu-latest
    environment: dev
    steps:
      - run: echo "개발 환경 배포"

  deploy-prod:
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: prod
    steps:
      - run: echo "운영 환경 배포"

5. 고급 최적화

5.1. 의존성 캐싱

- name: Cache Maven dependencies
  uses: actions/cache@v3
  with:
    path: ~/.m2
    key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
    restore-keys: ${{ runner.os }}-m2

5.2. 조건부 단계 실행

- name: Run integration tests
  if: github.event_name == 'push' && github.ref == 'refs/heads/main'
  run: ./mvnw verify -Pintegration-tests

5.3. 병렬 테스트 실행

strategy:
  matrix:
    java: [11, 17, 21]
    os: [ubuntu-latest, windows-latest]
steps:
  - uses: actions/setup-java@v4
    with:
      java-version: ${{ matrix.java }}
  - run: ./mvnw test

6. 모니터링 및 알림

6.1. Slack 알림 추가

- name: Notify Slack on success
  if: success()
  uses: slackapi/slack-github-action@v1
  with:
    payload: |
      {
        "text": "✅ 배포 성공: ${{ github.repository }}@${{ github.sha }}",
        "blocks": [
          {
            "type": "section",
            "text": {
              "type": "mrkdwn",
              "text": "*배포 성공*\\n저장소: ${{ github.repository }}\\n버전: ${{ needs.build.outputs.version }}\\n환경: ${{ github.event.inputs.environment || 'prod' }}"
            }
          }
        ]
      }
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

6.2. 배포 상태 배지 추가

README.md에 배지 추가:

![CI/CD](<https://github.com/username/repo/actions/workflows/ci-cd-pipeline.yml/badge.svg>)

7. 전체 파이프라인 실행 결과

7.1. 성공적인 실행

https://via.placeholder.com/800x400?text=CI%252FCD+Pipeline+Success

7.2. 로그 확인

GitHub Actions 탭에서 각 Step의 상세 로그를 확인할 수 있다.

Run ./mvnw test
[INFO] Scanning for projects...
[INFO]
[INFO] --------------------< com.example:demo >--------------------
[INFO] Building demo 1.0.0
[INFO]   from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-resources-plugin:3.2.0:resources (default-resources) @ demo ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Using 'UTF-8' encoding to copy filtered properties files.
[INFO] Copying 1 resource
[INFO]
[INFO] --- maven-compiler-plugin:3.10.1:compile (default-compile) @ demo ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 2 source files to /home/runner/work/demo/demo/target/classes
[INFO]
[INFO] --- maven-resources-plugin:3.2.0:testResources (default-testResources) @ demo ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Using 'UTF-8' encoding to copy filtered properties files.
[INFO] skip non existing resourceDirectory /home/runner/work/demo/demo/src/test/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.10.1:testCompile (default-testCompile) @ demo ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to /home/runner/work/demo/demo/target/test-classes
[INFO]
[INFO] --- maven-surefire-plugin:2.22.2:test (default-test) @ demo ---
[INFO] Running com.example.demo.DemoApplicationTests
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.456 s - in com.example.demo.DemoApplicationTests
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  12.345 s
[INFO] Finished at: 2024-01-15T09:30:45Z
[INFO] ------------------------------------------------------------------------

8. 정리

이번 포스팅에서는 실제 Spring Boot 애플리케이션의 CI/CD 파이프라인을 구축해보았다.

구현된 파이프라인:

  1. 테스트 자동화: 코드 push 시 자동으로 테스트 실행
  2. 빌드 자동화: 테스트 통과 시 JAR 파일 빌드
  3. Docker 이미지 빌드 및 Push: Docker Hub에 이미지 업로드
  4. 자동 배포: 서버에 SSH 접속하여 최신 이미지로 배포
  5. 헬스 체크: 배포 후 애플리케이션 정상 동작 확인
  6. 알림: Slack으로 배포 결과 전송

배운 점:

  • Job 간 의존성(needs)으로 파이프라인 순서 제어
  • outputs로 Job 간 데이터 공유
  • Secrets로 민감 정보 안전하게 관리
  • 환경별(environment) 배포 구성
  • 캐싱으로 빌드 속도 최적화

'CI&CD > GitHub Actions' 카테고리의 다른 글

[ADVANED #1] GitHub Actions에서 Docker 고급 빌드 전략 (Buildx, 캐싱, 보안)  (0) 2026.02.24
[BASIC #4] GitHub Actions 변수와 컨텍스트 마스터하기  (0) 2026.02.24
[BASIC #3] GitHub Actions 핵심 문법 완벽 가이드  (0) 2025.09.23
[BASIC #2] 워크플로우 생성 및 실행 실습  (0) 2025.06.24
[BASIC #1] CI/CD 개념부터 GitHub Actions 이해하기 - 이론편  (0) 2025.06.17
'CI&CD/GitHub Actions' 카테고리의 다른 글
  • [ADVANED #1] GitHub Actions에서 Docker 고급 빌드 전략 (Buildx, 캐싱, 보안)
  • [BASIC #4] GitHub Actions 변수와 컨텍스트 마스터하기
  • [BASIC #3] GitHub Actions 핵심 문법 완벽 가이드
  • [BASIC #2] 워크플로우 생성 및 실행 실습
h6bro
h6bro
백엔드 개발자의 기술 블로그
  • h6bro
    Jun's Tech Blog
    h6bro
  • 전체
    오늘
    어제
    • 분류 전체보기 (250) N
      • Java (18)
        • Core (9)
        • Design Pattern (9)
      • Spring (80)
        • Core (24)
        • MVC (6)
        • DB (10)
        • JPA (26)
        • Monitoring (3)
        • Security (11)
        • WebSocket (0)
      • Database (33)
        • Redis (15)
        • MySQL (18)
      • MSA (25) N
        • MSA 기본 (11)
        • MSA 아키텍처 (14) N
      • Kafka (30)
        • Core (18)
        • Connect (12)
      • ElasticSearch (11)
        • Search (11)
        • Logging (0)
      • Test (4)
        • k6 (4)
      • Docker (9)
      • CI&CD (10)
        • GitHub Actions (6)
        • ArgoCD (4)
      • Kubernetes (18)
        • Core (12)
        • Ops (6)
      • Cloud Engineering (4)
        • AWS Infrastructure (3)
        • AWS EKS (1)
        • Terraform (0)
      • Project (8)
        • LinkFolio (1)
        • Secondhand Market (7)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • Cloud Engineering 포스팅 정리
  • 인기 글

  • 태그

    ㅈ
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
h6bro
[BASIC #5] 실전 프로젝트: Spring Boot 앱 CI/CD 파이프라인 구축하기
상단으로

티스토리툴바