[CS300 #196] CI/CD — 통합과 배포를 사람 대신 파이프라인에 맡기기
컴퓨터공학 300 주제 시리즈의 196번째 글이다. 전체 지도는 여기.
한 줄 요약
지속적 통합(CI)은 모두가 자기 변경을 공유 코드베이스에 자주 합치고, 합칠 때마다 자동 빌드와 테스트로 검증하는 실천이다. 지속적 전달(CD)은 거기서 한 걸음 더 나가 코드가 언제든 운영에 배포할 수 있는 상태를 유지하는 것이고, 지속적 배포는 그 배포까지 자동으로 하는 것이다.
왜 필요한가
통합을 미루면 고통이 쌓인다. 다섯 명이 각자 한 달 동안 브랜치에서 작업하다가 릴리스 직전에 합치면, 충돌을 푸는 데만 며칠이 걸리고 합친 결과는 아무도 테스트해 본 적 없는 코드가 된다. 이것을 흔히 “통합 지옥” 이라고 부른다.
배포도 마찬가지다. 배포가 석 달에 한 번이면 배포 절차는 위키 문서와 담당자의 기억에만 남아 있고, 매번 무언가를 빠뜨린다. 한 번에 나가는 변경이 많으니 문제가 생겨도 원인을 찾기 어렵다.
CI/CD 의 답은 역설적이다. 아픈 일은 더 자주 해서 작게 만든다. 매일 통합하면 충돌이 작다. 매일 배포하면 배포 절차가 자동화될 수밖에 없고, 한 번에 나가는 변경이 작아 문제의 원인이 금방 보인다.
핵심 개념
지속적 통합(Continuous Integration)
Martin Fowler 의 Continuous Integration 글은 CI 를, 팀의 각 구성원이 적어도 하루에 한 번 자기 변경을 동료들의 변경과 함께 코드베이스에 합치고, 각 통합을 테스트를 포함한 자동 빌드로 검증하는 실천으로 설명한다. 같은 글이 드는 핵심 실천 중 일부는 다음과 같다.
- 모든 것을 버전 관리 저장소에 둔다(코드, 빌드 스크립트, 설정).
- 빌드를 자동화하고, 빌드 안에 테스트를 넣는다(self-testing build).
- 모두가 매일 메인라인에 커밋한다.
- 깨진 빌드는 즉시 고친다. 깨진 빌드 위에 다른 커밋을 쌓지 않는다.
- 빌드를 빠르게 유지한다.
중요한 점은 CI 는 도구가 아니라 실천이라는 것이다. CI 서버를 돌려도 각자 2주짜리 브랜치에서 작업하면 지속적 통합이 아니다. 앞 글의 트렁크 기반 개발과 CI 가 짝을 이루는 이유다.
지속적 전달과 지속적 배포
커밋 → 빌드 → 단위 테스트 → 통합 테스트 → 스테이징 배포 → [승인] → 운영 배포
└──────────── CI ────────────┘
└──────────────────── 지속적 전달 ───────────────────────┘ (운영 배포는 버튼 하나)
└──────────────────── 지속적 배포 ──────────────────────────────────┘ (버튼도 자동)
Fowler 의 ContinuousDelivery 설명에 따르면, 지속적 전달은 소프트웨어를 언제든 운영에 내보낼 수 있도록 만드는 규율이고, 지속적 배포는 모든 변경이 파이프라인을 통과하면 자동으로 운영에 나가는 것이다. 지속적 배포를 하려면 지속적 전달이 되어 있어야 하지만, 그 반대는 아니다. 규제나 사업상 이유로 배포 시점을 사람이 정해야 하는 조직은 지속적 전달까지만 한다.
파이프라인의 구성
| 단계 | 목적 | 실패하면 |
|---|---|---|
| 정적 검사(lint, 타입) | 빠르게 걸러지는 실수 | 몇 초 안에 피드백 |
| 단위 테스트 | 로직 검증 | 수십 초 ~ 몇 분 |
| 빌드·패키징 | 배포 산출물(컨테이너 이미지 등) 생성 | 한 번 만든 산출물을 모든 환경에서 재사용 |
| 통합·E2E 테스트 | 경계와 핵심 여정 확인 | 몇 분 ~ 수십 분 |
| 배포 | 스테이징 → 운영 | 롤백 경로가 준비돼 있어야 |
원칙 두 가지가 있다.
- 빠른 것을 앞에. 10초짜리 린트에서 걸릴 실수를 20분짜리 E2E 뒤에서 발견하면 시간 낭비다.
- 한 번 빌드해서 여러 번 배포. 스테이징용과 운영용 이미지를 따로 빌드하면, 스테이징에서 검증한 것과 운영에 나가는 것이 다를 수 있다. 같은 산출물에 환경별 설정만 바꿔 주입한다.
예시: GitHub Actions 워크플로
GitHub Actions 워크플로 문법으로 위 파이프라인의 앞부분을 쓰면 이런 모양이다.
name: ci
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -r requirements.txt
- run: ruff check .
- run: pytest -q
image:
needs: test # test 가 성공해야 실행
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker build -t app:${{ github.sha }} .
needs 가 단계 순서와 실패 시 중단을 표현한다. 이미지 태그를 커밋 해시(github.sha)로 붙이면 “운영에 나간 이미지가 정확히 어느 커밋인가” 가 이름에 남는다.
무엇을 재는가: DORA 지표
DORA(DevOps Research and Assessment) 연구 프로그램은 소프트웨어 전달 성과를 재는 지표를 정리해 왔다. 현재 DORA 가이드는 원래의 네 가지 핵심 지표에서 다섯 가지 지표 모델로 바뀌었다고 설명한다.
| 묶음 | 지표 | 뜻 |
|---|---|---|
| 처리량 | 변경 리드 타임 | 커밋이 운영에 배포되기까지 걸리는 시간 |
| 처리량 | 배포 빈도 | 기간당 배포 횟수 |
| 처리량 | 실패한 배포 복구 시간 | 즉각 조치가 필요한 배포 실패에서 복구하기까지의 시간 |
| 불안정성 | 변경 실패율 | 배포 직후 즉각 조치(롤백, 핫픽스)가 필요했던 배포의 비율 |
| 불안정성 | 배포 재작업률 | 운영 사고 때문에 계획에 없이 이루어진 배포의 비율 |
같은 가이드는 DORA 연구가 속도와 안정성이 맞바꾸는 관계가 아니라는 것을 반복해서 보여 줬다고 쓴다. 대부분의 팀에서 두 지표군은 함께 좋거나 함께 나쁘다. 그리고 지표 자체를 목표로 삼으면 사람들이 지표를 조작하기 시작한다는 함정(굿하트의 법칙)도 경고한다.
직접 해 보기
아주 작은 파이프라인 실행기로 “앞 단계가 실패하면 뒤 단계는 돌지 않는다” 를 확인하고, 배포 기록에서 DORA 지표 일부를 계산해 본다.
from datetime import datetime
from statistics import median
# ----- 1) 아주 작은 파이프라인 실행기: 앞 단계가 실패하면 뒤 단계는 돌지 않는다 -----
def lint(c): return "print(" not in c["diff"]
def unit(c): return c["tests_pass"]
def build(c): return True
def deploy(c): return True
PIPELINE = [("lint", lint), ("unit-test", unit), ("build-image", build), ("deploy", deploy)]
def run(commit):
for name, step in PIPELINE:
ok = step(commit)
print(f" {commit['sha']} {name:12s} {'ok' if ok else 'FAIL'}")
if not ok:
return False
return True
commits = [
{"sha": "a1c3", "diff": "x = 1", "tests_pass": True},
{"sha": "b7e2", "diff": "print(secret)", "tests_pass": True},
{"sha": "c9d0", "diff": "y = 2", "tests_pass": False},
]
for c in commits:
print(" =>", "배포됨" if run(c) else "중단")
# ----- 2) 배포 기록에서 DORA 지표 일부를 계산한다 -----
T = lambda s: datetime.fromisoformat(s)
deploys = [ # (커밋 시각, 배포 시각, 배포 직후 조치가 필요했는가)
(T("2026-10-05T10:00"), T("2026-10-05T11:30"), False),
(T("2026-10-06T09:00"), T("2026-10-06T09:40"), False),
(T("2026-10-06T15:00"), T("2026-10-07T10:00"), True),
(T("2026-10-08T13:00"), T("2026-10-08T13:25"), False),
(T("2026-10-09T16:00"), T("2026-10-09T16:50"), False),
]
lead = [d - c for c, d, _ in deploys]
days = (deploys[-1][1].date() - deploys[0][1].date()).days + 1
print(f"변경 리드 타임(중앙값): {median(lead)}")
print(f"배포 빈도: {len(deploys)}회 / {days}일")
print(f"변경 실패율: {sum(f for *_, f in deploys) / len(deploys):.0%}")
실행 결과:
a1c3 lint ok
a1c3 unit-test ok
a1c3 build-image ok
a1c3 deploy ok
=> 배포됨
b7e2 lint FAIL
=> 중단
c9d0 lint ok
c9d0 unit-test FAIL
=> 중단
변경 리드 타임(중앙값): 0:50:00
배포 빈도: 5회 / 5일
변경 실패율: 20%
b7e2 는 린트에서 걸려 테스트도 돌지 않았다. 몇 초 만에 피드백을 받은 것이다. 리드 타임은 평균이 아니라 중앙값으로 봤다. 10월 6일 오후 커밋처럼 밤을 넘긴 하나가 평균을 크게 끌어올리기 때문이다. 이런 지표는 팀의 개선 추세를 보는 데 쓰고, 팀끼리 순위를 매기는 데 쓰지 않는다.
현업에서는
- 깨진
main은 최우선 순위다. 빌드가 깨진 채로 다른 커밋이 쌓이면 무엇이 깨뜨렸는지 가리기 어려워진다. 많은 팀이 “깨뜨린 사람이 바로 고치거나, 못 고치면 되돌린다” 를 규칙으로 둔다. - 파이프라인 속도도 관리 대상이다. CI 가 40분 걸리면 개발자는 결과를 기다리지 않고 다른 일을 하다가 맥락을 잃는다. 테스트 병렬화, 의존성 캐시, 변경된 부분만 테스트하기로 시간을 줄인다.
- 홈랩에서도 같은 구조가 된다. 블로그나 사이드 프로젝트를 홈랩 k3s 클러스터에 올릴 때, 푸시 → 테스트 → 이미지 빌드(커밋 해시 태그) → 레지스트리 푸시 → 매니페스트의 이미지 태그 갱신 → GitOps 도구가 클러스터에 반영, 순서로 이어 두면 사람이
kubectl을 직접 칠 일이 거의 없어진다. 롤백은 매니페스트 커밋 하나를 되돌리는 것이 된다. - 비밀값은 파이프라인 설정이 아니라 비밀 저장소에. 워크플로 파일에 토큰을 적지 않고, 플랫폼의 시크릿 기능이나 외부 비밀 관리 도구에서 주입한다. 로그에 비밀값이 찍히지 않는지도 확인한다.
확인 문제
- CI 서버를 운영하지만 모든 개발자가 2주짜리 기능 브랜치에서 작업한다. 이 팀은 지속적 통합을 하고 있는가?
- 지속적 전달과 지속적 배포의 차이는?
- 파이프라인에서 린트를 E2E 테스트보다 앞에 두는 이유는?
- 스테이징과 운영에 같은 빌드 산출물을 써야 하는 이유는?
- 위 예제에서 리드 타임을 평균이 아니라 중앙값으로 본 이유는?
풀이
- 아니다. CI 는 도구가 아니라 적어도 하루 한 번 메인라인에 통합하고 매번 자동 검증하는 실천이다.
- 지속적 전달은 언제든 운영에 배포할 수 있는 상태를 유지하는 것(배포 결정은 사람이 할 수 있음)이고, 지속적 배포는 파이프라인을 통과한 모든 변경이 자동으로 운영에 배포되는 것이다.
- 빠르고 싼 검사를 앞에 두어 흔한 실수를 몇 초 안에 알려 주고, 비싼 단계의 시간을 아끼기 위해서다.
- 따로 빌드하면 스테이징에서 검증한 것과 운영에 나가는 것이 달라질 수 있다. 같은 산출물을 쓰면 검증 결과를 그대로 믿을 수 있다.
- 밤을 넘긴 배포처럼 하나의 큰 값이 평균을 크게 왜곡하기 때문이다. 중앙값은 이상치에 덜 민감하다.
더 읽을거리 (References)
- Martin Fowler, Continuous Integration
- Martin Fowler, ContinuousDelivery
- DORA, DORA’s software delivery performance metrics
- GitHub Docs, Workflow syntax for GitHub Actions
- Jez Humble, David Farley, Continuous Delivery: Reliable Software Releases through Build, Test, and Deployment Automation, Addison-Wesley, 2010 (서지 정보)