사전 지속적 통합
패턴

지속적 통합

gabury1

팀원 각자가 만든 변경을 하루에 최소 한 번 공용 코드베이스에 합치기로 한 방식입니다. 합칠 때마다 빌드와 테스트가 자동으로 돕니다. 합치는 간격이 짧아야 충돌이 몇 시간 안에 드러납니다.

상세

지속적 통합(Continuous Integration, 줄여서 CI)은 팀의 각 구성원이 자기 변경을 동료들의 변경과 함께 코드베이스에 최소한 매일 합치는 개발 실천이라고 Martin Fowler 는 적습니다. 이 통합 하나하나는 자동 빌드로 검증됩니다. 그 빌드에는 테스트가 포함됩니다. 목적은 통합 오류를 가능한 한 이르게 잡아내는 것입니다. Fowler 는 팀들이 이 방식으로 전달 지연의 위험이 줄고 통합에 드는 노력이 줄어든다고 말한다고 적습니다.

「자주」가 얼마나 자주인지는 원전이 못 박아 둡니다. Fowler 는 Kent Beck 의 말을 그대로 인용합니다. 어떤 코드도 두어 시간 넘게 통합되지 않은 채 앉아 있지 않는다.

개발자가 메인라인에 커밋하기 위한 전제 조건은 하나라고 Fowler 는 적습니다. 자기 코드를 정확히 빌드할 수 있어야 한다는 것입니다. 여기에는 빌드 테스트 통과가 포함됩니다. 순서는 다른 커밋 주기와 같습니다. 먼저 자기 작업 사본을 메인라인에 맞춰 갱신합니다. 메인라인과의 충돌을 해소합니다. 자기 기계에서 빌드합니다. 빌드가 통과하면 그때 메인라인으로 푸시합니다.

sequenceDiagram
    participant 개발자
    participant 메인라인
    participant 빌드
    개발자->>메인라인: 최신 변경 받아오기
    개발자->>개발자: 충돌 해소 후 로컬 빌드
    개발자->>메인라인: 푸시
    메인라인->>빌드: 통합 빌드와 테스트
    빌드-->>개발자: 결과

이 왕복이 하루에 한 번 이상 돕니다. 모두가 메인라인에 자주 푸시하면 두 개발자 사이의 충돌이 곧 드러난다고 Fowler 는 적습니다. 문제를 곧바로 고치는 열쇠는 곧바로 찾아내는 것입니다. 몇 시간마다 커밋하면 충돌이 생긴 지 몇 시간 안에 감지될 수 있고, 그 시점에는 일어난 일이 많지 않아 해소하기 쉽습니다. 몇 주 동안 감지되지 않은 충돌은 해소하기가 매우 어려울 수 있습니다.

푸시된 뒤에 도는 것은 자동화된 빌드입니다. 소스 코드를 돌아가는 시스템으로 바꾸는 일은 컴파일, 파일 옮기기, 데이터베이스에 스키마 적재 같은 것이 얽힌 복잡한 절차인 경우가 많다고 Fowler 는 적습니다. 그래도 이 부분의 다른 작업들처럼 자동화할 수 있고, 그 결과 자동화해야 한다고 적습니다. 사람에게 낯선 명령을 입력하게 하거나 대화상자를 눌러 나가게 하는 것은 시간 낭비이자 실수가 자라는 자리라고 적습니다.

그 빌드는 스스로 테스트합니다. 전통적으로 빌드는 컴파일과 링크, 그리고 프로그램을 실행 가능하게 만드는 부수 작업을 뜻했다고 Fowler 는 적습니다. 프로그램이 돌아간다고 해서 올바른 일을 한다는 뜻은 아닙니다. 현대의 정적 타입 언어가 많은 버그를 잡을 수 있지만 그 그물을 빠져나가는 것이 훨씬 많다고 적습니다. 수동 테스트는 변경 빈도를 감당하기에 너무 더딥니다. 그래서 버그가 애초에 제품에 들어가지 않게 해야 하고, 그 주된 기법이 통합 전마다 돌리는 포괄적인 테스트 스위트라고 적습니다. Fowler 는 이것을 자체 테스트 코드라고 부릅니다.

대가

