작업 기반 빌드 시스템

이 페이지에서는 작업 기반 빌드 시스템과 작동 방식, 작업 기반 시스템에서 발생할 수 있는 일부 정보 표시를 설명합니다. 셸 스크립트에 이어 작업 기반 빌드 시스템은 다음으로 논리적으로 발전한 빌드 시스템입니다.

작업 기반 빌드 시스템 이해

작업 기반 빌드 시스템에서 기본 작업 단위는 작업입니다. 각 작업은 모든 종류의 로직을 실행할 수 있는 스크립트이며 작업은 다른 작업을 그 전에 실행해야 하는 종속 항목으로 지정합니다. Ant, Maven, Gradle, Grunt, Rake와 같이 오늘날 사용되는 대부분의 주요 빌드 시스템은 작업 기반입니다. 대부분의 최신 빌드 시스템에서는 셸 스크립트 대신 엔지니어가 빌드 수행 방법을 설명하는 빌드 파일을 만들어야 합니다.

Ant 매뉴얼에서 이 예를 살펴보세요.

<project name="MyProject" default="dist" basedir=".">
   <description>
     simple example build file
   </description>
   <!-- set global properties for this build -->
   <property name="src" location="src"/>
   <property name="build" location="build"/>
   <property name="dist" location="dist"/>

   <target name="init">
     <!-- Create the time stamp -->
     <tstamp/>
     <!-- Create the build directory structure used by compile -->
     <mkdir dir="${build}"/>
   </target>
   <target name="compile" depends="init"
       description="compile the source">
     <!-- Compile the Java code from ${src} into ${build} -->
     <javac srcdir="${src}" destdir="${build}"/>
   </target>
   <target name="dist" depends="compile"
       description="generate the distribution">
     <!-- Create the distribution directory -->
     <mkdir dir="${dist}/lib"/>
     <!-- Put everything in ${build} into the MyProject-${DSTAMP}.jar file -->
     <jar jarfile="${dist}/lib/MyProject-${DSTAMP}.jar" basedir="${build}"/>
   </target>
   <target name="clean"
       description="clean up">
     <!-- Delete the ${build} and ${dist} directory trees -->
     <delete dir="${build}"/>
     <delete dir="${dist}"/>
   </target>
</project>

빌드 파일은 XML로 작성되며 작업 목록 (XML의 <target> 태그)과 함께 빌드에 관한 몇 가지 간단한 메타데이터를 정의합니다. Ant는 target이라는 단어를 사용하여 태스크를 나타내고 task라는 단어를 사용하여 명령어를 나타냅니다. 각 작업은 Ant에서 정의한 가능한 명령어 목록을 실행합니다. 여기에는 디렉터리 생성 및 삭제, javac 실행, JAR 파일 생성이 포함됩니다. 이 명령어 집합은 사용자가 제공한 플러그인을 통해 확장되어 모든 종류의 로직을 포괄할 수 있습니다. 각 작업은 종속 속성을 통해 종속 항목을 정의할 수도 있습니다. 이러한 종속 항목은 그림 1과 같이 비순환 그래프를 형성합니다.

종속 항목을 보여주는 아크릴 그래프

그림 1. 종속 항목을 보여주는 비순환 그래프

사용자는 Ant의 명령줄 도구에 작업을 제공하여 빌드를 수행합니다. 예를 들어 사용자가 ant dist를 입력하면 Ant는 다음 단계를 수행합니다.

  1. 현재 디렉터리에 build.xml라는 파일을 로드하고 파싱하여 그림 1과 같은 그래프 구조를 만듭니다.
  2. 명령줄에서 dist라는 작업을 찾아 compile라는 작업에 종속 항목이 있음을 발견합니다.
  3. compile라는 작업을 찾아 init라는 작업에 대한 종속 항목이 있음을 발견합니다.
  4. init라는 태스크를 찾아 종속 항목이 없음을 발견합니다.
  5. init 작업에 정의된 명령어를 실행합니다.
  6. 작업의 모든 종속 항목이 실행되었다는 가정 하에 compile 작업에 정의된 명령어를 실행합니다.
  7. 작업의 모든 종속 항목이 실행되었다는 가정 하에 dist 작업에 정의된 명령어를 실행합니다.

결국 dist 작업 실행 시 Ant가 실행하는 코드는 다음 셸 스크립트와 동일합니다.

