이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • Build, Test, Package 단계는 각각 어떤 실패를 미리 걸러야 하는가?
  • Spring Boot artifact를 pipeline에서 만들 때 재현성을 어떻게 확보하는가?
  • 테스트는 통과했는데 package나 image push가 실패하면 무엇을 확인해야 하는가?

개요

Build Test Package Pipeline은 코드 변경을 배포 가능한 산출물로 바꾸는 검증 흐름이다.

Spring 백엔드에서는 Gradle build, test, bootJar, container image build, artifact upload가 여기에 들어간다.

이 단계의 목표는 운영에 나갈 수 없는 변경을 배포 전에 멈추는 것이다.

단순히 명령을 순서대로 실행하는 것이 아니라 실패 단계마다 다른 의미를 읽어야 한다.

Build 단계

Build는 코드가 컴파일되고 의존성이 해결되는지 확인한다.

./gradlew clean compileJava processResources

의존성 저장소 장애, Java version 차이, annotation processing 오류, generated source 누락이 이 단계에서 드러난다.

로컬 JDK와 CI JDK가 다르면 “로컬은 되는데 CI는 실패”가 반복된다.

pipeline에는 Java version과 Gradle cache 기준을 명확히 둔다.

Test 단계

Test는 변경이 기존 계약을 깨지 않았는지 확인한다.

./gradlew test
./gradlew integrationTest

unit test는 빠르게 피드백을 준다.

integration test는 DB, Redis, HTTP client, migration 같은 외부 의존성 계약을 확인한다.

Spring context loading 실패는 profile, configuration property, bean condition 오류를 조기에 드러낸다.

flaky test는 무시하지 말고 격리해야 한다.

Package 단계

Package는 검증된 코드를 배포 가능한 artifact로 만든다.

Spring Boot에서는 bootJar가 대표적이다.

./gradlew clean test bootJar
ls -lh build/libs/*.jar

Jar 이름, manifest, version, profile 포함 여부, resource 파일 포함 여부를 확인한다.

테스트가 통과해도 package 단계에서 resource 누락이나 duplicate file 문제가 날 수 있다.

Pipeline 예시

name: build-test-package
on:
  push:
    branches: [main]
 
jobs:
  package:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: '21'
          cache: gradle
      - run: ./gradlew clean test bootJar
      - name: Upload jar
        uses: actions/upload-artifact@v4
        with:
          name: order-api-${{ github.sha }}
          path: build/libs/*.jar

artifact 이름에 commit SHA를 넣으면 어떤 코드에서 나온 산출물인지 추적하기 쉽다.

Container Image

운영 배포가 container 기준이라면 jar만 만들고 끝나면 부족하다.

image build와 push도 package의 일부로 본다.

docker build -t registry.example.com/order-api:${GITHUB_SHA} .
docker push registry.example.com/order-api:${GITHUB_SHA}

Dockerfile의 base image, build context, .dockerignore, layer cache가 결과에 영향을 준다.

image push 실패는 테스트 실패가 아니라 registry 권한, 네트워크, tag 정책 문제일 수 있다.

운영 체크리스트

  • CI의 Java version과 로컬 개발 기준이 일치하는가?
  • Gradle wrapper를 사용해 build tool version을 고정하는가?
  • unit test와 integration test 실패를 구분해 볼 수 있는가?
  • bootJar 산출물이 artifact로 저장되는가?
  • artifact 이름에 git SHA나 build number가 포함되는가?
  • container image tag와 artifact가 같은 commit을 가리키는가?
  • package 실패와 deploy 실패의 알림이 구분되는가?

장애 신호

  • compile은 성공하지만 Spring context test가 profile 누락으로 실패한다.
  • test는 통과하지만 bootJar에서 resource 중복이나 manifest 문제가 난다.
  • artifact upload가 성공했지만 파일명이 매번 같아 이전 파일과 구분되지 않는다.
  • image build는 성공했지만 registry push가 권한 오류로 실패한다.
  • cache가 깨져 dependency resolution이 랜덤하게 실패한다.
  • flaky test 때문에 pipeline rerun이 관행이 된다.

안전한 완화 조치

Build 실패는 환경 차이부터 확인한다.

JDK version, Gradle wrapper, dependency repository 상태를 본다.

Test 실패는 실패 test가 deterministic한지 먼저 확인한다.

무작정 rerun해서 초록색을 만들면 결함이 main에 들어갈 수 있다.

Package 실패는 artifact 경로, resource 포함 여부, image build context를 확인한다.

Registry push 실패는 credentials와 package permission을 확인하되 Secret 값을 로그로 찍지 않는다.

인프라 담당자와 공유할 자료

문제: main pipeline package 단계 실패
commit: abc1234
build: ./gradlew clean test bootJar 성공
image build: 성공
image push: denied, package write permission missing
영향: 배포 artifact 생성됨, registry publish 안 됨
필요 조치: deploy token scope 또는 OIDC role package write 확인

Build/Test/Package 실패는 같은 pipeline 실패라도 담당자와 조치가 다르므로 단계별 증거를 남긴다.

실전 팁

  • build tool version은 wrapper로 고정한다.
  • test report와 artifact를 pipeline 결과에 남긴다.
  • flaky test는 rerun 정책보다 원인 격리가 먼저다.
  • artifact 이름에는 commit SHA를 넣는다.
  • image push 실패를 애플리케이션 test 실패로 오해하지 않는다.

위험 신호!

  • CI 서버에 설치된 임의 Gradle을 사용한다.
  • test 실패를 계속 rerun해서 통과시키는 문화가 생긴다.
  • package artifact를 저장하지 않아 CD가 다시 빌드한다.
  • image tag가 latest뿐이라 어떤 commit인지 모른다.
  • Secret 확인을 위해 registry password를 로그에 출력한다.

확인 질문

확인 질문

  • Build와 Package 실패는 어떻게 다른가?
    • Build 실패는 코드 컴파일과 의존성 해결 문제이고, Package 실패는 검증된 코드를 배포 가능한 jar나 image로 만드는 과정의 문제다.
  • artifact 이름에 commit SHA가 필요한 이유는 무엇인가?
    • 산출물이 어떤 코드 입력에서 만들어졌는지 추적해야 rollback과 장애 분석이 가능하기 때문이다.
  • flaky test를 무시하면 왜 위험한가?
    • 실제 결함인지 환경 문제인지 구분하지 못한 채 같은 실패를 운영 변경 전 검증에서 계속 놓칠 수 있기 때문이다.

참고 문서