지속적 통합: 팀원들이 작업 결과를 자주 통합하는 소프트웨어 개발 방식이다. 통합할 때마다 자동 빌드(테스트 포함)하여 통합 오류를 빠르게 찾아낸다.
- CI의 기본 목적은 문제를 일으키는 변경을 가능한 한 조기에 자동으로 발견해내는 것이다.
- 요즘 시스템들은 리포지터리 안의 코드 말고도 동적으로 변하는 요소가 많다.
- 최근 유행하는 마이크로서비스 시스템에서는 문제의 원인이 코드베이스가 아니라 느슨하게 결합된 다른 마이크로서비스와의 네트워크 실패 때문인 경우가 많다.
- 전통적인 지속적 빌드에서는 바이너리에서 변경된 기능을 테스트하지만 이를 확장하면 업스트림 마이크로서비스의 변경들도 테스트할 수 있다.
- 의존성들의 변경까지 모두 지속적으로 통합하는 걸 목표로 삼아야 한다.
- 변경되는 요소들의 소유자가 우리 팀, 조직, 회사 외부의 개발자일 수도 있다.
대규모 개발을 고려한 CI: 빠르게 진화하는 복잡한 생태계 전체를 지속적으로 조립하고 테스트하는 개발 방식
- 테스트라는 시각에서 CI는 다음을 알려주는 패러다임이다.
- 코드(와 다른 요소)가 변경되어 지속적으로 통합되는 개발/릴리즈 워크플로에서 무슨(what) 테스트를 언제(when) 실행해야 하는가?
- 워크플로의 각 테스트 지점에서 (적절한 충실성을 갖춘) 테스트 대상 시스템(SUT)을 (합리적인 비용으로) 어떻게(how) 구성해야 하는가?
- 각 테스트 지점(프리서브밋, 포스트서브밋, 스테이징 배포 전, ...)에서 SUT를 어떻게 구성할까?
- 버그는 발견하는 시점이 늦을수록 처리 비용이 기하급수적으로 증가한다.
- 코드 변경 과정에서 문제를 발견해낼 수 있는 위치들
- 편집/컴파일/디버그 -> 프리서브밋 -> 포스트서브밋 -> 릴리즈 후보(RC) -> RC 승격(스테이징 등 임시 환경) -> 최종 RC 승격(프로덕션)
- 일반적으로 오른쪽 끝 가까이까지 살아남을수록 비용이 커지는 이유
- 문제의 코드에 익숙하지 않은 엔지니어가 분류해야 한다.
- 변경 작성자가 무엇을 왜 변경했는지 기억해내고 조사하는 노력이 더 많이 든다.
- 바로 옆 동료부터 (마지막 단계까지 잡아내지 못하면) 최종 사용자까지, 다른 이들에게 부정적인 영향을 준다.
- CI는 빠른 **피드백 루프(fast feedback loop)**를 이용하도록 유도하여 버그 비용을 최소로 줄여준다. 코드 또는 다른 변경을 테스트 시나리오에 통합하고 결과를 관찰하기까지가 하나의 피드백 루프이다.
- 다양한 피드백의 형태(피드백이 빠른 순)
- 로컬 개발 시의 편집-컴파일-디버그 루프
- 프리서브밋 시 변경 작성자에게 주어지는 자동 테스트 결과
- 두 프로젝트를 변경한 후 통합 시 오류. 양쪽 변경들을 서브밋한 다음 함께 테스트할 때 발견(예: 포스트서브밋)
- 내 프로젝트와 업스트림 마이크로서비스 의존성 사이의 호환성 충돌. 업스트림 서비스의 최신 변경이 스테이징 환경에 배포될 때 QA 엔지니어가 발견
- 기능을 먼저 이용해본 사내 이용자의 버그 리포트
- 외부 고객 혹은 언론 매체의 버그 리포트 (혹은 서비스 장애 리포트)
- **카나리 배포(canary release)**를 활용하면 프로덕션에서 일어나는 문제가 확실히 줄어든다. 프로덕션 전체에 배포하기 전에 일부에만 먼저 배포하여 초기 피드백 루프를 만들 수 있기 때문이다. 하지만 여러 가지 버전이 동시에 배포되어 있으면 호환성 문제가 생길 수 있으니 카나리 배포 자체로 문제를 일으킬 여지도 있다.
- **버전 왜곡(version skew)**이라는 문제는 분산 시스템에 호환되지 않는 여러 코드, 데이터, 설정 정보(configuration)가 공존하는 상태를 말한다.
- **실험(experiment)**과 **기능 플래그(feature flag)**도 매우 강력한 피드백 루프이다. 변경을 컴포넌트 단위로 격리한 후 프로덕션 환경에서 동적으로 켜고 끌 수 있게 하여 배포 위험을 줄여주는 기법들이다.
- CI가 제공하는 피드백을 많은 사람이 볼 수 있어야 한다.
- 구글은 통합 테스트 리포트 시스템을 이용하여 빌드 테스트 결과를 로그까지 누구든 쉽게 볼 수 있게 한다. 엔지니어가 로컬에서 수행한 빌드든 자동으로 수행되는 데브 혹은 스테이징 빌드든 모두 확인할 수 있다.
- 테스트 리포트 시스템은 빌드와 테스트의 실패 이력도 자세히 알려준다. 각 빌드(테스트)가 어디서 멈췄고, 어디서 실행했고, 누가 실행했는지도 알 수 있다. 분명한 실패인지 비결정적인 결과인지를 구분해주는 시스템도 마련했다.
- CI 테스트가 제공하는 피드백은 모두 조치가 가능해야 한다. 즉, 문제를 찾고 고치기가 쉬워야 한다. 테스트 출력 메시지의 가독성을 개선하면 피드백을 자동으로 이해하고 조치까지 자동으로 이루어지게 할 수 있다.
- 개발 관련 활동들을 자동화하면 장기적으로 엔지니어링 자원을 아낄 수 있다.
- 여러 개발 활동 중 CI는 특별히 빌드와 릴리즈 프로세스를 자동화한다. 각각을 따로 지속적 빌드와 지속적 배포라 한다. 지속적 테스트는 두 단계 모두에 자동으로 따라붙는다.
- **지속적 빌드(continuous build, CB)**는 가장 최근의 코드 변경을 헤드(트렁크)에 통합한 다음 자동으로 빌드와 테스트를 수행한다.
- CB에서는 테스트도 빌드의 한 과정으로 보기 때문에 컴파일을 통과하더라도 테스트에 실패하면 빌드 실패로 간주한다.
- 모든 테스트에 통과하면 CB가 UI에 통과 혹은 녹색으로 표시한다.
- 이 프로세스로 인해 리포지터리에는 실질적으로 두 가지 버전의 헤드가 존재할 수 있다.
- 참 헤드(true head) 최신 변경이 커밋된 버전
- 녹색 헤드(green head): CB가 검증한 최신 변경
- 지속적 배포(Continuous Delivery, CD)의 첫 번째 단계는 릴리즈 자동화이다. 헤드로부터 지속해서 최신 코드와 설정 정보를 가져와서 릴리즈 후보를 만들어 내는 작업이다.
- 릴리즈 후보(Release Candidate, RC): 자동화된 프로세스가 만든, 서로 밀접하게 관련된 요소들로 구성된 배포 가능한 단위. 지속적 빌드를 통과한 코드, 설정 정보, 기타 의존성들을 조합해 만든다.
- 릴리즈 후보에는 설정 정보까지도 포함된다. 설정 정보는 RC가 어디로 승격되느냐에 따라 조금씩 다를 수 있지만 반드시 포함시켜야 하는 매우 중요한 정보이다.
- 설정 정보를 꼭 바이너리 자체에 포함시킬 필요는 없다. 실험이나 기능 플래그처럼 다양한 시나리오에 대응할 수 있도록 설정 정보를 동적으로 바꿀 수 있게 하라고 권장한다.
- 정적인 설정 정보들은 모두 릴리즈 후보에 묶어 승격시켜서 해당 코드와 함께 테스트되도록 해야 한다. 실제로 프로덕션 버그의 상당수가 설정이 잘못되어 발생한다.
- 구글은 정적 설정 정보를 코드와 함께 버전 관리하여 코드 리뷰 프로세스를 똑같이 거치게 한다.
지속적 배포: 지속해서 릴리즈 후보를 조립한 다음 다양한 환경에 차례로 승격시켜 테스트하는 활동. 프로덕션까지 승격시키는 경우도 있고 그렇지 않은 경우도 있다.
- 구글의 경우 바이너리들의 크기가 대체로 상당히 크기 때문에 프로덕션에 새로 반영되는 변경들의 피드백을 바로바로 받기 위해 바이너리 모두를 매번 밀어 넣기란 거의 불가능하다.
- 대신 주로 실험이나 기능 플래그를 활용하는 선택적 지속적 배포 전략을 택한다.
- RC가 운영 환경들에서 단계별로 승격될 때 이상적으로는 아티팩트들(예: 바이너리, 컨테이너)을 다시 컴파일하거나 빌드하지 않아야 한다. 로컬에서 개발할 때부터 도커 같은 컨테이너를 이용하면 환경을 옮겨도 RC의 일관성을 지켜내기가 쉬워진다. 비슷하게 쿠버네티스 같은 오케스트레이션 도구를 이용하면 배포 사이에 일관성을 유지하기가 더 유리하다.
- 구글은 환경 간 릴리즈와 배포를 일관되게 관리하여 초기 테스트부터 충실성을 끌어올렸다. 그 덕에 프로덕션에서 뒤늦게 문제가 터지는 빈도를 크게 줄였다.
- 지속적 테스트(Continuous Testing, CT)까지 감안하면 CB와 CD가 코드 변경 생애에 어떻게 결합되는지 살펴본다.
- 프리서브밋 때 모든 테스트를 다 돌려보지 않는 이유는 무엇일까?
- 가장 큰 이유는 비용 때문이다. 개발에서 엔지니어 생산성이 매우 중요한데, 코드를 서브밋할 때마다 테스트 때문에 한참을 기다려야 한다면 생산성이 심각하게 떨어진다.
- 테스트 범위를 제한하거나 문제를 발견할 가능성이 높은 테스트를 찾는 모델을 고안하는 등의 방식으로 프리서브밋 때 수행할 테스트를 잘 선별해야 한다. 불안정하거나 비결정적인 테스트가 끼어들어서 엔지니어가 다음 일을 진행하지 못하게 돼도 손해가 막심하다.
- 프리서브밋 테스트가 수행되는 동안에도 리포지터리는 계속 수정될 수 있어서 장시간 열심히 테스트한 변경과 이미 호환되지 않게 달라져 있을 가능성도 있다. 이를 공중 충돌(mid-air collision)이라 부른다. 작은 리포지터리용 CI 시스템에서는 서브밋들을 하나씩 차례로 수행하게 하면 나타나지 않을 문제이다.
- 프리서브밋 때는 어떤 테스트를 수행해야 하는가?
- 경험에 따르면 빠르고 안정적인 테스트만 수행해야 한다. 프리서브밋 때는 커버리지를 다소 포기하고, 대신 놓친 문제들을 포스트서브밋 때 잡아내는 전략이다.
- 포스트서브밋 때는 더 오래 걸리고 (문제 시 대처할 방법만 있다면) 약간 불안정한 테스트도 포함시킬 수 있다.
- 프리서브밋 때 범위가 더 넓은 테스트를 수행해도 되지만, 이런 테스트를 원한다면 밀폐 테스트를 권한다. 내재된 불안정성을 줄여주는 검증된 기법이다.
- 범위가 넓고 불안정한 테스트를 허용하되 실패하기 시작하면 적극적으로 비활성화하는 것도 하나의 방법이다.
- 변경이 지속적 빌드를 통과했다면 곧바로 지속적 배포 단계로 넘어와 다음 릴리즈 후보(RC)에 반영된다.
- 지속적 배포가 RC를 만들 때는 RC 전체를 광범위하게 검증하는 더 큰 테스트들을 수행한다.
- RC는 일련의 테스트 환경에서 다음 단계 환경으로 승격될 때마다 매번 테스트된다. 테스트 환경으로는 샌드박스, 임시 환경, 공유 테스트 환경(예: 데브 또는 스테이징) 등이 있다. 공유 환경에 접어들면 수동 QA 테스트까지 진행하는 게 보통이다.
- 지속적 빌드에서 포스트서브밋 때 똑같은 테스트를 이미 수행했더라도 RC는 매 단계에서 철저하게 다시 테스트한다. 그래야 하는 중요한 이유는 다음을 참고한다.
- 온전성 검사: 코드를 잘라와 RC용으로 다시 컴파일할 때 예상치 못한 일이 발생하지 않았는지를 재차 확인한다.
- 검사 용이성: RC의 테스트 결과를 확인하려는 엔지니어가 지속적 빌드 로그를 파헤쳐보지 않고도 해당 RC와 관련된 결과를 바로 살펴볼 수 있다.
- 체리픽 허용: RC에 체리픽 수정을 반영한다면 지속적 빌드가 테스트한 코드와 달라진다.
- 비상 배포: 비상 상황 발생 시, 지속적 배포는 참 헤드의 코드를 잘라와서 (지속적 빌드 전체가 다시 통과될 대까지 기다리지 않고) 배포를 해도 괜찮겠다는 마음의 안정을 가져다주는 데 꼭 필요한 최소한의 테스트만 실행할 수 있다.
- 구글의 지속적인 자동화 테스트 프로세스는 배포의 최종 단계인 프로덕션까지 계속 이어진다.
- 앞서 RC 때와 동일한 테스트 스위트를 프로덕션 환경에서도 그대로 수행한다(프로버(prober)라고도 한다). 목적은 다음의 두 가지를 검증하기 위해서이다.
- 프로덕션이 테스트 대로 올바르게 동작하는가?
- 프로덕션을 검증하기에 적합한 테스트인가?
- 애플리케이션 승격 단계마다의 지속적 테스트는 버그를 잡기 위한 심층 방어 전략이 왜 중요한지를 상기시키는 역할을 한다. 품질과 안정성을 확보하려면 단 하나의 기술이나 전략만으로는 부족하다. 다양한 테스트 전략을 혼합해야 한다.
- CI를 '원점 회귀시킨 경보'라는 관점에서 바라보기 시작하면 여러 정책을 다시 생각하여 더 나은 모범 사례를 제시할 수 있다.
- 프로덕션 서비스의 가동 시간 목표를 100%로 잡으면 엄청난 비용을 쏟아부어야 한다. CI의 녹색 비율을 100%로 만들려 해도 마찬가지이다. 진짜로 이런 목표를 세웠다면 테스트가 서브밋을 막아서는 가장 큰 장벽이 될 것이다.
- 모든 경보를 동일하게 취급하는 것 역시 일반적으로 올바른 정책이 아니다. 프로덕션에서 경보가 발생했지만 서비스에 아무런 영향이 없었다면 그 경보는 조용히 꺼두는 게 옳은 선택이다. 테스트 실패도 마찬가지이다. CI 시스템이 '이 테스트는 관련 없는 이유로 실패한다고 알려져 있다'라고 말하는 법을 배울 때까지는 실패한 테스트를 비활성화시키는 일에 너무 깐깐해할 필요 없다. 모든 테스트 실패가 프로덕션에서 문제를 일으키는 건 아니다.
- '가장 최근 CI 결과가 녹색이 아니면 아무도 커밋할 수 없다'라는 정책은 대체로 옳지 않다. CI가 문제를 보고하면 사람들이 다른 변경을 커밋하거나 문제가 더 커지기 전에 조사를 해봐야 하는 건 분명하다. 하지만 근본 원인을 잘 이해하고 있고 프로덕션 영향을 주지 않을 게 확실하다면 무조건적으로 커밋을 막는 건 합리적이지 못하다.
- 이 외에 CI 구현할 때는 다음 사항들도 고민해야 한다.
- 프리서브밋 최적화: 앞서 언급한 잠재적인 문제를 고려하여 프리서브밋 시 어떤(which) 테스트를 어떻게(how) 수행해야 할까?
- **범인 찾기(culprit finding)**와 실패 격리(failure isolation): 문제를 일으킨 코드(또는 기타 변경)가 어느 것이고, 어느 시스템에서 문제가 발생했는가?
- 자원 제약: 테스트를 실행하려면 자원이 있어야 하고, 거대한 테스트는 엄청난 자원을 소비한다. 또한 프로세스 단계마다 자동화 테스트를 수행하는 데 필요한 인프라 비용도 상당할 수 있다.
- **실패 관리(failure management)**도 만만지 않은 문제이다. 실패 관리란 실패 시 어떻게 대응해야 하는가를 말한다.
- 경험상 거대한 종단간 테스트가 끼어들면 테스트 스위트를 녹색으로 유지하기가 극히 어렵다. 본질적으로 깨지기 쉽고 불규칙하고 디버그하기 어렵다. 따라서 이런 테스트를 임시로 비활성화하고 추적하는 메커니즘을 마련해 릴리즈는 계속 진행할 수 있도록 해야 한다.
- 불규칙한 테스트는 릴리즈 프로세스에 또 다른 문제를 일으킨다. 계속 진행해도 될지 확신하지 못하게 만들며, 동시에 매번 실패하는 게 아니라서 롤백해야 할 변경이 어느 것인지 찾기 어렵다.
- **테스트 불안정성(test instability)**도 또 다른 중요한 과제이다. 불안정성 문제는 테스트를 여러 번 시도하는 방식으로 대처할 수 있다.
- 서비스 중인 백엔드와의 통신은 안정적이지 않을 수 있어서 구글은 범위가 넓은 테스트에는 밀폐된 백엔드를 주로 이용한다. 안정성이 매우 중요한 프리서브밋 테스트 때 특히 유용하다.
밀폐 테스트(hermetic test): 모든 것을 갖춘 테스트 환경에서 수행하는 테스트
- 프로덕션 백엔드 등의 외부 의존성을 전혀 이용하지 않으며 테스트에 필요한 애플리케이션 서버나 자원 등을 모두 갖춘 환경에서 테스트하는 것을 말한다.
- 밀폐 테스트에는 중요한 특징인 우수한 결정성(determinism)과 격리(isolation)이라는 두 가지 특징이 있다.
- 밀폐된 서버들에도 시스템 시간, 무작위 숫자 생성, 스레드 간 경쟁 상태 등 비결정적일 소지가 여전히 남아 있다. 그러나 테스트에 영향을 주는 데이터가 적어도 외부 의존성 때문에 바뀔 일은 없으므로 똑같은 애플리케이션과 테스트 코드로 두 번 테스트하면 같은 결과를 얻을 것이다.
- 밀폐 테스트가 실패한다면 최근 수정한 애플리케이션 코드나 테스트가 원인일 것이다.
- 격리는 프로덕션의 문제가 밀폐 테스트에 영향을 주지 않는다는 뜻이다.
- 밀폐 테스트는 보통 기기 한 대에서 수행하므로 네트워크 연결 문제는 걱정하지 않아도 된다.
- 밀폐 테스트는 누가 실행하느냐에 따라 성공 여부가 달라지지 않는다.
- 밀폐 테스트에 필요한 백엔드는 가짜 서버(fake server)로 대체할 수 있다. 실제 백엔드보다 저렴하게 이용할 수 있으나 유지보수가 필요하며 충실성 측면에서 한계가 있다.
- 프리서브밋을 가치 있는 통합 테스트로 만들려면 필요한 전체 구성요소를 샌드박스에 담아 시작하는 완벽하게 밀폐된 설정을 활용하는 방법이 가장 깔끔하다.
실패해야 할 테스트가 성공한다. 예를 들면 잘못된 결과가 나와야 할 때도 캐시에 기록해둔 정상적인 값을 반환하는 경우이다.
성공해야 할 테스트가 실패한다. 기록해둔 데이터가 잘못되었을 때 발생한다. 응답 데이터를 업데이트해야 하는데, 데이터를 다시 기록하는 데는 시간이 걸린다. 그동안 해당 테스트는 실제로는 아무 문제가 없음에도 계속 실패할 것이다. 서브밋이 진행되지 못하게 가로막기 때문에 바람직하지 않다.
- CI 시스템은 무슨 테스트를 언제 실행해야 할지를 결정한다.
- 코드베이스가 오래되고 규모가 커질수록 CI 시스템이 더욱 필요해진다.
- 빠르고 더 안정적인 테스트는 프리서브밋 단계에서, 느리고 덜 결정적인 테스트는 포스트 서브밋 단계에서 실행하도록 최적화해야 한다.
- 볼 수 있고 조치할 수 있는 피드백이 CI 시스템을 더 효율적으로 만들어준다.