TimewareTimeware
블로그 목록으로
블로그

CI/CD 파이프라인 설계: 배포 시간을 45분에서 8분으로 줄인 실전 최적화

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

2026년 3월 5일Timeware Engineeringcicdgithub-actionsdevopsautomationperformance
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분짜리 파이프라인 분석

초기 파이프라인 구조:

yaml
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분, 겹침 없음)

병목 분석:

  1. npm install: 매번 새로 설치 (node_modules 캐시 없음)
  2. 모든 단계 직렬화: lint 끝나야 type-check 시작
  3. 전체 레포 빌드: 변경 없는 패키지도 포함
  4. checkout: 전체 히스토리 (fetch-depth: 0)

최적화 1: 캐시 전략 (8분 절감)

yaml
1- name: Setup Node.js with cache
2 uses: actions/setup-node@v4
3 with:
4 node-version: 20
5 cache: npm # npm ci와 package-lock.json 해시 기반
6
7# 또는 더 세밀한 캐시 제어
8- name: Cache node_modules
9 uses: actions/cache@v4
10 id: npm-cache
11 with:
12 path: |
13 node_modules
14 ~/.npm
15 key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
16 restore-keys: |
17 npm-${{ runner.os }}-
18
19- name: Install dependencies
20 if: steps.npm-cache.outputs.cache-hit != 'true'
21 run: npm ci

추가 캐시 전략:

yaml
1# Next.js 빌드 캐시
2- name: Cache Next.js build
3 uses: actions/cache@v4
4 with:
5 path: |
6 .next/cache
7 ${{ github.workspace }}/.next/cache
8 key: nextjs-${{ runner.os }}-${{ hashFiles('**/*.ts', '**/*.tsx', '**/*.css') }}
9 restore-keys: nextjs-${{ runner.os }}-
10
11# Playwright 브라우저 캐시
12- name: Cache Playwright binaries
13 uses: actions/cache@v4
14 with:
15 path: ~/.cache/ms-playwright
16 key: playwright-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}

절감 효과:npm install 8분 → 캐시 히트 시 20초

최적화 2: 병렬화 (15분 절감)

yaml
1jobs:
2 # 준비 단계
3 setup:
4 runs-on: ubuntu-latest
5 steps:
6 - uses: actions/checkout@v4
7 - uses: actions/setup-node@v4
8 with: { node-version: 20, cache: npm }
9 - run: npm ci
10 - name: Cache node_modules for parallel jobs
11 uses: actions/cache/save@v4
12 with:
13 path: node_modules
14 key: nm-${{ github.sha }}
15
16 # 병렬 실행 (setup 완료 후)
17 lint:
18 needs: setup
19 runs-on: ubuntu-latest
20 steps:
21 - uses: actions/checkout@v4
22 - uses: actions/cache/restore@v4
23 with: { path: node_modules, key: nm-${{ github.sha }} }
24 - run: npm run lint
25
26 type-check:
27 needs: setup
28 runs-on: ubuntu-latest
29 steps:
30 - uses: actions/checkout@v4
31 - uses: actions/cache/restore@v4
32 with: { path: node_modules, key: nm-${{ github.sha }} }
33 - run: npm run type-check
34
35 unit-test:
36 needs: setup
37 runs-on: ubuntu-latest
38 steps:
39 - uses: actions/checkout@v4
40 - uses: actions/cache/restore@v4
41 with: { path: node_modules, key: nm-${{ github.sha }} }
42 - run: npm run test:unit
43
44 # 단위 테스트 통과 후 통합 테스트
45 integration-test:
46 needs: [lint, type-check, unit-test]
47 runs-on: ubuntu-latest
48 services:
49 postgres:
50 image: postgres:17-alpine
51 env: { POSTGRES_PASSWORD: test }
52 options: --health-cmd pg_isready
53 steps:
54 - run: npm run test:integration
55
56 # 모든 테스트 통과 후 빌드
57 build:
58 needs: integration-test
59 runs-on: ubuntu-latest
60 steps:
61 - run: npm run build

병렬화 효과:

code
1이전: lint(3m) + type-check(5m) + unit-test(7m) = 15분 순차
2이후: max(lint, type-check, unit-test) = 7분 병렬
3절감: 8분

최적화 3: 선택적 CI 실행 (Monorepo)

monorepo에서 변경하지 않은 패키지까지 테스트하는 비효율 제거:

yaml
1jobs:
2 changes:
3 runs-on: ubuntu-latest
4 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@v4
10 - uses: dorny/paths-filter@v3
11 id: filter
12 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: changes
26 if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.shared == 'true'
27 runs-on: ubuntu-latest
28 steps:
29 - run: npm run test:frontend
30
31 test-backend:
32 needs: changes
33 if: needs.changes.outputs.backend == 'true' || needs.changes.outputs.shared == 'true'
34 runs-on: ubuntu-latest
35 steps:
36 - run: npm run test:backend

효과:

  • 프론트엔드 변경 시: 백엔드 테스트 스킵 → 7분 절감
  • 백엔드 변경 시: E2E 테스트 조건부 실행
  • 문서 변경 시: CI 전체 스킵 (lint만)

최적화 4: 테스트 샤딩

E2E 테스트를 여러 러너에 분산:

yaml
1e2e-test:
2 needs: build
3 runs-on: ubuntu-latest
4 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=blob
13
14 - name: Upload shard results
15 uses: actions/upload-artifact@v4
16 with:
17 name: playwright-blob-${{ matrix.shard }}
18 path: blob-report/
19
20merge-reports:
21 needs: e2e-test
22 runs-on: ubuntu-latest
23 steps:
24 - name: Download all shard results
25 uses: actions/download-artifact@v4
26 with:
27 pattern: playwright-blob-*
28 merge-multiple: true
29 path: all-blobs/
30
31 - name: Merge reports
32 run: npx playwright merge-reports --reporter=html all-blobs/

효과: E2E 8분 → 2분 (4 샤드 병렬)

최적화 5: Checkout 최적화

yaml
1# 전체 히스토리 불필요한 경우
2- uses: actions/checkout@v4
3 with:
4 fetch-depth: 1 # 최신 커밋만 (기본값 0 = 전체)
5 # 1.2GB 레포 → 2분에서 30초로
6
7# PR의 변경 파일만 분석할 때
8- uses: actions/checkout@v4
9 with:
10 fetch-depth: 0 # 비교를 위해 전체 필요

최종 결과

code
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 품질 지표

파이프라인 건강도를 측정하는 지표들:

yaml
1# GitHub Actions 내장 메트릭 수집
2- name: Record pipeline metrics
3 run: |
4 echo "pipeline_duration=${{ job.duration }}" >> $GITHUB_ENV
5 # 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)을 미리 준비해두세요.