CI/CD 파이프라인 설계: 배포 시간을 45분에서 8분으로 줄인 실전 최적화
GitHub Actions 기반 CI/CD 파이프라인에서 반복되는 설계 실수와 최적화 패턴. 캐시 전략, 병렬화, 선택적 실행으로 배포 시간 45분→8분 달성한 실전 사례.

요약
GitHub Actions 기반 CI/CD 파이프라인에서 반복되는 설계 실수와 최적화 패턴. 캐시 전략, 병렬화, 선택적 실행으로 배포 시간 45분→8분 달성한 실전 사례.
CI/CD 파이프라인 설계: 배포 시간을 45분에서 8분으로 줄인 실전 최적화
Executive Summary - Topic: GitHub Actions CI/CD 파이프라인 최적화 실전 패턴 - Target: DevOps 엔지니어, 백엔드 개발자, 플랫폼 팀 - TL;DR 1: 파이프라인 병목의 80%는 캐시 미활용과 직렬화 가능한 단계를 순차 실행하는 것 - TL;DR 2: 변경 파일 기반 선택적 CI 실행으로 monorepo에서 70% 시간 절감 가능 - TL;DR 3: 배포 시간이 10분을 넘으면 개발자 흐름(flow)이 끊기고 생산성이 급락한다
"PR을 올리고 리뷰가 시작되기 전에 CI가 끝나야 한다" — Timeware의 파이프라인 설계 원칙입니다.
이 원칙을 세운 건 45분짜리 파이프라인 때문이었습니다. 리뷰어가 CI를 기다리다 다른 일을 시작하고, CI가 끝나면 컨텍스트 스위칭 비용이 발생했습니다. 이 문제를 어떻게 해결했는지 공유합니다.
시작점: 45분짜리 파이프라인 분석
초기 파이프라인 구조:
1jobs:2 ci:3 steps:4 - checkout # 2분 (1.2GB 레포)5 - npm install # 8분 (캐시 없음)6 - lint # 3분7 - type-check # 5분8 - unit-tests # 7분9 - integration-tests # 10분10 - e2e-tests # 8분11 - build # 12분12 # 총: ~55분 (실제 45분, 겹침 없음)병목 분석:
- npm install: 매번 새로 설치 (node_modules 캐시 없음)
- 모든 단계 직렬화: lint 끝나야 type-check 시작
- 전체 레포 빌드: 변경 없는 패키지도 포함
- checkout: 전체 히스토리 (fetch-depth: 0)
최적화 1: 캐시 전략 (8분 절감)
1- name: Setup Node.js with cache2 uses: actions/setup-node@v43 with:4 node-version: 205 cache: npm # npm ci와 package-lock.json 해시 기반6 7# 또는 더 세밀한 캐시 제어8- name: Cache node_modules9 uses: actions/cache@v410 id: npm-cache11 with:12 path: |13 node_modules14 ~/.npm15 key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}16 restore-keys: |17 npm-${{ runner.os }}-18 19- name: Install dependencies20 if: steps.npm-cache.outputs.cache-hit != 'true'21 run: npm ci추가 캐시 전략:
1# Next.js 빌드 캐시2- name: Cache Next.js build3 uses: actions/cache@v44 with:5 path: |6 .next/cache7 ${{ github.workspace }}/.next/cache8 key: nextjs-${{ runner.os }}-${{ hashFiles('**/*.ts', '**/*.tsx', '**/*.css') }}9 restore-keys: nextjs-${{ runner.os }}-10 11# Playwright 브라우저 캐시12- name: Cache Playwright binaries13 uses: actions/cache@v414 with:15 path: ~/.cache/ms-playwright16 key: playwright-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}절감 효과:npm install 8분 → 캐시 히트 시 20초
최적화 2: 병렬화 (15분 절감)
1jobs:2 # 준비 단계3 setup:4 runs-on: ubuntu-latest5 steps:6 - uses: actions/checkout@v47 - uses: actions/setup-node@v48 with: { node-version: 20, cache: npm }9 - run: npm ci10 - name: Cache node_modules for parallel jobs11 uses: actions/cache/save@v412 with:13 path: node_modules14 key: nm-${{ github.sha }}15 16 # 병렬 실행 (setup 완료 후)17 lint:18 needs: setup19 runs-on: ubuntu-latest20 steps:21 - uses: actions/checkout@v422 - uses: actions/cache/restore@v423 with: { path: node_modules, key: nm-${{ github.sha }} }24 - run: npm run lint25 26 type-check:27 needs: setup28 runs-on: ubuntu-latest29 steps:30 - uses: actions/checkout@v431 - uses: actions/cache/restore@v432 with: { path: node_modules, key: nm-${{ github.sha }} }33 - run: npm run type-check34 35 unit-test:36 needs: setup37 runs-on: ubuntu-latest38 steps:39 - uses: actions/checkout@v440 - uses: actions/cache/restore@v441 with: { path: node_modules, key: nm-${{ github.sha }} }42 - run: npm run test:unit43 44 # 단위 테스트 통과 후 통합 테스트45 integration-test:46 needs: [lint, type-check, unit-test]47 runs-on: ubuntu-latest48 services:49 postgres:50 image: postgres:17-alpine51 env: { POSTGRES_PASSWORD: test }52 options: --health-cmd pg_isready53 steps:54 - run: npm run test:integration55 56 # 모든 테스트 통과 후 빌드57 build:58 needs: integration-test59 runs-on: ubuntu-latest60 steps:61 - run: npm run build병렬화 효과:
1이전: lint(3m) + type-check(5m) + unit-test(7m) = 15분 순차2이후: max(lint, type-check, unit-test) = 7분 병렬3절감: 8분최적화 3: 선택적 CI 실행 (Monorepo)
monorepo에서 변경하지 않은 패키지까지 테스트하는 비효율 제거:
1jobs:2 changes:3 runs-on: ubuntu-latest4 outputs:5 frontend: ${{ steps.filter.outputs.frontend }}6 backend: ${{ steps.filter.outputs.backend }}7 shared: ${{ steps.filter.outputs.shared }}8 steps:9 - uses: actions/checkout@v410 - uses: dorny/paths-filter@v311 id: filter12 with:13 filters: |14 frontend:15 - 'apps/web/**'16 - 'packages/ui/**'17 backend:18 - 'apps/api/**'19 - 'packages/db/**'20 shared:21 - 'packages/shared/**'22 - 'package.json'23 24 test-frontend:25 needs: changes26 if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.shared == 'true'27 runs-on: ubuntu-latest28 steps:29 - run: npm run test:frontend30 31 test-backend:32 needs: changes33 if: needs.changes.outputs.backend == 'true' || needs.changes.outputs.shared == 'true'34 runs-on: ubuntu-latest35 steps:36 - run: npm run test:backend효과:
- 프론트엔드 변경 시: 백엔드 테스트 스킵 → 7분 절감
- 백엔드 변경 시: E2E 테스트 조건부 실행
- 문서 변경 시: CI 전체 스킵 (lint만)
최적화 4: 테스트 샤딩
E2E 테스트를 여러 러너에 분산:
1e2e-test:2 needs: build3 runs-on: ubuntu-latest4 strategy:5 matrix:6 shard: [1, 2, 3, 4] # 4개 러너 병렬7 steps:8 - name: Run E2E tests (shard ${{ matrix.shard }}/4)9 run: |10 npx playwright test \11 --shard=${{ matrix.shard }}/4 \12 --reporter=blob13 14 - name: Upload shard results15 uses: actions/upload-artifact@v416 with:17 name: playwright-blob-${{ matrix.shard }}18 path: blob-report/19 20merge-reports:21 needs: e2e-test22 runs-on: ubuntu-latest23 steps:24 - name: Download all shard results25 uses: actions/download-artifact@v426 with:27 pattern: playwright-blob-*28 merge-multiple: true29 path: all-blobs/30 31 - name: Merge reports32 run: npx playwright merge-reports --reporter=html all-blobs/효과: E2E 8분 → 2분 (4 샤드 병렬)
최적화 5: Checkout 최적화
1# 전체 히스토리 불필요한 경우2- uses: actions/checkout@v43 with:4 fetch-depth: 1 # 최신 커밋만 (기본값 0 = 전체)5 # 1.2GB 레포 → 2분에서 30초로6 7# PR의 변경 파일만 분석할 때8- uses: actions/checkout@v49 with:10 fetch-depth: 0 # 비교를 위해 전체 필요최종 결과
1초기: checkout(2m) → install(8m) → lint(3m) → type-check(5m)2 → unit-test(7m) → integration(10m) → e2e(8m)3 → build(12m) = 총 45분4 5최적화 후:6[checkout(30s) + cache restore(20s)] → 병렬: [lint(3m) | type-check(5m) | unit-test(7m)]7 → integration-test(10m)8 → [e2e 샤드 ×4(2m) | build(4m)]9총: 약 8분10 11절감: 37분 (82%)CI/CD 품질 지표
파이프라인 건강도를 측정하는 지표들:
1# GitHub Actions 내장 메트릭 수집2- name: Record pipeline metrics3 run: |4 echo "pipeline_duration=${{ job.duration }}" >> $GITHUB_ENV5 # Datadog, Grafana 등으로 전송6 curl -X POST https://metrics.timeware.kr/ci \7 -d "duration=${{ job.duration }}&branch=${{ github.ref_name }}"목표 지표 (2026년 기준):
- P50 파이프라인 시간: < 10분
- P99 파이프라인 시간: < 15분
- 배포 실패율: < 2%
- MTTR (평균 복구 시간): < 30분
FAQ
Q. GitHub Actions의 병렬 job 비용이 늘어나지 않나요? A. 병렬 job은 각각 별도 러너를 사용하므로 총 계산 시간은 비슷하거나 더 많을 수 있습니다. 하지만 GitHub Actions는 wall-clock time(실제 경과 시간)이 아닌 compute minutes로 과금합니다. 병렬화는 개발자 대기 시간을 줄이는 것이 목적이며, 비용은 비슷하거나 캐시 효율로 오히려 줄 수 있습니다.
Q. secrets와 environment variables를 안전하게 관리하는 방법은? A. GitHub Environments를 사용하세요. production 환경은 보호 규칙(필수 리뷰어)과 함께 별도 secrets 세트를 갖습니다. secrets.PROD_DB_URL은 production environment에만 주입되고, PR 빌드에는 staging secrets만 사용됩니다.
Q. 배포 롤백 전략은 어떻게 설계해야 하나요? A. 블루/그린 배포와 feature flag를 조합하는 것이 가장 안전합니다. 즉각적 롤백이 필요하면: 1) git revert + 즉시 배포, 2) 이전 Docker 이미지 태그로 재배포, 3) DB 마이그레이션이 포함된 경우 반드시 역방향 마이그레이션(rollback migration)을 미리 준비해두세요.