콘텐츠로 바로가기

Build Systems & Artifact Engineering

소스 코드를 하드웨어가 읽을 수 있는 바이너리나 패키지로 변환하는 빌드 공정과, 생성된 결과물의 버전과 무결성을 관리하는 아티팩트 보관 물리학을 다루는 학습 노드입니다.

Article
M

Me

hyunyoun's Blog

software-engineering-devopssoftware-engineeringdev-opsreliabilitycdbuild-systemsartifact-engineeringci11 min read

1. Overview

빌드 시스템과 아티팩트 엔지니어링(Build Systems & Artifact Engineering)은 소스코드(Text) 쪼가리들을 모아, 그 어떤 환경에 던져놔도 100% 똑같이 굴러가는 단단하고 불변하는 실행 덩어리(Artifact)로 제련해 내는 팩토리(Factory)의 물리학을 해부합니다.

학습자는 로컬 노트북에 깔린 라이브러리 버전 때문에 서버에서만 에러가 터지는 지옥을 끝내기 위해, 의존성(Dependencies)을 멱등하게 잠가버리는 패키지 매니저(NPM, Maven, Gradle)의 락(Lock) 메커니즘을 뜯어봅니다. 나아가 소스코드를 바이트코드로, 다시 도커 이미지로 구워내는 과정에서 필요 없는 쓰레기를 쳐내고 용량을 1/10로 압축하는 **멀티 스테이지 빌드(Multi-stage Build)**를 장악합니다. 마지막으로, 한 번 구워진 아티팩트는 절대 다시 빌드하지 않고 환경 변수만 갈아 끼워 Dev-Staging-Prod를 관통시키는 빌드 불변성(Build Immutability) 역량을 확보합니다.

2. Scope & Boundaries

In-Scope

  • Dependency Management: 패키지 매니저(Maven, Gradle, NPM), Lock 파일 메커니즘, Transitive Dependencies (전이적 의존성) 충돌 해결.
  • Build Physics: 컴파일(Compile), 링크(Link), 패키징(Packaging, e.g., JAR/WAR, 바이너리).
  • Artifact Immutability: "Build Once, Deploy Anywhere". 12-Factor App의 빌드/릴리즈/런 분리 원칙.
  • Container Artifacts: Dockerfile 작성법, 멀티 스테이지 빌드(Multi-stage Build), 레이어 캐싱(Layer Caching).

Out-of-Scope

  • 도커(Docker) 컨테이너 오케스트레이션: 쿠버네티스(Kubernetes)를 이용해 이 아티팩트를 수백 대의 서버에 뿌리고 로드밸런싱 하는 기술 \rightarrow 09-05-01/04 GitOps 또는 K8s 전용 모듈로 위임.
  • CI 파이프라인 게이트: 빌드 이전에 유닛 테스트와 린트를 돌리는 전략 \rightarrow 09-04-04 CI Gates 영역에서 기학습.

Boundaries

  • Source Code vs Artifact: 소스코드(*.java, *.ts)는 요리책(Recipe)에 불과합니다. 요리사의 컨디션(서버 OS, 설치된 Node 버전)에 따라 맛이 바뀝니다. 반면 아티팩트(.jar, 도커 이미지)는 이미 진공 포장되어 얼려진 3분 카레(Ready-to-eat)입니다. 전자레인지(런타임)만 있으면 무조건 똑같은 맛이 납니다. "코드를 배포한다"는 구시대적 발상을 버리고, "진공 포장된 아티팩트를 배포한다"는 클라우드 네이티브의 대원칙에 명확한 선을 긋습니다.

3. Counterexample

  • "내 PC에선 되는데요?" (환경 불일치): 신입 개발자가 npm install react를 치고 로컬에서 화려한 UI를 짰습니다. 서버에 소스코드를 올리고 서버에서 npm install을 쳤더니 화면이 하얗게 죽어버렸습니다. package-lock.json을 Git에 올리지 않아, 로컬에선 어제 배포된 v1.2가 깔렸고 오늘 서버에선 방금 릴리즈된 v1.3(버그 있음)이 깔린 탓입니다. 의존성(Dependency)을 '결정론적(Deterministic)'으로 잠그지(Lock) 않고 우연에 맡긴 참사입니다.
  • 돌연변이 아티팩트 (Mutable Build): 개발팀이 개발 서버(Dev)에 코드를 빌드해서 배포했습니다. QA 테스트를 통과했습니다. 자, 이제 실서버(Prod)에 배포할 차례입니다. 개발자는 '실서버용 설정 파일'을 넣고 실서버에서 코드를 "다시(Re-build)" 빌드했습니다. 그런데 그 찰나의 순간에 외부 라이브러리가 업데이트되며 실서버 빌드에만 다른 코드가 섞여 들어갔습니다. Dev에서 테스트한 놈과 Prod에 올라간 놈이 물리적으로 다른 객체가 되어버린, 12-Factor 위반의 재앙입니다.

