한 줄 정의

지속적 통합(CI)이란 코드 변경이 로컬에서 프로덕션으로 가는 길목마다 ‘무슨 테스트를 언제 돌릴지’ 를 정해, 문제를 가능한 한 일찍·자동으로 잡아내는 개발 방식입니다.

쉽게 말하면

CI는 책을 인쇄소에 넘기기 전까지의 여러 단계 교정 과 같습니다.

타이핑하다 오타가 나면 그 자리에서 고치고(로컬), 동료가 빠르게 훑고(프리서브밋), 편집자가 꼼꼼히 보고(포스트서브밋), 인쇄 직전 최종 교정쇄를 봅니다(릴리스 후보). 만약 오타가 이 모두를 뚫고 인쇄·배포된 뒤 발견되면 리콜이라는 엄청난 비용을 치릅니다(프로덕션 장애).

핵심은 둘입니다. 단계가 오른쪽으로 갈수록 같은 오류를 고치는 비용이 폭발 하고, 단계마다 검토 성격이 다릅니다(앞은 빠르게, 뒤는 철저하게). 그래서 “문제를 최대한 왼쪽에서 잡되, 빠른 테스트는 앞에 느린 테스트는 뒤에 배치한다”가 이 장의 전부입니다.

왜 중요한가?

현대 시스템에서 ‘통합할 대상’은 내 코드만이 아닙니다. 업스트림 마이크로서비스, 설정, 데이터, 런타임이 내가 잠든 사이에도 각자 일정대로 바뀝니다. 마이크로서비스 환경에선 장애 원인이 내 코드가 아니라 다른 서비스와의 네트워크 실패인 경우가 더 많습니다.

이걸 자동으로 잡지 못하면, 문제가 변경 생애의 오른쪽 끝까지 살아남아 최종 사용자 앞에서 터집니다. 그때는 코드를 모르는 엔지니어가 분류해야 하고, 작성자조차 “내가 뭘 왜 바꿨더라”를 기억해내야 하며, 피해 범위도 가장 넓습니다.

핵심 내용

코드 변경의 생애 — 비용은 오른쪽으로 갈수록 폭발한다

CI의 모든 것은 코드 변경 하나가 로컬에서 프로덕션까지 가는 하나의 화살표 위에 있습니다.

flowchart LR
    A["편집·컴파일·디버그<br/>(로컬)"] --> B["프리서브밋"]
    B --> C["포스트서브밋·헤드<br/>지속적 빌드(CB)"]
    C --> D["릴리스 후보(RC)<br/>지속적 배포(CD)"]
    D --> E["임시·스테이징 환경"]
    E --> F["프로덕션"]

오른쪽으로 갈수록 두 가지가 함께 커집니다 — 버그를 늦게 잡았을 때의 비용, 그리고 그 단계에서 돌려야 하는 테스트의 범위. 그래서 문제를 최대한 왼쪽에서 잡자는 사고방식을 테스트를 통한 원점 회귀(shift left) 라 부릅니다.

CB·CD·CT — 자동화 삼형제

CI는 빌드와 릴리스를 자동화하며, 테스트는 두 단계 모두에 따라붙습니다.

  • 지속적 빌드(CB): 최신 변경을 헤드에 통합해 자동으로 빌드+테스트합니다. 테스트도 빌드의 일부라, 컴파일을 통과해도 테스트가 깨지면 ‘빌드 실패’입니다. 그래서 헤드가 둘로 갈립니다 — 최신 커밋인 참 헤드, CB가 검증한 녹색 헤드. 엔지니어는 보통 안정된 녹색 헤드에서 작업합니다.
  • 지속적 배포(CD): 헤드에서 코드와 설정을 가져와 배포 단위인 릴리스 후보(RC) 를 만들고, 여러 환경에 차례로 승격시키며 테스트합니다.
  • 지속적 테스트(CT): 위 두 단계에 자동으로 붙는 테스트 활동입니다.

설정도 코드처럼 다뤄라

프로덕션 버그의 상당수는 코드가 아니라 설정 오타 입니다. 그래서 정적 설정은 RC에 묶어 코드와 함께 테스트하고, 코드처럼 버전 관리·리뷰해야 합니다.

무슨 테스트를 어디서?

핵심 전략은 빠르고 안정적인 테스트는 앞(프리서브밋)에서, 느리고 덜 안정적인 테스트는 뒤(포스트서브밋 이후)에서 돌리는 것입니다.

단계무슨 테스트
프리서브밋빠르고 안정적인 작은 테스트(주로 단위 테스트), 변경된 프로젝트로 한정서브밋마다 오래 기다리면 생산성이 죽음. 불안정한 테스트는 제외
포스트서브밋(CB)영향받는 모든 테스트프리서브밋이 놓친 문제를 잡음(대신 이따금 롤백)
릴리스 후보(CD)RC 전체를 검증하는 더 큰 테스트, 승격 단계마다 반복재컴파일·체리픽·비상배포 때 달라질 수 있어 매번 재검증
프로덕션RC와 동일한 스위트(프로버)프로덕션이 실제로 잘 도는지, 테스트가 적합한지 확인

