인프라

Spring Boot CI/CD 빌드 시간 단축하기 - Gradle 최적화 전략

chanyoun 2026. 2. 9. 14:30
CI-CD 빌드 시간 단축

Spring Boot CI/CD 빌드 시간 단축하기 - Gradle 최적화 전략

Spring Boot 프로젝트의 CI/CD 파이프라인에서 빌드와 테스트 단계가 과도하게 오래 걸리는 문제를 해결한 경험을 공유합니다.

특히 RestDocs, Swagger 문서 생성을 포함한 복잡한 빌드 프로세스에서 6~7분 걸리던 CI/CD 시간을 절반 이하로 단축할 수 있었습니다.

 

1. 문제상황

CI/CD 파이프라인에서 다음과 같은 문제들이 발생했습니다.

  • GitHub Actions에서 빌드 완료까지 6~7분 이상 소요됩니다.

    image-20260204115719052

  • 특히 test 단계와 build 단계에서 대부분의 시간이 소비됩니다.

  • 코드 변경이 없어도 매번 전체 빌드를 새로 실행합니다.

  • Docker 이미지 빌드도 매번 처음부터 다시 시작됩니다.

 

2. 원인

 

2.1 과도한 태스크 의존성

build.gradle 파일을 분석한 결과, build 태스크가 11개의 의존 태스크를 순차적으로 실행하고 있었습니다.

tasks.named("build") {
    dependsOn "ensureDocsDir", "cleanDocs", "test",
              "asciidoctorMember", "asciidoctorAdmin", "asciidoctorDirector",
              "generateExceptionDocs", "openapi3", "generateSwaggerUI",
              "copyDocs", "bootJar", "patchSwaggerUI"
}

이 중 일부 태스크는 병렬로 실행 가능한데도 순차적으로 실행되고 있었습니다.

 

2.2 불필요한 clean 작업

CI/CD 환경에서는 매번 새로운 환경에서 빌드가 실행되는데, ./gradlew clean build 명령어로 인해 이전 빌드 결과를 삭제하는 불필요한 작업이 수행되고 있었습니다.

# 기존 설정
- name: Build with Gradle
  run: ./gradlew clean build

 

2.3 Gradle 빌드 캐시 미사용

Gradle은 빌드 캐시 기능을 제공하지만, 활성화되지 않아 의존성 다운로드와 컴파일을 매번 반복하고 있었습니다.

 

2.4 Docker 레이어 캐싱 미사용

Docker 이미지를 빌드할 때도 레이어 캐싱을 사용하지 않아, 변경되지 않은 레이어까지 매번 다시 빌드하고 있었습니다.

 

3. 원인 검증

최적화 전략을 세우기 위해 먼저 로컬 환경에서 빌드 프로세스를 분석했습니다.

 

3.1 빌드 태스크 분석

./gradlew build --dry-run

./gradlew build --dry-run: 실제로 빌드를 실행하지 않고, 어떤 태스크들이 어떤 순서로 실행될지만 보여줍니다.

위 명령어로 태스크 실행 순서를 확인한 결과, 다음과 같이 순차 실행되는 것을 확인했습니다.

:compileJava SKIPPED
:processResources SKIPPED
:classes SKIPPED
:jar SKIPPED
:ensureDocsDir SKIPPED
:cleanDocs SKIPPED
:compileTestJava SKIPPED
:processTestResources SKIPPED
:testClasses SKIPPED
:test SKIPPED
:asciidoctorAdmin SKIPPED
:asciidoctorDirector SKIPPED
:asciidoctorMember SKIPPED
:generateExceptionMarkdown SKIPPED
:generateExceptionDocs SKIPPED
:check SKIPPED
:openapi3 SKIPPED
:generateSwaggerUISample SKIPPED
:generateSwaggerUI SKIPPED
:patchSwaggerUI SKIPPED
:copyDocs SKIPPED
:resolveMainClassName SKIPPED
:bootJar SKIPPED
:assemble SKIPPED
:build SKIPPED

asciidoctorMember, asciidoctorAdmin, asciidoctorDirector 세 태스크는 서로 독립적이므로 병렬 실행이 가능합니다.

 

3.2 빌드 시간 프로파일링

./gradlew build --profile

./gradlew build --profile: 빌드를 실행하면서 각 태스크가 얼마나 시간이 걸렸는지 상세한 리포트를 생성합니다.

빌드가 완료되면 be/build/reports/profile/profile-<timestamp>.html 파일이 생성됩니다.

# 리포트 열기
open be/build/reports/profile/profile-*.html

image-20260204123017957

프로파일링 결과:

  • test: 전체 시간의 68.2%
  • asciidoctor* 태스크들: 전체 시간의 9.9%
  • 의존성 다운로드: 전체 시간의 1%

따라서 의존성 다운로드 부분은 로컬에서는 큰 문제가 아닌 것을 확인했습니다. 먼저 asciidoctor 태스크들을 병렬 실행하여 해당 부분을 최적화합니다.

 

4. 해결방법

 

4.1 Gradle 빌드 캐시 활성화

 

로컬에서 먼저 테스트

최적화 옵션을 로컬에서 먼저 테스트합니다.