4. Prerequisites

  • 컴파일러 기초 (Basic): 소스코드가 기계어나 바이트코드로 변환되는 과정. (05-02 Compilers)
  • CI 파이프라인 (Basic): 코드가 중앙에 통합되는 흐름. (09-04-04 CI Gates)

5. Learning Map

Sequence Core Cluster Objective & Description Evidence (BoK)
1 Dependency Physics & The Lock 내가 짠 1,000줄의 코드를 띄우기 위해 남이 짠 1,000만 줄의 코드를 끌어올 때 터지는 '버전 충돌 지옥'을 Lock 파일로 멱등하게 잠가버립니다. P1
2 The Immutability of Artifacts "환경마다 코드를 새로 빌드한다"는 야만적 행위를 멈추고, 한 번 구워진 황금 이미지는 절대 변하지 않는다는 12-Factor 원칙을 쥡니다. P5
3 Docker Image Engineering 개발용 무거운 컴파일러를 다 쳐내고, 오직 실행에 필요한 최소한의 런타임만 남겨 1GB짜리 이미지를 50MB로 압축하는 수술을 뜯어봅니다. Industry
4 The Layer Caching Strategy 소스코드 1줄 바꿨다고 도커가 10분 동안 빌드되는 빡침을 막기 위해, 잘 안 변하는 레이어를 밑에 깔고 자주 변하는 레이어를 위로 올리는 캐시 역학을 장악합니다. Industry

6. Learning Topics

Basic

Core Topic 01: 전이적 의존성과 잠금장치 (Dependency Physics & The Lock)

  • Why to Learn: 어제는 정상 배포됐는데 오늘 아침엔 아무것도 안 건드렸는데 빌드가 터지는 '의존성 지옥(Dependency Hell)'의 공포를 멱등성(Idempotency)으로 제압하기 위함입니다.
  • What to Learn:
    • Concepts: Package Manager (NPM, Maven, Gradle, pip), Semantic Versioning (SemVer), Transitive Dependency (전이적 의존성), Lock file (package-lock.json, pom.xml Dependency Management).
    • Skills: "A 라이브러리(버전 1)가 B(버전 2)를 부르고, C 라이브러리(버전 3)가 B(버전 1)를 부를 때" 터지는 충돌(Conflict)을 해소하고, ^1.2.3 같은 느슨한 버저닝을 Lock 파일로 픽스(Fix)하여 어느 컴퓨터에서 빌드하든 100% 똑같은 바이트가 나오게 통제하기.
  • How to Learn:
    • 1단계: 전이적 의존성 (Transitive Dependency): 나는 스프링 부트(Spring Boot) 딱 하나 설치했습니다. 그런데 다운로드 폴더를 보니 라이브러리가 100개가 깔려 있습니다. 스프링이 내장 톰캣을 부르고, 톰캣이 로깅 라이브러리를 부른 탓입니다.
    • 2단계: 느슨한 버전의 배신: package.jsonreact: "^18.0.0"이라고 썼습니다. ^ 기호는 "18점 대 버전 안에서 최신 마이너 업데이트가 나오면 알아서 받아라"는 뜻입니다. 3달 뒤 신입이 npm install을 치면 18.0.0이 아니라 18.3.0이 깔립니다. 환장할 노릇입니다.
    • 3단계: Lock의 철퇴: 그래서 기계가 package-lock.json을 만듭니다. 여기엔 18.0.2라고 정확한 해시값까지 박혀 있습니다. Git에 이 Lock 파일을 무조건 올려야, 서버든 동료 PC든 1바이트의 오차도 없는 동일한 패키지 생태계가 구축되는 원리를 해부합니다.
  • Implement: 의존성 충돌(Dependency Clash) 해결 시뮬레이션. 상황: Lib_ALog4j v1 요구, Lib_BLog4j v2 요구. 빌드 시 충돌(Class path 꼬임). 해결: Gradle/Maven의 ResolutionStrategy 또는 exclude 구문을 사용하여 강제로 Log4j v2로 통합(Evict)하는 코드 레벨의 교통정리 시각화.