장수 브랜치를 포기해야 합니다. 트렁크 기반 개발 쪽은 이 규율을 이렇게 적습니다. 개발자들이 트렁크라는 단일 브랜치에서 협업하고, 문서화된 기법을 써서 다른 장수 개발 브랜치를 만들라는 압력에 저항한다. 같은 문서는 팀 개인들이 하루에 여러 번 트렁크에 커밋하면 모든 팀원이 최소 24시간에 한 번 트렁크에 커밋한다는 지속적 통합의 핵심 요구를 만족시키기 쉬워진다고 적습니다. 이 서술은 Paul Hammant 의 https://trunkbaseddevelopment.com/ 에 있습니다.

그래서 미완성 코드가 릴리스에 실립니다. 지속적 통합은 조금이라도 진전이 있고 빌드가 건강하면 곧바로 통합하는 것을 뜻한다고 Fowler 는 적습니다. 이는 사용자에게 보이는 기능이 온전히 갖춰지기 전에 통합하는 일을 자주 낳습니다. 릴리스에 들어간 미완성 기능의 코드, 곧 잠복 코드를 어떻게 다룰지 따로 정해야 합니다. 지속적 통합을 하는 팀은 메인라인으로 보내는 모든 코드가 프로덕션 품질이도록, 그리고 그 코드를 검증하는 테스트가 함께 가도록 보장한다고 적습니다. 잠복 코드는 프로덕션에서 한 번도 실행되지 않을 수 있습니다. 그렇다고 테스트에서 돌지 않는 것은 아닙니다.

잠복 코드를 감추는 장치가 따로 붙습니다. 키스톤 인터페이스는 새 기능으로 가는 경로를 여는 인터페이스를 코드베이스에 맨 마지막으로 추가하는 방법입니다. 다크 런칭은 사용자에게 보이기 전에 프로덕션에서 일부 변경을 시험하는 방법입니다. 성능에 미치는 영향을 가늠하는 데 쓸모가 있다고 적습니다. 키스톤으로 안 되는 경우에는 기능 플래그를 씁니다. 플래그는 환경의 일부로 설정되며, 환경별 설정 파일에 두기도 합니다. 브랜치 바이 앱스트랙션은 바뀌는 모듈들에 내부 인터페이스를 만들어 옛 로직과 새 로직 사이를 오가게 하는 기법입니다.

이 장치들이 복잡도를 들여옵니다. Pete Hodgson 은 기능 토글이 복잡도를 끌어들인다고 적습니다. 영리한 토글 구현 관행과 적절한 도구로 그 복잡도를 붙잡아 둘 수 있지만, 시스템 안의 토글 개수 자체를 억제하는 것도 목표로 삼아야 한다고 적습니다. Fowler 도 기능이 완전히 릴리스되면 이 로직을 곧바로 걷어내서 플래그가 코드베이스를 어지럽히지 않게 한다고 적습니다.

빌드 시간을 계속 관리해야 합니다. 지속적 통합의 요점 전부가 신속한 피드백을 주는 것이라고 Fowler 는 적습니다. 오래 걸리는 빌드만큼 지속적 통합의 피를 빨아먹는 것은 없다고 적습니다. 자기 동료 대부분은 한 시간 걸리는 빌드를 전혀 말이 안 되는 것으로 여긴다고 적습니다. 대부분의 프로젝트에서는 익스트림 프로그래밍의 십 분 빌드 지침이 무리 없는 범위라고 적습니다. 자신들의 현대적인 프로젝트 대부분이 이를 달성한다고 적습니다. 빌드 시간에서 깎아낸 1분은 개발자마다 커밋할 때마다 아끼는 1분입니다. 지속적 통합이 잦은 커밋을 요구하므로 이것이 크게 쌓인다고 적습니다.

예시

커밋마다 도는 파이프라인을 정의 파일 한 벌로 적습니다. 세 제품의 최소 형태가 각각 이렇습니다.

GitHub Actions

YAML
name: Node.js CI
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v6
    - name: Use Node.js
      uses: actions/setup-node@v7
      with:
        node-version: '20.x'
    - run: npm ci
    - run: npm run build --if-present
    - run: npm test

