- 구글은 코드베이스 전반을 전면적으로 건드리는 변경을 원자적으로 수행한다는 어이디어를 포기했다.
- 경험에 따르면 코드베이스와 엔지니어 수가 늘어날수록 원자적으로 수행할 수 있는 변경의 크기는 줄어든다.
- 논리적으로 연관되어 있으나 현실적인 한계 때문에 원자적으로 서브밋할 수 없는 변경들의 집합
- LSC로 인해 생성되는 변경
- 코드베이스 전반을 훑는 분석 도구로 찾은 공통 안티패턴 청소
- 폐기 대상 API 호출 대체
- (컴파일러 업그레이드 등) 저수준 인프라 개선사항 활성화
- 사용자들을 옛 시스템에서 새로운 시스템으로 마이그레이션
- 구글에서는 상당 비중을 인프라팀들이 수행한다. 하지만 LSC 도구들과 지원 자원들은 누구나 이용할 수 있다.
- 새로운 클래스, 함수, 시스템을 만든 후 사용자 모두에게 새 버전을 쓰도록 강제하지 않는 이유?
- 하부 시스템을 구축하고 관리하는 인프라팀들은 그 시스템을 활용하는 수만 개의 참조를 수정하는 데 필요한 도메인 지식도 갖추고 있다. 인프라를 이용하는 팀들은 이런 마이그레이션을 처리하는 방법을 모를 가능 성이 크다.
- 합당한 보상 없이 할 일만 늘어나는 상황을 좋아할 사람은 없다. 새 시스템이 기존 시스템보다 낫더라도 팀에 따라서는 자발적으로 업그레이드할 만한 매력을 느끼지 못할 수도 있다. 반드시 마이그레이션해야 할 만큼 새로운 시스템이 중요하다면 비용을 조직 차원에서 부담하는 게 맞다.
- 대규모로 변경해야 할 시스템을 소유한 팀이 주도해야 변경을 완료하는 데 유리하다.
- 구글의 LSC 프로세스를 이해하려면 왜 수많은 변경들이 원자적으로 커밋될 수 없는지 이해해야 한다.
- 대부분 버전 관리 시스템(VCS)에서는 기능을 수행하는 비용이 변경의 크기에 비례해 커진다. 파일 수십 개 규모의 작은 커밋은 무리없이 처리해주지만 파일 수천 개를 원자적으로 커밋하기에는 메모리나 프로세싱 능력이 부족할 수 있다.
- 중앙집중형 VCS에서는 커밋 중에는 다른 사용자가 쓰기 작업을 하지 못한다. 거대한 커밋은 다른 사용자들의 일을 멈춰세운다.
- 변경의 규모가 커질수록 병합 시 충돌이 생길 가능성이 커진다.
- 변경에 포함되는 파일이 많아질수록 병합 충돌이 나타날 확률이 높아지며, 같은 리포지터리를 이용하는 엔지니어가 많을수록 문제는 더욱 복잡해진다.
- 유령의 묘지: 너무 오래되고 둔하고 복잡해서 아무도 손대려 하지 않는 시스템. 흔히 사업상 아주 중요한 시스템 중인 경우가 많다.
- LSC 관점에서 유령의 묘지는 모든 형태의 의미 있는 진보를 가로막는다.
- 충실한 테스트는 유령의 묘지 퇴출에 아주 효과적이다. 소프트웨어가 철저하게 테스트된다면 변경해도 이상이 생기지 않으리라는 믿음이 생긴다.
- LSC가 가능하려면 LSC에 수반되는 작업 대부분을 사람이 아니라 컴퓨터가 처리해줘야 한다.
- 컴퓨터가 변경 코드를 정확한 위치에 올바르게 반영하려면 환경이 일관되어야 한다. 하나의 조직에서 다양한 VCS와 CI 시스템을 운영하고 프로젝트별로 도구와 스타일 가이드가 다르다면 코드베이스 전체를 아우르는 변경은 진행하기 어렵다. 반대로 환경을 단순화해 일관성을 높이면 인력을 재배치할 때도 좋고 컴퓨터가 변경을 자동으로 수행하는 데도 좋다.
- 모든 변경은 테스트되어야 한다. 하지만 변경의 덩치가 커지면 제대로 테스트하기가 훨씬 어렵다.
- 작은 변경을 자주 진행하면 같은 테스트들이 여러 번 실행된다. 테스트가 실패한 원인을 추적하는 데 드는 엔지니어의 시간은 이런 추가 테스트들을 수행하는 컴퓨팅 시간보다 훨씬 비싼 자원이므로 구글은 기꺼이 절충했다.
- 테스트 자동화 플랫폼(Test Automation Platform, TAP) 열차
- TAP 열차가 유용한 이유
- LSC는 대부분 순수한 리팩터링이라서 주제 범위가 매우 좁고 코드의 원래 의미가 달라지지 않는다.
- LSC를 구성하는 개별 변경은 단순하고 면밀히 검토해 진행하므로 잘못될 가능성이 크지 않다.
- 열차는 다음 다섯 단계로 진행되며, 3시간마다 새로 출발한다.
- 열차에 실린 변경 각가에 대해 무작위로 선택한 테스트 1,000개를 수행한다.
- 1,000개의 테스트를 통과한 변경들을 모아서 그중 대표 변경을 하나 뽑는다. 이 변경의 이름이 바로 '열차(The Train)'이다.
- 변경들에 직접 영향받는 테스트들을 모두 실행한다. 일정 이상 큰, 혹은 저수준의 LSC인 경우, 이때 구글 리포지터리의 모든 테스트가 실행된다. 그래서 이 단계는 여섯 시간 이상 걸리기도 한다.
- 불규칙하지 않으면서 실패한 테스트만 추려 개별 변경에 대해 독립적으로 다시 수행한다. 열차에 올라탄 변경 중 어느 것이 실패의 원흉인지를 알아내는 단계이다.
- TAP이 열차에 탑승한 각 변경에 대한 보고서를 생성한다. 보고서에는 성공한 대상과 실패한 대상이 모두 포함되어 있어서 LSC가 서브밋해도 안전한지 판단하는 근거로 활용된다.
- 모든 변경은 서브밋하기 전에 리뷰를 거쳐야 한다. LSC도 예외가 아니다.
- LSC를 별도의 샤드(shard; 조각)로 나누면 훨씬 쉬워진다.
- 구글은 LSC를 위해 인프라에 막대한 투자를 했다. 이 인프라에는 변경 생성, 변경 관리, 변경 리뷰, 테스트 도구 등이 포함된다.
- 도구들보다 중요한 요인은 대규모 변경과 이를 감독하는 프로세스를 둘러싼 문화적 규범의 진화였다.
- 구글은 수많은 소스 코드를 하나의 리포지터리(모노리포)에 보관하며 모든 엔지니어가 그 안의 코드 대부분을 볼 수 있다.
- 이로 인해 누구라도 다른 프로젝트 파일을 수정한 후 승인 권한자에게 검토를 요청할 수 있다. 하지만 그럴 때마다 변경하는 비용과 리뷰 비용이 발생한다.
- 구글은 LSC를 만들려는 팀과 개인을 위한 가벼운 승인 프로세스를 고안했다.
- 다양한 언어의 미묘한 특성에 익숙한 숙련된 엔지니어 그룹이 감독하며, 이때 새로 만들려는 LSC 관련 도메인에 전문가를 초대한다.
- 프로세스의 목표는 LSC를 막는 게 아니다. 오히려 변경 작성자가 구글의 기술과 인적 지원을 최대한 활용하여 가장 이상적인 변경을 생성하도록 돕는 것이다.
- 때로는 LSC에 영향받는 개별 팀의 소유자가 특정 커밋에 대해 댓글로 질문을 던질 수 있다. 그러면 보통의 리뷰 때와 마찬가지로 변경 작성자가 답변을 달아준다.
- LSC를 진행하려면 코드베이스 전반을 분석할 수 있어야 한다. 텍스트 기반의 전통적인 분석은 물론 의미를 추적하는 분석도 중요하다.
- 어떤 도구로 변경을 생성하든 인력 투입량이 코드베이스보다 느리게 커져야 한다. 달리 말하면 리포지터리가 커져도 사람이 개입하는 시간은 크게 달라지지 않아야 한다.
- 도구에 투자하는 비용은 지금 당장에도, 또 앞으로도 결실을 맺어 되돌아온다. 수정해야 하는 코드가 500곳이 넘어가면 사람이 일일이 수정하기보다는 변경 생성 도구를 익혀 이용하는게 효율이 좋았다. 숙련된 코드 관리자라면 훨씬 적은 수정도 도구를 이용하는 편이 낫다.
- 대규모 변경 인프라에서 가장 중요한 도구는 마스터 변경을 여러 개의 샤드로 나눈 후 테스트, 메일링, 리뷰, 커밋 단계를 독립적으로 관리해주는 도구이다.
- 구글은 Rosie라는 도구를 사용한다. Rosie를 이용하면 광범위한 변경에 수반되는 수많은 변경을 독립적으로 테스트, 리뷰, 서브밋할 수 있는 작은 샤드들로 나눌 수 있다.
- 테스트는 소프트웨어가 기대한 대로 동작함을 검증하는 중요한 수단이다.
- 사람이 작성하지 않은 변경을 적용하려 할 때 특히 중요하다.
- 건실한 테스트 문화와 인프라가 갖춰져 있다면 도구가 생성한 변경이 의외의 부작용을 일으키지 않을 것임을 더 강하게 확신할 수 있다.
- 구글의 LSC는 다른 변경과 똑같은 CI 인프라를 활용하지만 테스트 전략은 조금 다르게 가져간다. LSC 테스트는 마스터 변경이 문제를 일으키지 않음을 확인하는 일 외에, 개별 샤드를 안전하고 독립적으로 서브밋할 수 있는지까지 검증해야 한다.
- 개별 샤드에는 임의의 파일이 포함될 수 있으므로 표준적인 프로젝트별 프리서브밋 테스트를 이용하지 않는다. 대신 각 샤드에 영향받는 테스트들을 추적하여 수행하는 방식을 활용한다.
- 구글은 LSC를 주로 언어별로 진행하며, 언어에 따라 LSC 난이도가 크게 다르다.
- 타입 별칭과 전달 함수를 지원하는 프로그래밍 언어라면 새로운 시스템으로 마이그레이션하는 중에도 기존 코드가 문제없이 동작하게 만들기가 훨씬 쉽다.
- 정적 타입 언어가 동적 타입 언어보다 훨씬 유리하다. 강력한 정적 분석과 컴파일러 기반 도구가 제공하는 상당한 양의 정보를 도구 제작에 활용하면 문제를 일으키는 변경을 테스트 단계까지 가기 전에 걸러낼 수 있다.
- 어떤 언어를 선택할지는 여러 측면에서 코드 수명과 관련이 깊다. 개발자 생산성에 치중하는 언어일수록 유지보수가 더 어려워진다.
- 자동 포맷터 역시 LSC 인프라에서 중요한 역할을 담당한다. 구글의 모든 LSC 생성 도구가 언어별 자동 포맷터까지 실행해준다.
- 권한 부여
- 변경 생성
- 샤드 관리
- 마무리 청소
- 일반적으로 새로운 시스템, 클래스, 혹은 함수를 작성한 후에 프로세스가 시작되지만, 사실 새로운 시스템을 설계할 때부터 염두에 두어야 한다.
- 구글은 LSC 작성자에게 다음의 내용을 포함한 간단한 제안 문서를 작성해달라고 요청한다.
- 변경을 제안하는 이유
- 코드베이스 전반에 주는 예상 영향(예: 제안한 LSC로 인해 생성되는 작은 샤드 수)
- 리뷰어들이 던질만한 질문과 그에 대한 답변
- 작성자는 리팩터링될 API들의 소유자들로부터 도메인 리뷰도 받아야 한다.
- 그런 다음 전체 프로세스 감독자 10여 명으로 구성된 위원회에 제안을 송부한다. 위원회는 논의 후 어떻게 진행해야 할지를 피드백한다.
- 이때 위원회는 대체로 LSC에 관한 코드 리뷰 전체를 한 명의 글로벌 승인자에게 할당한다. 영향받는 각 프로젝트의 소유자들이 리뷰해줄 거라고 생각할 수 있지만, 대다수의 기계적인 LSC는 해당 변경의 본질과 필요한 빌드 자동화를 깊이 이해하는 전문가 한 명에게 맡기는 게 효율적이다.
- 변경이 승인되면 작성자가 서브밋할 수 있다. 역사적으로 위원회는 승인에 관대하며, 변경 하나하나보다는 관련된 변경들을 일괄로 승인해준다.
- 이 프로세스의 목적은 LSC 작성자에게 너무 부담주지 않는 선에서 감독 및 에스컬레이션 경로를 제공하는 것이다.
- 위원회는 LSC가 갈등을 일으킬 시 중재해주는 에스컬레이션 기관의 권한도 갖는다. 변경에 반대하는 개별 소유자는 위원회에 이의를 제기할 수 있고, 그러면 위원회가 중재에 나선다. 하지만 실제로 이런 일은 거의 일어나지 않는다.
- 승인을 얻은 LSC 작성자는 실제로 코드를 수정하기 시작한다.
- 대체로 거대한 전역 변경 하나를 생성한 후 다수의 독립적인 샤드로 쪼개게 된다. 보통은 변경이 너무 커서 하나의 글로벌 변경으로 처리하지 못한다.
- 변경 생성 프로세스는 가능한 한 자동화해야 한다. 누군가 이전 방식의 코드로 되돌아가거나 변경된 코드에서 병합 충돌이 발생했을 때 상위 변경을 업데이트해야 하기 때문이다.
- 글로벌 변경을 생성했따면, 작성자는 이어서 Rosie를 실행한다. Rosie는 거대한 변경을 하나 입력받아서 서브밋할 수 있는 작은 변경(샤드)들로 쪼개준다.
- 프로젝트 경계와 소유권 규칙을 참고하여 개별 샤드를 독립된 테스트-메일-서브밋 파이프라인에 태운다.
- 독립된 각각의 샤드는 구글의 CI 프레임워크인 TAP에 의해 테스트된다. 이때 수정되는 파일들에 직간접적으로 의존하는 모든 테스트를 수행하기 때문에 때로는 CI 시스템에 큰 부담을 준다.
- 독립적으로 실행할 때는 일어날 확률이 매우 낮은 사건이라도 많은 테스트를 한꺼번에 실행하다 보면 엄청난 규모 때문에 거의 확실하게 일어난다는 단점이 있다. 불규칙한 테스트 혹은 깨지기 쉬운 테스트들이 LSC 작성자들에게 특히 골치이다.
- 구글은 최근에 튀었던 테스트보다 변경 자체를 훨씬 신뢰한다. 그래서 구글의 자동화 도구에서는 서브밋 시 최근에 튄 불규칙한 테스트들 무시하도록 했다. 각 샤드에서 불규칙한 테스트가 걸러주는 회귀 문제가 발생할 수 있다는 뜻이 되지만, 발생 빈도가 매우 드물기 때문에 혹시 발생하더라도 자동화보다는 사람끼리 소통하여 해결하는 게 더 쉽다.
- 모든 LSC 프로세스에서 개별 샤드는 독립적으로 커밋할 수 있어야 한다. 샤드끼리 상호 의존하면 안 되고, 샤드 생성 단계에서 헤더 파일과 그 구현처럼 서로 연관된 변경을 같은 그룹으로 묶어줘야 한다.
- 대규모 변경의 샤드들도 리뷰하고 커밋하기 전에 프로젝트별 검사 항목들을 통과해야 한다.
- Rosie는 테스트를 수행하여 변경이 안전하다고 검증한 다음 적절한 리뷰어에게 메일을 보낸다.
- 구글은 LSC 전부터 실패하던 프리서브밋 검사는 거의 무시한다. 개별 프로젝트에서는 이 실패들을 수정하고 원래 작업을 이어서 진행하기가 쉽지만, 코드베이스 전반을 수정하는 LSC에까지 확장 적용하기는 어렵다.
- 로컬 코드 소유자들은 자신의 코드를 항시 오류가 없도록 관리해야 한다. 이는 인프라팀과의 사회적 계약이다.
- Rosie가 생성한 변경들도 표준 리뷰 프로세스를 거쳐야 한다.
- 각 코드의 소유자들이 LSC를 여느 변경만큼 엄격하게 살펴보지 않는 경우가 많다. 그래서 구글은 단순한 승인 차원이 아니라 맥락을 살펴 검토해야 하는 변경만을 로컬 소유자들에게 보낸다. 그 외의 변경은 글로벌 승인자에게만 보낸다. 글로벌 승인자란 리포지터리 전체의 어떤 변경도 승인할 수 있는 권한이 있는 사람이다.
- 글로벌 승인자를 활용할 때는 모든 개별 샤드가 글로벌 승인자 한 명에게 할당된다. 글로벌 승인자는 리뷰 대상 프로그래밍 언어와 라이브러리에 해박한 사람 중에 선정하며, 대규모 변경 작성자와 소통하여 어떤 종류의 변경이 예상되는지를 파악한다. 변경의 세부사항을 이해하고 예상되는 문제를 고려하여 작업 절차를 적절히 수정한다.
- 글로벌 리뷰어들은 변경 각각을 따로 리뷰하는 대신 패턴 기반의 도구를 사용하여 기대를 충족하는 변경들을 자동으로 승인한다. 그래서 병합 충돌이 나거나 도구가 자동 판단하지 못하는 등 일부의 변경들만 직접 리뷰하면 된다.
- 마지막으로 개별 변경을 커밋한다.
- 메일 보내기 단계 때와 마찬가지로 변경을 리포지터리로 실제 커밋하기 전에는 다양한 프로젝트별 프리커밋 검사가 진행된다.
- Rosie 덕에 널리 쓰이는 심볼의 이름, 클래스의 코드베이스 내 위치 등 예전에는 한 번 정해지면 되돌릴 수 없던 기술적 결정들이 이제는 최종 결정이 아닐 수도 있게 되었다.
- LSC마다 완료의 정의가 다르다. 옛 시스템을 완벽하게 제거해내야 할 수도, 중요한 참조들을 마이그레이션한 후 옛 참조들은 자연스럽게 사라지도록 내버려두는 것으로 끝낼 수도 있다.
- 하지만 어떤 경우든 대규모 변경이 애써 제거한 심볼이나 시스템이 다시 사용되는 일을 막아주는 방책이 꼭 필요하다. 구글은 Tricorder 프레임워크를 이용한다.
- 과거에는 한 번 결정하면 되돌릴 수 없던 설계를 LSC 덕분에 필요하면 사후에 변경할 수 있게 되었다.
- 핵심 인프라 메인테이너들은 구글 코드베이스의 많은 부분을 새로운 시스템, 언어 버전, 라이브러리 이디엄으로 마이그레이션할 수 있는 능력을 얻었다.
- LSC 프로세스는 되돌릴 수 없다고 여기던 기술적 결정들을 다시 생각해볼 수 있게 해준다.
- 전통적인 리팩터링 모델은 코드 규모가 커지면 한계를 드러낸다.
- LSC에 성공하려면 LSC를 습관처럼 진행해야 한다(코드베이스를 너무 내버려두면 고치기가 점점 어려워진다. 꾸준히 조금씩 개선해야 한다).