주요 내용
- 개발자들의 생산성 유지를 위해 제대로 된 빌드 시스템은 필수
- 빌드 시스템에 적절한 제한을 두면 개발자는 더 편리해짐
- 아티팩트 중신으로 구성된 빌드 시스템이 태스크 중심보다 확장성과 안정성 면에서 우수
- 아티팩트와 의존성을 정의할 때 모듈을 작게 나누자 -> 작은 모듈들이 병렬 빌드와 증분 빌드의 이점을 더 잘 활용하기 때문
- 외부 의존성의 버전도 명확하게 버전관리 해야함 -> 단순히 '최신' 버전에 의존해서는 안됨
구글은 엔지니어가 빠르고 안정적으로 빌드할 수 있도록 설립 초기부터 자체 빌드 시스템을 구축하는데 엄청나게 투자했습니다. 구글이 생각하는 모던 빌드 시스템이란 무엇이고 이런 시스템을 어떻게 활용하는지에 대해 알아봅시다.
모든 빌드 시스템의 기본 목적은 엔지니어들이 작성한 소스 코드를 기계가 읽을 수 있는 바이너리로 변환하는 것입니다. 좋은 빌드 시스템은 다음 두가지를 최적화 합니다.
과거 빌드 시스템은 속도와 정확성 사이에서 절충 하려다 보니 빌드 결과가 일관되지 못하는 문제가 있엇습니다. 구글의 빌드 시스템 Bazel은 속도와 정확성 어느 하나 희생하지 않고 빌드 시스템이 언제나 효율적이며 일관된 빌드를 수행하는 것을 목표로 했습니다.
빌드 시스템 없이 프로젝트 규모를 확장하려고 하면 온갖 난관에 부딪힐 것입니다. 우선 컴파일러로는 불충분합니다. 컴파일러는 외부 의존성을 다루는 방법을 전혀 모르기 때문입니다. 셸 스크립트로도 부족합니다. 시스템이 조금만 복잡해져도 스크립트를 개발하고 관리하는데 어려워집니다. 그리고 스크립트로 의존성을 매번 정확한 순서로 빌드하도록 하면 속도가 매우 느립니다.
그러므로 제대로된 빌드 시스템이 꼭 있어야만 합니다.
작업 사이의 의존성, ARTIFACT 사이의 의존성, 자기 코드베이스 내부 의존성, third-party가 소유한 코드나 데이터로의 외부 의존성 등 다양한 의존성이 있습니다. 어떤 경우이든 빌드 시스템을 구축하는 데는 "이걸 하려면 저게 필요해" 패턴이 반복되며, 이러한 의존성을 관리하는 일이 가장 기본이 되는 작업입니다.
기본 작업 단위는 태스크 입니다. 셸 스크립트, Ant, Maven, Gradle, Grunt 등이 대표적인 태스크 기반 빌드 시스템입니다. 대부분 모던 빌드 시스템은 셸 스크립트 대신 빌드 파일을 이용합니다. 빌드 파일은 빌드 수행 방법을 기술한 파일입니다. 이는 대부분 엔지니어가 작성합니다. 빌드 파일을 사용하면 다음과 같은 이점이 있습니다.
하지만 단점이 있습니다. 태스크 기반 빌드 시스템은 빌드 스크립트가 커져서 복잡해질 수록 다루기 어렵습니다. 시스템은 스크립트가 뭘 하는지 알 수 없으므로 각각의 빌드 단계를 매우 보수적으로 실행할 수밖에 없고, 이는 성능 저하의 원인이 됩니다. 또한 시스템은 각 스크립트가 할 일을 올바르게 수행하고 있는지 확인할 방법이 없습니다.
엔지니어에게 너무 많은 힘을, 시스템에는 충분하지 못한 힘을 준다.
주요 단점
- 빌드 단계들을 병렬로 실행하기 어렵다.
- 증분 빌드(incremental build)를 수행하기 어렵다.
- 스크립트를 유지보수하고 디버깅 하기 어렵다.
태스크 기반 프레임워크에서는 성능, 정확성 유지보수성 문제를 한번에 해결할 수 있는 방법이 없습니다. 빌드 중에 실행되는 임의의 코드(빌드 스크립트)를 엔지니어가 작성할 수 있다는 것은 빌드를 빠르고 정확하게 수행하는 데 필요한 정보 일부가 누락될 수 있다는 뜻입니다.
아티팩트 기반 빌드 시스템은 태스크 기반보다 엔지니어의 힘을 제한합니다. 엔지니어는 시스템에게 '무엇'을 빌드할지 정해줄 수 있지만 '어떻게'는 시스템이 알아서 합니다. 어떤 도구를 언제 실행할지를 빌드 시스템이 완전히 통제하므로 정확성을 보장하고 효율성을 높일 수 있습니다. 프로그래머 입장에서는 유연성이 줄어드는 대신 빌드의 각 단계에서 무슨 일이 이루어지는지를 빌드 시스템이 알게 됩니다. 빌드 시스템은 이 지식을 활용해 빌드 프로세스를 병렬화하고 최대한 많은 것을 재사용해 효율을 극대화시킵니다.
기능적 관점에서 아티팩트 기반 빌드 시스템은 함수형 프로그래밍과 비슷한 점이 많습니다. 함수형 언어는 문제들을 병렬화하기가 쉽고 정확성을 보장해줍니다. 함수형 프로그램으로 표현하기 가장 쉬운 문제로는 일련의 규칙이나 함수를 이용해 데이터 조각 하나를 다른 데이터로 변환하기가 있습니다. 아티팩트 기반 빌드 시스템도 마찬가지 입니다.
Bazel의 특징
- 도구도 의존성으로 취급
빌드에 필요한 도구를 선언하도록해 언제 어느 시스템에서 빌드하든 정확한 도구들이 먼저 갖춰지도록 한다. 플랫폼에 의존하는 문제는 툴체인을 이용해 해결한다.- 빌드 시스템 확장
커스텀 규칙을 추가해 타깃 종류를 확장하는 방법을 제공한다.- 환경 격리하기
샌드박싱 기술로 액션들끼리 같은 파일을 써 서로 충돌하는 문제를 막았다. 액션은 입력으로 선언하지 않은 파일은 읽을 수 없고, 출력으로 선언하지 않은 파일에 쓰면 액션 종료 즉시 버려진다. 심지어 액션들이 네트워크로 서로 통신할 수 없다.- 외부 의존성 명확히 드러내기
의존성 변경은 의식적으로 진행해야 하지만 중앙에서 한 번만 이루어져야 한다. 버전 충돌 문제는 외부 의존성 각각의 암호화 해시를 워크스페이스 차원의 매니페스트 파일에 기록하여 해결한다. 해시를 통해 전체 파일을 소스 관리하에 두지 않고도 고유하게 식별 가능해진다.
분산 빌드란 단위 작업들을 여러 컴퓨터에 뿌려 빌드한 후 취합해 최종 결과를 만들어주는 기술입니다. 빌드 단위를 충분히 작게 쪼갤 수 있다면 큰 빌드도 원하는 시간 내 끝낼 수 있습니다.
원격 캐싱
빌드를 수행하는 모든 시스템, 즉 개발자 컴퓨터와 지속적 통합 시스템 모두 공통의 원격 캐시 서비스를 참조하는 모양입니다. 자주 변경되지 않는 저수준 라이브러리는 한 번 빌드되면 많은 사용자에게 공유됩니다. 이런 식으로 구글은 빌드 시스템 운영 비용을 크게 절감하고 있습니다.
원격 캐시 시스템이 제역할을 하려면 빌드 시스템이 빌드를 완벽하게 재현할 수 있어야 합니다. 참고로 캐시된 아티팩트들의 key로는 해당 타깃과 입력값들의 해시 모두가 제공되어야 합니다. 아티팩트를 다운로드 하는 시간이 새로 빌드할 때보다 빨라야 원격 캐시가 의미 있습니다.
원격 실행
원격 실행은 빌드를 하는 '실제' 작업들을 여러 워커에 나눠 수행하는 기술입니다. 빌드 마스터는 요청 받은 빌드를 구성하는 액션들을 스케줄링 합니다. 이때 worker pool이 활용됩니다. 빌드 환경은 필요한 모든 것을 완벽하게 자기 기술(self-descriptive)해야 워커들이 사람의 개입없이 동작할 수 있습니다.
분산 빌드 @ 구글
ObjFS는 구글 버전의 원격캐시입니다. ObjFS는 백엔드와 프론트엔드로 구성됩니다. 백엔드는 구글의 프로덕션 머신들 전체에 배포된 빅테이블에 결과를 저장합니다. 프론트엔드는 Objfsd라는 이름의 FUSE 데몬이 각 개발자의 컴퓨터에서 실행되는 형태입니다. 파일의 내용은 사용자가 직접 요청할 때만 on-demand로 다운로드합니다. 덕분에 네트워크와 디스크 사용을 크게 줄여주며, 모든 빌드 결과를 개발자 컴퓨터에 저장할 때보다 빌드가 두 배나 빨라집니다.
Forge는 구글의 원격 실행 시스템입니다. Blaze단의 Forge 클라이언트(Forge Distributor)가 데이터센터에서 실행 중인 스케줄러에 수행할 액션들을 전송합니다. 스케줄러는 액션의 결과를 캐싱해두었다가 똑같은 액션이 요청될 경우 즉시 돌려줍니다. 캐시된 결과가 없다면 해당 액션을 큐에 추가합니다. 큐에서 가져온 액션들을 실행하고, 결과는 ObjFS 빅테이블에 직접 저장합니다.
프로젝트의 각 모듈은 다른 모듈과의 의존 관계를 BUILD 파일에 기술합니다. 이 모듈과 의존성을 어떻게 구성하느냐가 빌드 시스템의 성능과 감당할 수 있는 작업량에 지대한 영향을 줍니다.
작은 모듈 사용
Bazel에서 모듈은 빌드 가능한 단위를 지정하는 타깃을 말합니다. 모듈의 단위를 크게 잡으면 build 파일을 관리하기 쉽겠지만 빌드 단계를 병렬로 실행하거나 분산할 수 없습니다. 반대로 작게 잡으면 빌드 시스템은 캐시와 분산 빌드를 최대로 이용할 수 있습니다. 하지만 엔지니어가 build 파일을 유지보수하기 힘들어집니다. 구글은 작은 단위의 모듈을 선호합니다.
자바의 경우 각 디렉토리가 보통 하나의 패키지, 타깃, 빌드 파일을 갖습니다.
이를 [1:1:1 규칙]이라 합니다.
모듈 가시성 최소화
Bazel을 포함한 여러 빌드 시스템은 타깃이 가시성을 명시할 수 있게 합니다. 가시성이란 자신에게 의존할 수 있는 타깃의 범위를 지정하는 속성입니다. 가시 범위는 가능한 좁히는 것이 좋습니다.
의존성 관리
내부 의존성
내부 의존성은 의존하는 타깃 대부분이 같은 소스 리포지터리에서 정의되고 빌드 됩니다. 이는 소스로부터 빌드된다는 점에서 외부 의존성과 차이가 납니다. 또한 '버전'이란 개념이 없습니다.
내부 의존성에서 주의할 점은 전이 의존성을 어떻게 취급하느냐 입니다. 구글은 Blaze에 엄격한 전이 의존성 모드(strict transitive dependency mode)를 도입하여 문제를 해결했습니다.
외부 의존성
외부 의존성은 빌드 시스템 바깥에서 저장되어 있는 아티팩트를 발합니다. 아티팩트 리포지터리에서 직접 가져와 그대로 이용합니다. 또한 내부 의존성과 달리 '버전'이 있습니다.
Vendoring: 외부 의존성을 자신의 관리 하에 두는 것