컴퓨터공학 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 테스트 경계와 핵심 여정 확인 몇 분 ~ 수십 분
배포 스테이징 → 운영 롤백 경로가 준비돼 있어야

원칙 두 가지가 있다.

  1. 빠른 것을 앞에. 10초짜리 린트에서 걸릴 실수를 20분짜리 E2E 뒤에서 발견하면 시간 낭비다.
  2. 한 번 빌드해서 여러 번 배포. 스테이징용과 운영용 이미지를 따로 빌드하면, 스테이징에서 검증한 것과 운영에 나가는 것이 다를 수 있다. 같은 산출물에 환경별 설정만 바꿔 주입한다.

예시: 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 을 직접 칠 일이 거의 없어진다. 롤백은 매니페스트 커밋 하나를 되돌리는 것이 된다.
  • 비밀값은 파이프라인 설정이 아니라 비밀 저장소에. 워크플로 파일에 토큰을 적지 않고, 플랫폼의 시크릿 기능이나 외부 비밀 관리 도구에서 주입한다. 로그에 비밀값이 찍히지 않는지도 확인한다.

확인 문제

  1. CI 서버를 운영하지만 모든 개발자가 2주짜리 기능 브랜치에서 작업한다. 이 팀은 지속적 통합을 하고 있는가?
  2. 지속적 전달과 지속적 배포의 차이는?
  3. 파이프라인에서 린트를 E2E 테스트보다 앞에 두는 이유는?
  4. 스테이징과 운영에 같은 빌드 산출물을 써야 하는 이유는?
  5. 위 예제에서 리드 타임을 평균이 아니라 중앙값으로 본 이유는?

풀이

  1. 아니다. CI 는 도구가 아니라 적어도 하루 한 번 메인라인에 통합하고 매번 자동 검증하는 실천이다.
  2. 지속적 전달은 언제든 운영에 배포할 수 있는 상태를 유지하는 것(배포 결정은 사람이 할 수 있음)이고, 지속적 배포는 파이프라인을 통과한 모든 변경이 자동으로 운영에 배포되는 것이다.
  3. 빠르고 싼 검사를 앞에 두어 흔한 실수를 몇 초 안에 알려 주고, 비싼 단계의 시간을 아끼기 위해서다.
  4. 따로 빌드하면 스테이징에서 검증한 것과 운영에 나가는 것이 달라질 수 있다. 같은 산출물을 쓰면 검증 결과를 그대로 믿을 수 있다.
  5. 밤을 넘긴 배포처럼 하나의 큰 값이 평균을 크게 왜곡하기 때문이다. 중앙값은 이상치에 덜 민감하다.

더 읽을거리 (References)