종속 항목 관리

이전 페이지를 살펴보면 한 가지 주제가 반복됩니다. 자체 코드를 관리하는 것은 비교적 간단하지만 종속 항목을 관리하는 것은 훨씬 더 어렵습니다. 종속 항목에는 여러 유형이 있습니다. 때로는 작업에 종속 항목이 있고(예: 출시를 완료로 표시하기 전에 문서를 푸시) 때로는 아티팩트에 종속 항목이 있습니다(예: 코드를 빌드하려면 최신 버전의 컴퓨터 비전 라이브러리가 필요함). 때로는 코드베이스의 다른 부분에 내부 종속 항목이 있고 때로는 조직 또는 서드 파티의 다른 팀이 소유한 코드 또는 데이터에 외부 종속 항목이 있습니다. 하지만 어떤 경우든 '이것을 가지려면 저것이 필요하다'는 개념은 빌드 시스템 설계에서 반복적으로 나타나며 종속 항목 관리는 빌드 시스템의 가장 기본적인 작업입니다.

모듈 및 종속 항목 처리

Bazel과 같은 아티팩트 기반 빌드 시스템을 사용하는 프로젝트는 모듈 집합으로 나뉘며 모듈은 BUILD 파일을 통해 서로에 대한 종속 항목을 표현합니다. 이러한 모듈과 종속 항목을 적절하게 구성하면 빌드 시스템의 성능과 유지보수에 필요한 작업량에 큰 영향을 미칠 수 있습니다.

세분화된 모듈 및 1:1:1 규칙 사용

아티팩트 기반 빌드를 구성할 때 가장 먼저 떠오르는 질문은 개별 모듈이 포함해야 하는 기능의 양을 결정하는 것입니다. Bazel에서 모듈java_library 또는 go_binary와 같은 빌드 가능한 단위를 지정하는 타겟으로 표시됩니다. 한쪽 극단에서는 루트에 하나의 BUILD 파일을 배치하고 해당 프로젝트의 모든 소스 파일을 재귀적으로 globbing하여 전체 프로젝트를 단일 모듈에 포함할 수 있습니다. 다른 극단에서는 거의 모든 소스 파일을 자체 모듈로 만들 수 있으므로 각 파일은 종속된 다른 모든 파일을 BUILD 파일에 나열해야 합니다.

대부분의 프로젝트는 이러한 극단 사이의 어딘가에 있으며 선택에는 a 성능과 유지보수 간의 절충이 필요합니다. 전체 프로젝트에 단일 모듈을 사용하면 외부 종속 항목을 추가할 때를 제외하고는 BUILD 파일을 건드릴 필요가 없지만 빌드 시스템은 항상 전체 프로젝트를 한 번에 빌드해야 합니다. 즉, 빌드의 일부를 병렬화하거나 배포할 수 없으며 이미 빌드한 부분을 캐시할 수도 없습니다. 파일당 하나의 모듈은 반대입니다. 빌드 시스템 은 빌드의 캐싱 및 예약 단계에서 최대한의 유연성을 갖지만 엔지니어는 참조하는 파일이 변경될 때마다 종속 항목 목록을 유지하는 데 더 많은 노력을 기울여야 합니다.

정확한 세분성은 언어에 따라 (그리고 종종 언어 내에서도) 다르지만 Google은 일반적으로 작업 기반 빌드 시스템에서 작성할 수 있는 것보다 훨씬 작은 모듈을 선 호합니다. Google의 일반적인 프로덕션 바이너리는 종종 수만 개의 타겟에 종속되며 적당한 규모의 팀도 코드베이스 내에서 수백 개의 타겟을 소유할 수 있습니다. 패키징에 대한 강력한 기본 제공 개념이 있는 Java와 같은 언어의 경우 각 디렉터리에는 일반적으로 단일 패키지, 타겟, BUILD 파일이 포함됩니다 (Bazel을 기반으로 하는 또 다른 빌드 시스템 인 Pants는 이를 1:1:1 규칙이라고 함). 패키징 규칙이 약한 언어는 BUILD 파일당 여러 타겟을 정의하는 경우가 많습니다.