프리서브밋에서 모든 걸 잡지 않는 이유는 비용(느림), 불안정성, 그리고 공중 충돌(mid-air collision) 입니다 — 내 테스트가 도는 사이 다른 사람이 무관한 파일을 바꿔 내 테스트가 깨지는 현상으로, 구글 규모에선 거의 매일 일어납니다.

같은 테스트를 단계마다 반복하는 이 구조가 곧 심층 방어(defense in depth) 입니다. 하나의 그물로는 다 못 잡으니 여러 그물을 겹칩니다.

CI는 경보와 같다

이 장의 가장 깊은 통찰은 CI를 모니터링&경보의 ‘왼쪽 버전’ 으로 보는 것입니다. 경보는 워크플로 뒤쪽(프로덕션)에서, CI는 앞쪽(개발)에서, 둘 다 ‘문제를 최대한 빨리 자동으로 발견’합니다.

이 관점이 통념을 뒤집습니다. SRE가 가동시간 100%를 목표로 삼지 않듯, CI 녹색 100%도 잘못된 목표 입니다. 무해한 경보를 끄듯 무해한 테스트 실패도 꺼도 되고, ‘CI가 녹색이 아니면 누구도 커밋 불가’ 같은 정책도 대체로 과합니다. 핵심은 테스트는 정말 중요한 조건(불변성)이 깨졌을 때만 실패해야 한다 는 것 — 시도 때도 없이 깨지는 불규칙한 테스트는 거짓 경보만큼 해롭습니다.

밀폐 테스트 — 불안정성을 줄이는 처방

밀폐 테스트(hermetic test): 외부 의존성 없이 필요한 모든 것을 갖춘 환경에서 돌리는 테스트

외부 백엔드에 의존하지 않으니 결정성(같은 코드면 항상 같은 결과)과 격리(프로덕션과 서로 영향 없음)를 얻습니다. 그래서 실패하면 원인이 최근 변경일 가능성이 커, 디버깅 범위가 좁아집니다. 만드는 방법은 가짜 서버(fake), 샌드박스(구성요소를 통째로 띄움), 기록/재생(실제 요청·응답을 녹화해 재생) 셋입니다.

비교 / 트레이드오프

기록/재생의 딜레마 — 거짓 양성 vs 거짓 음성

기록/재생은 강력하지만 테스트를 깨지기 쉽게 만듭니다.

구분무슨 일원인
거짓 양성실패해야 할 테스트가 성공잘못된 결과가 나와야 할 때도 캐시에 녹화된 정상값을 반환
거짓 음성성공해야 할 테스트가 실패녹화 데이터가 낡음 → 멀쩡한 테스트가 서브밋을 가로막음

이상적으로는 요청이 유의미하게 바뀔 때만 실패해야 하지만, 거대 시스템에서 “언제가 유의미한 변경인가”를 자동 판별하기란 매우 어렵습니다.

현실의 CI — TAP과 테이크아웃이 주는 교훈

구글은 TAP 으로 코드베이스 전체를 빌드합니다(매일 5만+ 변경, 40억+ 테스트). 실무에 바로 쓸 만한 교훈은 이렇습니다.

  • 프리서브밋 95% 통과 → 일단 서브밋 허용, 나머지는 비동기로. 완벽을 포기하는 대신 빌드 경찰(Build Cop)+자동 롤백 이라는 안전망에 베팅합니다. 가장 빠르고 안전한 복구는 거의 항상 롤백 입니다.
  • 피드백을 볼 수 있고 조치 가능하게. 테이크아웃은 해독 불가능한 로그를 깔끔한 UI+실패 링크로 바꿔 디버깅 지원 부담을 35% 줄였습니다.
  • 같은 스위트를 프로덕션에도 돌려 실패를 격리. 내 빌드 문제와 남의 마이크로서비스 문제를 저렴하게 구분합니다.
  • 즉시 못 고치는 실패는 끄지 말고 버그 태그로 추적. 그래야 녹색을 유지하면서도 잊지 않습니다.

내 생각

  • ‘테스트를 왼쪽으로 당겨라’가 이 장의 단 하나의 명제다. “이 테스트를 어느 단계에 둘까”는 결국 ‘실행 비용 vs 늦게 잡았을 때의 비용’ 비교 한 줄로 정해집니다.

  • ‘CI는 경보다’가 가장 값진 재프레이밍이다. 녹색 100%를 목표로 삼는 순간 비용이 폭발합니다. ‘오차 예산’처럼 ‘CI 실패 예산’을 두고, 무해한 실패엔 깐깐하지 않아도 된다는 발상을 파이프라인 정책에 옮길 만합니다.

  • 설정을 코드처럼 버전 관리·테스트하라는 원칙은 GitOps 그 자체다. 장애의 상당수가 config 오타라는 점을 생각하면 과소평가돼 있습니다.

  • 기록/재생 = WireMock·VCR 패턴. 거짓 양성·음성 딜레마, “언제 녹화본을 갱신할까”라는 영원한 숙제가 우리가 쓰는 도구와 똑같습니다.

관련 개념