./createTimestamp.sh
mkdir build/
javac src/* -d build/
mkdir -p dist/lib/
jar cf dist/lib/MyProject-$(date --iso-8601).jar build/*

문법이 제거되면 빌드 파일과 빌드 스크립트는 실제로 크게 다르지 않습니다. 하지만 우리는 이미 이렇게 많은 것을 얻었습니다. 다른 디렉터리에 새 빌드 파일을 만들어 함께 연결할 수 있습니다. 기존 작업에 종속된 새 작업을 임의의 복잡한 방식으로 쉽게 추가할 수 있습니다. 단일 작업의 이름만 ant 명령줄 도구에 전달하면 실행되어야 하는 모든 작업이 결정됩니다.

Ant는 2000년에 처음 출시된 오래된 소프트웨어입니다. Maven 및 Gradle과 같은 다른 도구는 지난 몇 년 동안 Ant를 개선했고, 외부 종속 항목의 자동 관리와 같은 기능과 XML을 사용하지 않는 깔끔한 문법을 추가하여 근본적으로 Ant를 대체했습니다. 하지만 이러한 최신 시스템의 특성은 동일하게 유지됩니다. 엔지니어가 원칙적인 모듈식으로 빌드 스크립트를 작업으로 작성하고 이러한 작업을 실행하고 이들 간의 종속 항목을 관리하는 도구를 제공할 수 있습니다.

작업 기반 빌드 시스템의 어두운 측면

이러한 도구는 기본적으로 엔지니어가 어떤 스크립트든 작업으로 정의할 수 있게 해주기 때문에 매우 강력하므로 개발자가 상상할 수 있는 모든 작업을 수행할 수 있습니다. 하지만 이러한 성능에는 단점이 수반되며 작업 기반 빌드 시스템은 빌드 스크립트가 더 복잡해짐에 따라 사용하기 어려워질 수 있습니다. 이러한 시스템의 문제는 실제로 엔지니어에게 너무 많은 전원을 공급하지만 시스템에는 충분하지 않은 전력을 공급한다는 것입니다. 시스템이 스크립트가 수행하는 작업을 전혀 알지 못하기 때문에 빌드 단계를 예약하고 실행하는 방식이 매우 보수적이어야 하므로 성능이 저하됩니다. 또한 시스템에서 각 스크립트가 올바르게 실행되고 있는지 확인할 수 있는 방법이 없기 때문에 스크립트는 복잡성이 커져 디버깅이 필요한 또 다른 요소가 되는 경향이 있습니다.

빌드 단계 동시 로드의 어려움

최신 개발 워크스테이션은 여러 빌드 단계를 병렬로 실행할 수 있는 여러 개의 코어가 있어 매우 강력합니다. 하지만 작업 기반 시스템은 작업 실행 가능해 보이는 경우에도 작업 실행을 병렬 처리할 수 없는 경우가 많습니다. 작업 A가 작업 B와 C에 종속된다고 가정해 보겠습니다. 작업 B와 C는 서로 종속되지 않으므로 시스템이 작업 A에 더 빨리 도달할 수 있도록 동시에 실행하는 것이 안전할까요? 아마도 동일한 리소스를 만지지 않는다면 그러나 둘 다 같은 파일을 사용하여 상태를 추적하고 동시에 실행하면 충돌이 발생할 수 있습니다. 일반적으로 시스템에서 알 수 있는 방법은 없으므로 이러한 충돌의 위험을 감수하거나(드물지만 디버그하기 매우 어려운 빌드 문제를 야기하거나) 전체 빌드를 단일 프로세스의 단일 스레드에서 실행되도록 제한해야 합니다. 이는 강력한 개발자 머신으로 인해 엄청난 낭비가 될 수 있으며 빌드를 여러 머신에 배포할 수 있는 가능성을 완전히 차단합니다.

증분 빌드 수행의 어려움

우수한 빌드 시스템을 사용하면 엔지니어가 안정적인 증분 빌드를 실행할 수 있으므로, 약간의 변경사항으로 인해 전체 코드베이스를 처음부터 다시 빌드할 필요가 없습니다. 이는 빌드 시스템이 느리고 앞서 언급한 이유로 빌드 단계를 병렬 처리할 수 없는 경우 특히 중요합니다. 하지만 안타깝게도 작업 기반 빌드 시스템도 이 부분에서 어려움을 겪습니다. 작업은 무엇이든 할 수 있기 때문에 일반적으로 이미 완료되었는지 확인할 방법은 없습니다. 대부분의 작업은 단순히 소스 파일 세트를 가져와서 컴파일러를 실행하여 바이너리 세트를 생성합니다. 따라서 기본 소스 파일이 변경되지 않은 경우에는 태스크를 다시 실행할 필요가 없습니다. 그러나 추가 정보 없이는 시스템이 이를 확실히 알 수 없습니다. 작업이 변경되었을 수 있는 파일을 다운로드하거나 실행할 때마다 다를 수 있는 타임스탬프를 작성할 수 있습니다. 정확성을 보장하기 위해 일반적으로 시스템은 각 빌드 중에 모든 작업을 다시 실행해야 합니다. 일부 빌드 시스템은 엔지니어가 작업을 재실행해야 하는 조건을 지정할 수 있도록 하여 증분 빌드를 사용하고자 합니다. 이 방법이 가능할 때도 있지만 생각나는 것보다 훨씬 어려운 문제인 경우도 있습니다. 예를 들어 C++와 같이 다른 파일에 의해 파일을 직접 포함할 수 있는 언어에서는 입력 소스를 파싱하지 않고 변경사항을 관찰해야 하는 전체 파일 세트를 결정할 수 없습니다. 엔지니어는 결국 지름길을 선택하게 되는데, 이러한 지름길은 작업 결과가 허용되지 않는 경우에도 재사용되는 드물고 답답한 문제로 이어질 수 있습니다. 이런 일이 자주 발생하면 엔지니어는 새로운 상태를 얻기 위해 모든 빌드 전에 클린을 실행하는 습관을 들이고 증분 빌드를 처음부터 구축하려는 목적을 완전히 무효화합니다. 작업을 재실행해야 하는 시기를 파악하는 것은 놀라울 정도로 미묘하며, 사람보다 기계가 더 잘 처리하는 작업입니다.

스크립트 유지 관리 및 디버깅의 어려움

마지막으로 작업 기반 빌드 시스템이 적용하는 빌드 스크립트는 사용하기 어려운 경우가 많습니다. 빌드 스크립트는 정밀도가 낮은 경우가 많지만 빌드 중인 시스템과 같은 코드이며 버그가 숨겨지기 쉬운 위치입니다. 다음은 작업 기반 빌드 시스템에서 작업할 때 매우 흔히 발생하는 버그의 예입니다.

  • 작업 A는 작업 B에 의존하여 특정 파일을 출력으로 생성합니다. 작업 B의 소유자는 다른 작업이 이 작업에 의존한다는 사실을 알지 못하기 때문에 다른 위치에서 출력을 생성하기 위해 변경합니다. 이는 누군가가 태스크 A를 실행하려고 하다가 실패한다는 것을 알게 될 때까지 감지할 수 없습니다.
  • 작업 A는 작업 A에 필요한 출력으로 특정 파일을 생성하는 작업 C에 종속되는 작업 B에 종속됩니다. 작업 B의 소유자는 더 이상 작업 C에 종속되지 않기로 결정했습니다. 그러면 작업 B가 작업 C에 전혀 신경 쓰지 않아도 작업 A가 실패합니다.
  • 새 작업의 개발자가 실수로 작업을 실행 중인 머신에 대해 도구 위치나 특정 환경 변수의 값과 같은 가정을 합니다. 이 작업은 사용자의 머신에서 작동하지만 다른 개발자가 시도할 때마다 실패합니다.
  • 작업에는 인터넷에서 파일을 다운로드하거나 빌드에 타임스탬프를 추가하는 등의 비확정 구성요소가 포함됩니다. 이제 사람들은 빌드를 실행할 때마다 다른 결과를 얻게 됩니다. 즉, 엔지니어가 자동화된 빌드 시스템에서 발생하는 장애나 장애를 항상 재현하고 수정할 수 있는 것은 아닙니다.
  • 종속 항목이 여러 개인 작업은 경합 상태를 생성할 수 있습니다. 작업 A가 작업 B와 작업 C 모두에 종속되고 작업 B와 C가 모두 동일한 파일을 수정하는 경우 작업 B와 C 중 어느 것이 먼저 완료되었는지에 따라 작업 A의 결과가 달라집니다.

여기에 설명된 작업 기반 프레임워크 내에서 이러한 성능, 정확성 또는 유지보수 문제를 해결할 수 있는 범용적 방법은 없습니다. 엔지니어가 빌드 중에 실행되는 임의의 코드를 작성할 수 있다면 시스템은 빌드를 항상 빠르고 올바르게 실행하기 위해 필요한 정보를 충분히 확보할 수 없습니다. 문제를 해결하려면 엔지니어의 손에서 힘을 빼고 이를 시스템에 다시 맡기고 시스템의 역할이 작업을 실행하는 것이 아니라 아티팩트를 생성하는 것으로 다시 개념화해야 합니다.

이 접근 방식을 통해 Blaze 및 Bazel과 같은 아티팩트 기반 빌드 시스템을 만들었습니다.