Core Topic 02: 황금 이미지의 법칙 (The Immutability of Artifacts)

  • Why to Learn: "QA 테스트는 끝났고, 이제 실서버용으로 코드를 빌드할게요!"라는 말이 왜 클라우드 시대에 회사 문을 닫게 만들 수 있는 치명적 헛소리인지 깨닫기 위함입니다.
  • What to Learn:
    • Concepts: Artifact (아티팩트/빌드 결과물), Build Immutability (빌드 불변성), 12-Factor App (Build, Release, Run 분리).
    • Skills: 소스코드를 빌드해서 만든 1개의 아티팩트(app-v1.0.jar)를 Dev 서버, Staging 서버, Prod 서버에 재사용하고, 환경마다 달라지는 DB 접속 정보는 아티팩트를 뜯어고치지 않고 외부 환경 변수(ENV)로 주입하기.
  • How to Learn:
    • 1단계: 돌연변이의 탄생: 코드를 Dev에서 테스트했습니다. 통과했습니다. 이제 Prod에 올리기 위해 소스코드 내의 DB_URLProd-DB로 텍스트 교체한 뒤 다시 Build를 돌렸습니다. 그 3분 사이에 인터넷망에서 악성 NPM 패키지가 섞여 들어왔습니다. QA가 검증한 건 깨끗한 A였는데, 고객에게 배포된 건 오염된 A'가 됩니다.
    • 2단계: 진공 포장 (Build Once): 소스코드를 빌드(Build)하는 행위는 릴리즈 파이프라인의 맨 처음에 단 1번만 일어납니다. 이 결과물(Artifact)은 해시값이 찍힌 채 진공 포장되어 '아티팩트 저장소(Nexus, Docker Hub)'에 박제됩니다.
    • 3단계: 환경 변수 주입 (Release & Run): Prod 서버에 배포할 때는, 새로 빌드하는 게 아니라 아까 박제해 둔 그 진공 포장팩을 그대로 다운받아 런타임에 올립니다. 달라지는 DB 주소는 포장지를 뜯지 않고 외곽에서 주사기(환경 변수, Environment Variable)로 찔러 넣습니다(DI). 배포의 신뢰성이 100%에 수렴하는 불변의 아키텍처를 뜯어봅니다.
  • Implement: 12-Factor 배포 파이프라인 렌더링. [Stage: Build]: 소스코드 + npm ci \rightarrow app-image:v1.0 생성 (불변 객체). [Stage: QA 배포]: app-image:v1.0 + ENV(QA_DB) \rightarrow Run. (QA 통과). [Stage: PROD 배포]: (Build 단계 생략) \rightarrow 아까 그 app-image:v1.0 + ENV(PROD_DB) \rightarrow Run. "내가 테스트한 바로 그 바이트(Byte) 덩어리가 고객에게 간다"는 절대 진리 시각화.

Practical

Core Topic 03: 용량 다이어트, 멀티 스테이지 빌드 (Docker Multi-stage Build)

  • Why to Learn: 100MB짜리 앱 하나 띄우는데 도커 이미지가 1.5GB가 넘어 네트워크 전송비가 폭발하고 배포 속도가 거북이가 되는 멍청한 인프라 낭비를 박살 내기 위함입니다.
  • What to Learn:
    • Concepts: Dockerfile, Containerization, Multi-stage Build, Base Image, Build Dependencies vs Runtime Dependencies.
    • Skills: Dockerfile 안에서 FROM maven으로 무거운 컴파일러를 띄워 소스코드를 .jar로 굽고, 두 번째 FROM alpine-jre라는 초경량 런타임으로 넘어가 구워진 .jar 파일만 쏙 빼와서 이미지를 완성하는 이단 분리 설계하기.
  • How to Learn:
    • 1단계: 비만의 원인: 자바 소스코드를 .jar 파일로 컴파일하려면 JDK(Java Development Kit) 500MB짜리가 필요합니다. 멍청하게 Dockerfile을 짜면, 최종 배포 이미지 안에 이 무거운 JDK와 소스코드(.java)가 그대로 다 남아있게 됩니다.
    • 2단계: 멀티 스테이지 (수술실과 병실의 분리): Dockerfile 안에 FROM을 두 번 씁니다. 첫 번째 FROM은 톱과 망치가 있는 '공장(Builder)'입니다. 여기서 코드를 지지고 볶아 .jar를 만듭니다.
    • 3단계: 핵심만 추출: 두 번째 FROM은 톱과 망치가 없는 아주 작고 깨끗한 '실행 전용 방(Runtime)'입니다. 첫 번째 공장에서 완성된 .jar 파일 딱 1개만 COPY --from=builder로 가져옵니다. 1.5GB짜리 이미지가 50MB로 쪼그라들며, 해커가 컨테이너를 털어도 컴파일러가 없어 2차 해킹을 못 하는 완벽한 경량화 마법을 해부합니다.
  • Implement: Multi-stage Dockerfile 작성 및 용량 비교 벤치마크. Single-stage: FROM golang:1.20 \rightarrow 소스코드 복사 \rightarrow go build \rightarrow 결과물 크기 800MB. Multi-stage: FROM golang:1.20 AS builder (빌드) \rightarrow FROM scratch (빈 껍데기) \rightarrow COPY --from=builder /app /app \rightarrow 결과물 크기 5MB. 무려 160배의 용량 압축을 이뤄내는 컨테이너 엔지니어링 렌더링.