작은 빌드 타겟의 이점은 실제로 대규모로 나타나기 시작합니다. 이는 더 빠른 분산 빌드와 타겟을 다시 빌드할 필요가 적기 때문입니다. 테스트가 시작되면 이점이 더욱 두드러집니다. 세분화된 타겟은 빌드 시스템이 특정 변경사항의 영향을 받을 수 있는 제한된 테스트 하위 집합만 실행하는 데 훨씬 더 스마트할 수 있음을 의미하기 때문입니다. Google은 작은 타겟을 사용하는 시스템적 이점을 믿기 때문에 개발자에게 부담을 주지 않도록 BUILD 파일을 자동으로 관리하는 도구에 투자하여 단점을 완화하는 데 상당한 진전을 이루었습니다.

buildifierbuildozer와 같은 이러한 도구 중 일부는 Bazel의 buildtools 디렉터리에서 사용할 수 있습니다.

모듈 공개 상태 최소화

Bazel 및 기타 빌드 시스템을 사용하면 각 타겟이 공개 상태(종속될 수 있는 다른 타겟을 결정하는 속성)를 지정할 수 있습니다. 비공개 타겟 은 자체 BUILD 파일 내에서만 참조할 수 있습니다. 타겟은 명시적으로 정의된 BUILD 파일 목록의 타겟 또는 공개 공개 상태의 경우 작업공간의 모든 타겟에 더 넓은 공개 상태를 부여할 수 있습니다.

대부분의 프로그래밍 언어와 마찬가지로 공개 상태를 최대한 최소화하는 것이 가장 좋습니다. 일반적으로 Google의 팀은 Google의 모든 팀에서 사용할 수 있는 널리 사용되는 라이브러리를 나타내는 경우에만 타겟을 공개합니다. 다른 사용자가 코드를 사용하기 전에 조정해야 하는 팀은 고객 타겟의 허용 목록을 타겟의 공개 상태로 유지합니다. 각 팀의 내부 구현 타겟은 팀이 소유한 디렉터리로만 제한되며 대부분의 BUILD 파일에는 비공개가 아닌 타겟이 하나만 있습니다.

종속 항목 관리

모듈은 서로를 참조할 수 있어야 합니다. 코드베이스를 세분화된 모듈로 나누는 것의 단점은 이러한 모듈 간의 종속 항목을 관리해야 한다는 것입니다 (도구를 사용하면 이를 자동화할 수 있음). 이러한 종속 항목을 표현하는 것은 일반적으로 BUILD 파일의 콘텐츠 대부분을 차지합니다.

내부 종속 항목

세분화된 모듈로 나뉜 대규모 프로젝트에서 대부분의 종속 항목은 내부 종속 항목일 가능성이 높습니다. 즉, 동일한 소스 저장소에서 정의되고 빌드된 다른 타겟에 대한 종속 항목입니다. 내부 종속 항목은 빌드를 실행하는 동안 미리 빌드된 아티팩트로 다운로드되는 것이 아니라 소스에서 빌드된다는 점에서 외부 종속 항목과 다릅니다. 이는 내부 종속 항목에 '버전'이라는 개념이 없다는 의미이기도 합니다. 타겟과 모든 내부 종속 항목은 항상 저장소의 동일한 커밋/수정 버전에서 빌드됩니다. 내부 종속 항목과 관련하여 주의해야 할 한 가지 문제는 전이 종속 항목을 처리하는 방법입니다 (그림 1). 타겟 A가 공통 라이브러리 타겟 C에 종속된 타겟 B에 종속된다고 가정해 보겠습니다. 타겟 A가 타겟 C에 정의된 클래스를 사용할 수 있어야 할까요?

전이 종속 항목

그림 1. 전이 종속 항목

기본 도구에 관한 한 이에는 문제가 없습니다. B와 C는 모두 빌드될 때 타겟 A에 연결되므로 C에 정의된 모든 기호는 A에 알려집니다. Bazel은 수년 동안 이를 허용했지만 Google이 성장하면서 문제가 발생하기 시작했습니다. B가 더 이상 C에 종속될 필요가 없도록 리팩터링되었다고 가정해 보겠습니다. 그러면 C에 대한 B의 종속 항목이 삭제되면 A와 B에 대한 종속 항목을 통해 C를 사용한 다른 타겟이 중단됩니다. 실제로 타겟의 종속 항목은 공개 계약의 일부가 되었으며 안전하게 변경할 수 없었습니다. 이는 종속 항목이 시간이 지남에 따라 누적되고 Google의 빌드가 느려지기 시작했음을 의미합니다.