기존 빌드 시간은 대략 1분 2.83초 정도 걸렸으니, 캐시 및 병렬 실행 시 얼마나 줄어드는지 확인합니다.

# 최적화된 빌드 시간 측정
time ./gradlew build --build-cache --parallel

--build-cache 옵션으로 빌드 캐시를 활성화하고, --parallel 옵션으로 독립적인 태스크들을 병렬 실행합니다.

image-20260204123352606

병렬 실행 시 대략 6~7초 정도 줄어든 것을 확인할 수 있습니다.

 

GitHub Actions에 적용

로컬 테스트에서 효과가 확인되었으니, GitHub Actions workflow 파일에 Gradle 캐싱을 추가합니다.

- name: Setup Gradle
  uses: gradle/gradle-build-action@v2
  with:
    cache-read-only: false  # dev 브랜치에서도 캐시 쓰기 허용

- name: Build with Gradle
  run: ./gradlew build --build-cache --parallel

cache-read-only: false: 기본적으로 main/master 브랜치가 아닌 경우 캐시를 읽기만 가능합니다. cache-read-only: false를 설정하면 현재 브랜치에서도 캐시를 저장할 수 있어, 다음 빌드 시 활용할 수 있습니다.

 

4.2 clean 작업 제거

CI 환경에서는 매번 새로운 환경이므로 clean이 불필요합니다.

# 변경 전
- name: Build with Gradle
  run: ./gradlew clean build

# 변경 후
- name: Build with Gradle
  run: ./gradlew build --build-cache --parallel

 

4.3 Docker 레이어 캐싱 활성화

GitHub Actions의 캐시 기능을 활용하여 Docker 빌드를 최적화합니다.

# 4. docker hub로 build & push
- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@v2

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

- name: Docker build and push
  uses: docker/build-push-action@v4
  with:
    context: ${{ env.BE_WORKING_DIR }}      # ./be
    file: ${{ env.BE_WORKING_DIR }}/Dockerfile  # ./be/Dockerfile
    push: true
    tags: ${{ secrets.DOCKER_USERNAME }}/motd-dev-be:latest
    cache-from: type=gha
    cache-to: type=gha,mode=max

cache-fromcache-to 옵션으로 GitHub Actions 캐시를 활용합니다.

 

5. 검증

 

5.1 로컬 빌드 시간 비교

# 최적화 전
time ./gradlew clean build
# 결과: 약 1분 5초

# 최적화 후 (첫 실행 - 캐시 생성)
time ./gradlew build --build-cache --parallel
# 결과: 약 59초

# 최적화 후 (두 번째 실행 - 캐시 히트)
time ./gradlew build --build-cache --parallel
# 결과: 약 57초

 

5.2 CI/CD 파이프라인 실행 시간

로컬 테스트에서 효과가 확인되어, GitHub Actions에도 동일한 최적화를 적용했습니다.

  • 최적화 전: 6분 10초
  • 최적화 후: 2분 31초

image-20260204115719052

image-20260206151747727

image-20260206152053148

위 summary에서 확인할 수 있듯이, Gradle Build Action이 자동으로 캐시를 관리하고 있습니다.

 

캐시 동작 분석

복원 단계 (Entries Restored)

  • 6개 항목, 511 MB를 18.8초에 복원

  • 이전 빌드에서 저장한 다음 항목들을 재사용합니다:

    • 다운로드한 의존성 (dependencies)
    • 컴파일된 클래스 파일
    • Gradle wrapper
    • Build cache (이전 빌드 결과)

저장 단계 (Entries Saved)

  • 4개 항목, 324 MB를 10.9초에 저장

  • 변경된 부분만 업데이트하여 다음 빌드를 위해 저장합니다

  • Cache cleanup 효과: 511 MB → 324 MB (37% 감소)

    • 오래된 버전의 라이브러리 제거
    • 더 이상 사용하지 않는 build cache 정리

 

5.3 로컬 vs CI/CD 개선 효과 차이 분석

로컬에서는 보통 ./gradlew clean build만 수행하고(배포 Job처럼 Docker build/push를 하지 않음),

~/.gradle/caches가 유지되기 때문에 의존성 다운로드 시간은 대부분 이미 상수에 가깝습니다.

또한 clean은 프로젝트 빌드 산출물만 지우고, Gradle 의존성 캐시까지 매번 비우진 않기 때문에 --build-cache의 체감이 크지 않을 수 있습니다.

그래서 로컬에서의 개선은 주로 --parallel로 독립 태스크를 병렬 실행한 만큼(약 6~7초)만 나타났습니다.

 

반면 CI/CD 환경은 매 실행이 거의 “새 환경”에서 시작하므로, 캐시가 없으면 의존성/Gradle 관련 파일을 다시 받거나 빌드 결과물을 매번 새로 만들게 됩니다.

여기서 Gradle 캐시(의존성/빌드 캐시) 복원 + Docker 레이어 캐시(cache-from/to: gha)를 적용하면 의존성 준비 시간과 Docker 빌드 시간이 함께 줄어들어, 전체 실행 시간이 6분 10초 → 2분 31초(약 59%)로 크게 감소할 수 있습니다.