on: [push] 가 방아쇠입니다. 푸시가 들어올 때마다 이 워크플로가 돕니다. runs-on 이 잡을 돌릴 러너를 ubuntu-latest 로 정합니다. actions/checkout@v6 이 코드를 받아오고 actions/setup-node@v7 이 20.x 판 Node.js 를 준비합니다. 마지막 세 줄이 의존성 설치와 빌드와 테스트입니다. GitHub 문서는 이것을 Node.js 한 판만으로 빌드하고 테스트하는 형태로 적습니다. 원문은 https://docs.github.com/en/actions/tutorials/build-and-test-code/nodejs 에 있습니다.

GitLab CI

YAML
build-job:
  stage: build
  script:
    - echo "Hello, $GITLAB_USER_LOGIN!"

test-job1:
  stage: test
  script:
    - echo "This job tests something"

test-job2:
  stage: test
  script:
    - echo "This job tests something, but takes more time than test-job1."
    - echo "After the echo commands complete, it runs the sleep command for 20 seconds"
    - echo "which simulates a test that runs 20 seconds longer than test-job1"
    - sleep 20

deploy-prod:
  stage: deploy
  script:
    - echo "This job deploys something from the $CI_COMMIT_BRANCH branch."
  environment: production

GitLab 문서는 이 예제가 build-job · test-job1 · test-job2 · deploy-prod 네 개의 잡을 보여준다고 적습니다. 잡마다 stage 로 자기가 속한 단계를 적습니다. test 단계에는 잡이 둘 들어 있습니다. 문서는 test-job2 의 sleep 20 이 test-job1 보다 20초 오래 걸리는 테스트를 흉내 내는 것이라고 적습니다. 원문은 https://docs.gitlab.com/ci/quick_start/ 에 있습니다.

Jenkinsfile

groovy
pipeline {
    agent any
    stages {
        stage('Build') {
            steps {
                echo 'Building..'
            }
        }
        stage('Test') {
            steps {
                echo 'Testing..'
            }
        }
        stage('Deploy') {
            steps {
                echo 'Deploying....'
            }
        }
    }
}

Jenkins 문서는 이것을 기본적인 3단계 지속적 전달 파이프라인을 구현한 파이프라인이라고 적습니다. stages 안에 Build · Test · Deploy 세 스테이지가 순서대로 적혀 있습니다. 스테이지마다 steps 가 그 단계에서 실행할 것을 담습니다. 앞의 두 파일이 설정 형식인 데 견주어 이것은 선언형 파이프라인 문법으로 적힌 파일입니다. 원문은 https://www.jenkins.io/doc/book/pipeline/jenkinsfile/ 에 있습니다.

실패

빌드가 깨진 채로 남으면 성립하지 않습니다. 지속적 통합은 메인라인이 건강한 상태로 유지될 때만 돌아간다고 Fowler 는 적습니다. 통합 빌드가 실패하면 곧바로 고쳐야 합니다. Kent Beck 의 말을 인용합니다. 빌드를 고치는 일보다 우선순위가 높은 작업을 가진 사람은 아무도 없다. 팀 전원이 하던 일을 멈춰야 한다는 뜻은 아니라고 적습니다. 보통은 두어 명이면 다시 돌아가게 만들 수 있습니다. 다만 빌드 수정을 긴급하고 우선순위 높은 작업으로 의식적으로 앞세운다는 뜻이라고 적습니다.

고치는 방법으로 Fowler 가 먼저 대는 것은 되돌리기입니다. 메인라인에서 마지막 커밋을 되돌려 마지막으로 알려진 정상 빌드 상태로 시스템을 돌려놓는 것입니다. 원인이 즉시 분명하면 새 커밋으로 바로 고칠 수 있습니다. 그렇지 않으면 메인라인을 되돌려 두고 별도 개발 환경에서 문제를 파악합니다. 나머지 팀은 그동안 메인라인에서 계속 작업합니다. 일부 팀은 펜딩 헤드를 써서 메인라인이 깨질 위험 자체를 없앤다고 적습니다. 푸시된 커밋을 곧바로 메인라인에 올리지 않고 다른 브랜치에 두었다가 빌드가 초록일 때만 옮기는 방식입니다.