Google은 결국 Bazel에 '엄격한 전이 종속 항목 모드'를 도입하여 이 문제를 해결했습니다. 이 모드에서 Bazel은 타겟이 직접 종속되지 않고 기호를 참조하려고 하는지 감지하고, 그렇다면 종속 항목을 자동으로 삽입하는 데 사용할 수 있는 오류 및 셸 명령어로 실패합니다. Google의 전체 코드베이스에 이 변경사항을 출시하고 수백만 개의 빌드 타겟을 모두 리팩터링하여 종속 항목을 명시적으로 나열하는 것은 수년에 걸친 노력이었지만 그만한 가치가 있었습니다. 이제 타겟에 불필요한 종속 항목이 적기 때문에 빌드가 훨씬 빨라졌으며 엔지니어는 종속된 타겟을 중단할 염려 없이 필요하지 않은 종속 항목을 삭제할 수 있습니다.

일반적으로 엄격한 전이 종속 항목을 적용하는 데는 절충이 필요했습니다. 자주 사용되는 라이브러리를 이제 우연히 가져오는 대신 여러 곳에 명시적으로 나열해야 하므로 빌드 파일이 더 자세해졌고 엔지니어는 BUILD 파일에 종속 항목을 추가하는 데 더 많은 노력을 기울여야 했습니다. 그 이후로 누락된 종속 항목을 자동으로 감지하고 개발자 개입 없이 BUILD 파일에 추가하여 이러한 작업을 줄이는 도구를 개발했습니다. 하지만 이러한 도구가 없더라도 코드베이스가 확장됨에 따라 절충이 그만한 가치가 있다는 것을 알았습니다. BUILD 파일에 종속 항목을 명시적으로 추가하는 것은 일회성 비용이지만 암시적 전이 종속 항목을 처리하면 빌드 타겟이 존재하는 한 지속적인 문제가 발생할 수 있습니다. Bazel 은 기본적으로 Java 코드에 엄격한 전이 종속 항목을 적용합니다.

외부 종속 항목

종속 항목이 내부 종속 항목이 아니면 외부 종속 항목이어야 합니다. 외부 종속 항목은 빌드 시스템 외부에서 빌드되고 저장되는 아티팩트에 대한 종속 항목입니다. 종속 항목은 아티팩트 저장소 (일반적으로 인터넷을 통해 액세스)에서 직접 가져오고 소스에서 빌드되는 대신 있는 그대로 사용됩니다. 외부 종속 항목과 내부 종속 항목의 가장 큰 차이점 중 하나는 외부 종속 항목에 버전이 있고 이러한 버전이 프로젝트의 소스 코드와 독립적으로 존재한다는 것입니다.

자동 종속 항목 관리와 수동 종속 항목 관리 비교

빌드 시스템을 사용하면 외부 종속 항목의 버전을 수동 또는 자동으로 관리할 수 있습니다. 수동으로 관리되는 경우 빌드 파일은 아티팩트 저장소에서 다운로드하려는 버전을 명시적으로 나열하며 종종 시맨틱 버전 문자열과 같은 1.1.4을 사용합니다. 자동으로 관리되는 경우 소스 파일은 허용되는 버전의 범위를 지정하고 빌드 시스템은 항상 최신 버전을 다운로드합니다. 예를 들어 Gradle을 사용하면 종속 항목의 주 버전이 1인 한 종속 항목의 부 버전 또는 패치 버전을 허용할 수 있도록 종속 항목 버전을 '1.+'로 선언할 수 있습니다.

자동으로 관리되는 종속 항목은 소규모 프로젝트에는 편리할 수 있지만 일반적으로 상당한 규모의 프로젝트나 둘 이상의 엔지니어가 작업하는 프로젝트에서는 재앙의 원인이 됩니다. 자동으로 관리되는 종속 항목의 문제는 버전이 업데이트되는 시기를 제어할 수 없다는 것입니다. 서드 파티가 시맨틱 버전 관리를 사용한다고 주장하더라도 서드 파티가 호환성이 손상되는 업데이트를 하지 않을 것이라고 보장할 방법이 없으므로 어느 날 작동하던 빌드가 다음 날 작동하지 않을 수 있으며 변경된 내용을 쉽게 감지하거나 작동 상태로 롤백할 방법이 없습니다. 빌드가 중단되지 않더라도 추적할 수 없는 미묘한 동작 또는 성능 변경이 있을 수 있습니다.

반대로 수동으로 관리되는 종속 항목은 소스 제어의 변경이 필요하므로 쉽게 검색하고 롤백할 수 있으며 이전 버전의 저장소를 체크아웃하여 이전 종속 항목으로 빌드할 수 있습니다. Bazel은 모든 종속 항목의 버전을 수동으로 지정해야 합니다. 적당한 규모에서도 수동 버전 관리의 오버헤드는 제공하는 안정성을 위해 그만한 가치가 있습니다.