Advanced

Core Topic 04: 시간의 연금술, 도커 레이어 캐싱 (The Layer Caching Strategy)

  • Why to Learn: 소스코드의 오타 1글자 고쳤을 뿐인데 도커 빌드가 매번 처음부터 npm install을 다시 돌리며 10분씩 멈춰있는 빡침을, 단 1초 컷으로 줄여버리기 위함입니다.
  • What to Learn:
    • Concepts: Docker Image Layers, Layer Caching, Union File System, Instruction Ordering.
    • Skills: Dockerfile의 명령어(RUN, COPY) 순서를 짤 때, 자주 변하는 소스코드 복사(COPY src)를 밑으로 내리고, 잘 안 변하는 패키지 설치(COPY package.json && RUN npm install)를 위로 올려 캐시 적중률(Hit Ratio) 99% 달성하기.
  • How to Learn:
    • 1단계: 레이어(Layer)의 물리학: 도커 이미지는 하나의 거대한 덩어리가 아니라, 여러 장의 투명한 셀로판지(Layer)가 겹쳐진 형태입니다. Dockerfile의 줄 하나하나가 셀로판지 1장입니다.
    • 2단계: 캐시 붕괴의 원리: 도커는 빌드할 때 위에서부터 내려오며 "어? 이 줄은 어제랑 똑같네? 캐시 써야지" 하다가, 단 한 줄이라도 변경점이 발견되면 그 밑에 있는 모든 캐시를 다 찢어버리고 새로 빌드합니다.
    • 3단계: 순서의 미학: 멍청한 도커파일: COPY . /app (소스 전체 복사) \rightarrow RUN npm install. 이러면 소스코드 1글자만 바꿔도 소스 복사 단계에서 캐시가 깨지니, 매번 5분 걸리는 npm install을 다시 돌립니다.
    • 4단계: 천재의 도커파일: COPY package.json (설정만 복사) \rightarrow RUN npm install \rightarrow COPY src /app (나머지 소스 복사). 소스코드가 바뀌어도 3단계에서 캐시가 깨지므로, 1~2단계의 5분짜리 설치 작업은 영원히 캐시를 타게 됩니다. 순서 하나 바꿨을 뿐인데 파이프라인의 체증이 뻥 뚫리는 기적을 뜯어봅니다.
  • Implement: 도커 레이어 파괴 메커니즘 디버깅 모사. 명령어 1: COPY requirements.txt . 명령어 2: RUN pip install -r requirements.txt (소요 시간 3분). 명령어 3: COPY app.py . 개발자 Action: app.py 내용 수정. Build Result: 명령어 1(Cache Hit) \rightarrow 명령어 2(Cache Hit, 0초 소요) \rightarrow 명령어 3(Cache Miss, 새로 복사). 총 빌드 시간 2초 컷 완료. 아키텍트의 설계적 통찰 렌더링.

7. Terminology

