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)를 이용해 이 아티팩트를 수백 대의 서버에 뿌리고 로드밸런싱 하는 기술 09-05-01/04 GitOps 또는 K8s 전용 모듈로 위임.
- CI 파이프라인 게이트: 빌드 이전에 유닛 테스트와 린트를 돌리는 전략 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
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.xmlDependency Management). - Skills: "A 라이브러리(버전 1)가 B(버전 2)를 부르고, C 라이브러리(버전 3)가 B(버전 1)를 부를 때" 터지는 충돌(Conflict)을 해소하고,
^1.2.3같은 느슨한 버저닝을 Lock 파일로 픽스(Fix)하여 어느 컴퓨터에서 빌드하든 100% 똑같은 바이트가 나오게 통제하기.
- Concepts: Package Manager (NPM, Maven, Gradle, pip), Semantic Versioning (SemVer), Transitive Dependency (전이적 의존성), Lock file (
- How to Learn:
- 1단계: 전이적 의존성 (Transitive Dependency): 나는 스프링 부트(Spring Boot) 딱 하나 설치했습니다. 그런데 다운로드 폴더를 보니 라이브러리가 100개가 깔려 있습니다. 스프링이 내장 톰캣을 부르고, 톰캣이 로깅 라이브러리를 부른 탓입니다.
- 2단계: 느슨한 버전의 배신:
package.json에react: "^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_A는Log4j v1요구,Lib_B는Log4j v2요구. 빌드 시 충돌(Class path 꼬임).해결: Gradle/Maven의ResolutionStrategy또는exclude구문을 사용하여 강제로Log4j v2로 통합(Evict)하는 코드 레벨의 교통정리 시각화.
Recommended
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_URL을Prod-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%에 수렴하는 불변의 아키텍처를 뜯어봅니다.
- 1단계: 돌연변이의 탄생: 코드를 Dev에서 테스트했습니다. 통과했습니다. 이제 Prod에 올리기 위해 소스코드 내의
- Implement: 12-Factor 배포 파이프라인 렌더링.
[Stage: Build]: 소스코드 +npm ciapp-image:v1.0생성 (불변 객체).[Stage: QA 배포]:app-image:v1.0+ENV(QA_DB)Run. (QA 통과).[Stage: PROD 배포]: (Build 단계 생략) 아까 그app-image:v1.0+ENV(PROD_DB)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차 해킹을 못 하는 완벽한 경량화 마법을 해부합니다.
- 1단계: 비만의 원인: 자바 소스코드를
- Implement: Multi-stage Dockerfile 작성 및 용량 비교 벤치마크.
Single-stage:FROM golang:1.20소스코드 복사go build결과물 크기 800MB.Multi-stage:FROM golang:1.20 AS builder(빌드)FROM scratch(빈 껍데기)COPY --from=builder /app /app결과물 크기 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(소스 전체 복사)RUN npm install. 이러면 소스코드 1글자만 바꿔도 소스 복사 단계에서 캐시가 깨지니, 매번 5분 걸리는npm install을 다시 돌립니다. - 4단계: 천재의 도커파일:
COPY package.json(설정만 복사)RUN npm installCOPY src /app(나머지 소스 복사). 소스코드가 바뀌어도 3단계에서 캐시가 깨지므로, 1~2단계의 5분짜리 설치 작업은 영원히 캐시를 타게 됩니다. 순서 하나 바꿨을 뿐인데 파이프라인의 체증이 뻥 뚫리는 기적을 뜯어봅니다.
- 1단계: 레이어(Layer)의 물리학: 도커 이미지는 하나의 거대한 덩어리가 아니라, 여러 장의 투명한 셀로판지(Layer)가 겹쳐진 형태입니다.
- 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) 명령어 2(Cache Hit, 0초 소요) 명령어 3(Cache Miss, 새로 복사). 총 빌드 시간 2초 컷 완료. 아키텍트의 설계적 통찰 렌더링.
7. Terminology
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로 이를 은폐할 수 있는가?