단일 버전 규칙

라이브러리의 여러 버전은 일반적으로 여러 아티팩트로 표시되므로 이론적으로 동일한 외부 종속 항목의 여러 버전이 빌드 시스템에서 다른 이름으로 선언되지 않을 이유가 없습니다. 이렇게 하면 각 타겟이 사용할 종속 항목 버전을 선택할 수 있습니다. 실제로 많은 문제가 발생하므로 Google은 코드베이스의 모든 서드 파티 종속 항목에 엄격한 단일 버전 규칙 을 적용합니다.

여러 버전을 허용하는 데 가장 큰 문제는 다이아몬드 종속 항목 문제입니다. 타겟 A가 타겟 B와 외부 라이브러리의 v1에 종속된다고 가정해 보겠습니다. 나중에 타겟 B가 동일한 외부 라이브러리의 v2에 대한 종속 항목을 추가하도록 리팩터링되면 타겟 A는 이제 동일한 라이브러리의 두 가지 다른 버전에 암시적으로 종속되므로 중단됩니다. 실제로 타겟의 사용자가 이미 다른 버전에 종속되어 있을 수 있으므로 타겟에서 여러 버전이 있는 서드 파티 라이브러리에 새 종속 항목을 추가하는 것은 안전하지 않습니다. 단일 버전 규칙을 따르면 이러한 충돌이 불가능합니다. 타겟이 서드 파티 라이브러리에 종속 항목을 추가하면 기존 종속 항목은 이미 동일한 버전에 있으므로 함께 사용할 수 있습니다.

전이 외부 종속 항목

외부 종속 항목의 전이 종속 항목을 처리하는 것은 특히 어려울 수 있습니다. Maven Central과 같은 많은 아티팩트 저장소를 사용하면 아티팩트가 저장소의 다른 아티팩트의 특정 버전에 대한 종속 항목을 지정할 수 있습니다. Maven 또는 Gradle과 같은 빌드 도구는 기본적으로 각 전이 종속 항목을 재귀적으로 다운로드합니다. 즉, 프로젝트에 단일 종속 항목을 추가하면 총 수십 개의 아티팩트가 다운로드될 수 있습니다.

이는 매우 편리합니다. 새 라이브러리에 종속 항목을 추가할 때 해당 라이브러리의 각 전이 종속 항목을 추적하고 모두 수동으로 추가해야 하는 것은 큰 고통입니다. 하지만 큰 단점도 있습니다. 여러 라이브러리가 동일한 서드 파티 라이브러리의 여러 버전에 종속될 수 있으므로 이 전략은 단일 버전 규칙을 위반하고 다이아몬드 종속 항목 문제를 일으킵니다. 타겟이 동일한 종속 항목의 여러 버전을 사용하는 두 개의 외부 라이브러리에 종속된 경우 어떤 종속 항목을 가져올지 알 수 없습니다. 이는 새 버전이 일부 종속 항목의 충돌하는 버전을 가져오기 시작하면 외부 종속 항목을 업데이트하면 코드베이스 전체에서 관련이 없는 것처럼 보이는 오류가 발생할 수 있음을 의미하기도 합니다.

이러한 이유로 Bazel은 전이 종속 항목을 자동으로 다운로드하지 않습니다. 불행히도 만병통치약은 없습니다. Bazel의 대안은 저장소의 모든 외부 종속 항목과 저장소 전체에서 해당 종속 항목에 사용되는 명시적 버전을 나열하는 전역 파일이 필요하다는 것입니다. 다행히 Bazel은 Maven 아티팩트 집합의 전이 종속 항목이 포함된 이러한 파일을 자동으로 생성할 수 있는 도구를 제공합니다. 이 도구는 한 번 실행하여 프로젝트의 초기 WORKSPACE 파일 을 생성할 수 있으며 이 파일은 각 종속 항목의 버전 을 조정하기 위해 수동으로 업데이트할 수 있습니다.

여기서도 선택은 편의성과 확장성 사이의 선택입니다. 소규모 프로젝트는 전이 종속 항목 을 직접 관리하는 것에 대해 걱정하지 않는 것을 선호할 수 있으며 자동 전이 종속 항목을 사용하여 문제를 해결할 수 있습니다. 이 전략은 조직 과 코드베이스가 커짐에 따라 점점 매력이 떨어지고 충돌과 예기치 않은 결과가 점점 더 자주 발생합니다. 대규모에서는 종속 항목을 수동으로 관리하는 비용이 자동 종속 항목 관리로 인해 발생하는 문제를 처리하는 비용보다 훨씬 적습니다.