도구만 돌리고 매일 합치지 않으면 이름만 남습니다. 이 글이 소개하는 자가진단은 세 문항입니다. 팀의 모두가 공용 메인라인에 최소한 매일 커밋하고 푸시하는가. 그런 커밋마다 자동 빌드와 테스트가 도는가. 빌드가 실패했을 때 보통 십 분 안에 초록으로 돌아오는가. 처음에는 청중 대부분이 손을 듭니다. 첫 문항에서 절반 넘게 내려갑니다. 둘째 문항에서 남은 손의 절반이 내려갑니다. 마지막 문항까지 남는 손은 몇 개뿐이라고 적습니다. 십 분 안에 초록 커밋 빌드를 만들지 못하면 마지막 초록 빌드로 되돌려야 한다고 적습니다.

처음에 손이 많이 올라가는 이유도 Fowler 가 적습니다. 지속적 통합이 자기 기능 브랜치들에 대고 지속적 통합 서버를 돌리는 것이라는 통념이 흔하다는 것입니다. Kent Beck 이 익스트림 프로그래밍의 일부로 처음 서술하고 이름 붙인 지속적 통합은 도구와 아무 상관이 없다고 적습니다. 처음에 그것은 사람의 작업 흐름이었습니다. 소스 저장소에 대고 데몬 프로세스를 돌린다는 발상은 나중에 왔습니다. 도움이 되긴 하지만 사람들이 매일 커밋하는 공용 메인라인 위에서 돌 때만 지속적 통합이라고 적습니다. 기능 브랜치마다 그런 프로세스를 돌리는 것은 이름을 깎아내리는 CI 시어터라고 적습니다. 그 수고를 들일 만하게 만드는 이득을 주지 못하는 작업 흐름을 낳는다고 적습니다.

메인라인에서 받아오기만 하고 되밀지 않으면 절반만 통합한 것입니다. 브랜치에서 일하는 사람들이 메인라인의 변경을 정기적으로 가져와 자기 작업이 깨지지 않는지 확인하는 일이 있다고 Fowler 는 적습니다. 이것은 온전한 통합 절차가 아닙니다. 온전한 메인라인 통합은 개발자가 자기 작업을 메인라인으로 되밀 것을 요구합니다. 되밀지 않으면 다른 팀원이 그 작업을 볼 수 없고 충돌을 확인할 수 없습니다. 이 반쪽 통합은 브랜치가 벌어지는 것도, 충돌이 곪는 것도, 낮은 빈도 통합의 문제들도 막지 못합니다.

통합이 힘들었던 경험이 빈도를 낮추는 쪽으로 팀을 밀기도 합니다. Fowler 는 이런 비선형 효과가 흔히 그렇듯 통합이 잘못된 교훈을 배우는 함정이 되기 쉽다고 적습니다. 어려웠던 통합이 너무 충격적이어서 팀이 통합을 덜 자주 하기로 결정하는 일이 있습니다. 그것은 앞으로의 문제를 더 키울 뿐이라고 적습니다.

관련 항목

이 실천이 다루는 브랜치·병합 규율

메인라인 · 트렁크 기반 개발 · 기능 브랜치 · 버전관리 · 펜딩 헤드 · 머지 큐

미완성 코드를 감추는 장치

기능 토글 · 기능 플래그 · 키스톤 인터페이스 · 다크 런칭 · 브랜치 바이 앱스트랙션 · 병렬 변경 · 캐너리 릴리스 · 의존성 네트워크

이것을 이루는 구성 요소

커밋 빌드 · 테스트 스위트 · 배포 파이프라인

이것을 실제로 구현·채택한 제품

Jenkins · GitHub Actions · GitLab CI · CruiseControl

이것에서 자주 나는 오류·장애

병합 충돌 · 시맨틱 컨플릭트 · 플레이키 테스트 · 빌드 깨짐 · CI 시어터 · 세미 통합 · 시맨틱 디퓨전

이 실천 다음에 오는 배포 단계

지속적 전달 · 지속적 배포 · 커밋에서 배포까지 · 배포 · 롤백

이것이 비롯된 익스트림 프로그래밍 계열

익스트림 프로그래밍 · 테스트 주도 개발 · 자체 테스트 코드

다른 이름: Continuous Integration · CI