Term (EN / ko, abbr) 1문장 정의 단계(기본/권장/실무/심화) 역할/맥락 관련 개념 유사/대비/함께 사용 오해 포인트 Evidence(Primary/Secondary/Industry) Flags(core)
Artifact (아티팩트/결과물) 소스코드 텍스트가 컴파일러와 패키저를 거쳐 만들어진, 그 어떤 서버에 던져놔도 똑같이 동작하는 불변의 실행 덩어리(예: .jar, 도커 이미지)입니다. 기본 배포의 대상이자 불변성의 단위 Build Immutability / CI/CD Source Code 아티팩트 안에 환경 변수(DB 비밀번호 등)를 하드코딩해서 박아넣으면 아티팩트의 재사용성이 파괴되므로 최악의 안티 패턴임 P1:CS2023 core
Lock File (잠금 파일) ^1.0.0처럼 느슨하게 적힌 라이브러리 버전 요구사항을, 현재 테스트를 통과한 정확한 버전(예: 1.0.4)과 해시값으로 박제하여 멱등성을 보장하는 안전장치입니다. 권장 의존성 트리의 멱등성 및 일관성 확보 Dependency Manager / Transitive Dependency package.json / pom.xml "어차피 자동 생성되는 파일인데 Git에 올려야 되나?" 하고 .gitignore에 넣는 순간, 팀원들의 로컬 환경이 다 깨지는 지옥이 열림 P5:SFIA core
Multi-stage Build (멀티 스테이지 빌드) 하나의 Dockerfile 안에서 무거운 '컴파일용 컨테이너'를 띄워 빌드한 뒤, 결과물만 쏙 빼서 가벼운 '실행용 컨테이너'로 옮겨 담아 최종 이미지 용량을 1/10로 박살 내는 기법입니다. 실무 도커 이미지 경량화 및 보안 강화(컴파일러 제거) Dockerfile / Base Image Single-stage Build 런타임 이미지에 소스코드(*.java) 쪼가리나 빌드 툴(Maven)이 남아있다면 멀티 스테이지 빌드를 실패한 것임 Industry core
Layer Caching (레이어 캐싱) 도커가 빌드할 때 명령어(COPY, RUN)마다 투명한 셀로판지(Layer)를 구워두고, 파일이 1바이트라도 안 바뀌었으면 예전 셀로판지를 재활용하여 빌드 시간을 0초로 만드는 최적화 마법입니다. 심화 도커 빌드 파이프라인 속도 최적화 Docker Image / Cache Invalidation CI Dependency Cache 윗줄의 레이어가 변경되어 캐시가 깨지면(Invalidate), 그 아랫줄의 모든 명령어는 파일이 안 바뀌었어도 강제로 다시 실행(Rebuild)됨을 잊지 말아야 함 Industry core

8. References

Primary

  • [P1] CS2023 - Software Engineering (SE) - Software Evolution (Build systems and release management)
  • [P5] SFIA - Release and Deployment (RELM) - Artifact management and build processes

Secondary

  • [The Twelve-Factor App] Adam Wiggins - V. Build, release, run (Strict separation of stages)
  • [Docker Best Practices] - Use multi-stage builds, Leverage build cache

Industry

  • [MartinFowler.com] - Immutable Server
  • [NPM Documentation] - package-lock.json (Understanding dependency lock physics)

9. Final Checklist

Primary

  • 개발 환경(Dev)과 운영 환경(Prod)의 차이로 버그가 발생했을 때, package.json만 Git에 올리고 package-lock.json을 누락하여 서로 다른 버전의 의존성이 다운로드된 치명적 원인을 지적할 수 있는가?
  • 12-Factor App 원칙에 기반하여, 배포 환경(Staging vs Prod)이 다르다고 코드를 2번 새로 빌드(Build)하는 야만적 행위를 금지하고, 하나의 아티팩트(Artifact)에 환경 변수만 주입하는 구조를 설계할 수 있는가?

Secondary

  • 크기가 1GB에 육박하는 Spring Boot 도커 이미지를 마주했을 때, 멀티 스테이지 빌드(Multi-stage Build)를 도입하여 빌드 도구(Maven/Gradle)와 JDK를 걷어내고 JRE만 남겨 100MB 이하로 압축하는 Dockerfile을 작성할 수 있는가?
  • 라이브러리 간의 전이적 의존성(Transitive Dependency)이 충돌하여 클래스 경로(Classpath) 에러가 터졌을 때, 패키지 매니저의 의존성 트리(Dependency Tree)를 분석하고 exclude 문법을 사용해 특정 버전을 강제로 쳐낼 수 있는가?

Industry

  • 도커 이미지를 빌드할 때마다 npm install에 5분이 걸리는 파이프라인을 최적화하기 위해, 소스코드 COPY 명령어보다 package.json COPY 명령어를 상단에 배치하여 레이어 캐싱(Layer Caching) 적중률을 극대화할 수 있는가?
  • 생성된 도커 이미지 안에 클라우드 접속 키(Secret)가 임시 파일로 쓰였다가 RUN rm 명령어로 삭제된 경우, 도커 레이어(Layer)의 불변성 원리 때문에 여전히 하위 레이어에 해킹 가능한 평문으로 남아있음을 경고하고 Multi-stage로 이를 은폐할 수 있는가?

CI/CD & Delivery Platform

1 / 4