외부 종속 항목을 사용하여 빌드 결과 캐싱

외부 종속 항목은 일반적으로 소스 코드를 제공하지 않고 라이브러리의 안정적인 버전을 출시하는 서드 파티에서 제공합니다. 일부 조직에서는 자체 코드 중 일부를 아티팩트로 제공하여 다른 코드 조각이 내부 종속 항목이 아닌 서드 파티 종속 항목으로 종속될 수 있도록 할 수도 있습니다. 이론적으로 아티팩트 가 빌드하는 데는 시간이 오래 걸리지만 다운로드하는 데는 시간이 오래 걸리지 않는 경우 빌드 속도를 높일 수 있습니다.

하지만 이로 인해 많은 오버헤드와 복잡성이 발생합니다. 누군가가 이러한 각 아티팩트를 빌드하고 아티팩트 저장소에 업로드해야 하며 클라이언트는 최신 버전을 최신 상태로 유지해야 합니다. 시스템의 여러 부분이 저장소의 여러 지점에서 빌드되고 더 이상 소스 트리의 일관된 뷰가 없으므로 디버깅도 훨씬 더 어려워집니다.

아티팩트 빌드에 시간이 오래 걸리는 문제를 해결하는 더 좋은 방법은 앞에서 설명한 대로 원격 캐싱을 지원하는 빌드 시스템을 사용하는 것입니다. 이러한 빌드 시스템은 모든 빌드의 결과 아티팩트를 엔지니어 간에 공유되는 위치에 저장하므로 개발자가 다른 사용자가 최근에 빌드한 아티팩트에 종속된 경우 빌드 시스템은 빌드하는 대신 자동으로 다운로드합니다. 이렇게 하면 항상 동일한 소스에서 빌드되는 것처럼 빌드가 일관성을 유지하면서 아티팩트에 직접 종속되는 모든 성능 이점을 제공합니다. 이는 Google에서 내부적으로 사용하는 전략이며 Bazel은 원격 캐시를 사용하도록 구성할 수 있습니다.

외부 종속 항목의 보안 및 안정성

서드 파티 소스의 아티팩트에 종속되는 것은 본질적으로 위험합니다. 외부 종속 항목을 다운로드할 수 없는 경우 전체 빌드가 중단될 수 있으므로 서드 파티 소스 (예: 아티팩트 저장소)가 다운되면 가용성 위험이 있습니다. 보안 위험도 있습니다. 서드 파티 시스템 이 공격자에게 침해되면 공격자는 참조된 아티팩트를 자체 디자인으로 대체하여 빌드에 임의의 코드 를 삽입할 수 있습니다. 이러한 두 가지 문제는 종속된 아티팩트를 제어하는 서버에 미러링하고 빌드 시스템이 Maven Central과 같은 서드 파티 아티팩트 저장소에 액세스하지 못하도록 차단하여 완화할 수 있습니다. 절충은 이러한 미러를 유지하는 데 노력과 리소스가 필요하므로 이를 사용할지 여부는 프로젝트의 규모에 따라 달라지는 경우가 많습니다. 보안 문제는 각 서드 파티 아티팩트의 해시를 소스 저장소에 지정하도록 요구하여 아티팩트가 변조된 경우 빌드가 실패하도록 함으로써 오버헤드 없이 완전히 방지할 수도 있습니다. 문제를 완전히 해결하는 또 다른 대안은 프로젝트의 종속 항목을 벤더링하는 것입니다. 프로젝트가 종속 항목을 벤더링할 때 소스 또는 바이너리로 프로젝트의 소스 코드와 함께 소스 제어에서 종속 항목을 확인합니다. 이는 프로젝트의 모든 외부 종속 항목이 내부 종속 항목으로 변환됨을 의미합니다. Google은 이 접근 방식을 내부적으로 사용하여 Google 전체에서 참조되는 모든 서드 파티 라이브러리를 Google 소스 트리의 루트 에 있는 third_party 디렉터리에서 확인합니다. 하지만 이는 Google의 소스 제어 시스템이 매우 큰 모노리포를 처리하도록 맞춤설정되어 있기 때문에 Google에서만 작동하며 벤더링은 모든 조직에 적합한 옵션이 아닐 수 있습니다.