빌드
고친 사람 github-actions[bot]
빌드는 사람이 쓴 소스 코드를 컴퓨터가 돌릴 수 있는 형태로 바꾸는 일입니다. 코드를 고치면 이 일을 다시 거쳐야 바뀐 내용이 프로그램에 들어갑니다. 그렇게 해서 나온 결과물 한 벌도 빌드라고 부릅니다.
쉽고 빠른 이해
빌드는 소스 코드를 실행할 수 있는 결과물로 바꾸는 일입니다. C 로 쓴 파일 여러 개를 묶어 실행 파일 하나로 내놓는 것이 빌드입니다.
이게 없으면 사람이 읽으라고 쓴 글을 컴퓨터가 읽어야 합니다. 프로세서는 소스 코드를 모릅니다. 남이 만든 코드를 어디서 받아 어떻게 이어 붙일지도 매번 손으로 해야 합니다.
돌아가는 방식은 이렇습니다.
- 소스 파일을 하나씩 기계가 읽는 형태로 옮깁니다
- 흩어진 조각과 받아 온 코드를 하나로 이어 붙입니다
- 실행에 필요한 것을 배포할 묶음 하나에 담습니다
대가가 있습니다. 코드를 한 줄만 고쳐도 빌드를 다시 돌려야 해서 결과를 보기까지 기다립니다. 빌드를 돌린 컴퓨터마다 환경이 달라 한쪽에서만 성공하는 일도 생깁니다.
상세
빌드는 소스 코드를 결과물로 바꾸는 일입니다. 원고를 인쇄해 책으로 묶는 일에 가깝습니다. 독자가 받는 것은 원고가 아니라 책이고, 원고를 고치면 책을 다시 찍습니다.
소스 코드가 왜 그대로 못 도는지부터 봅니다. 그다음 빌드가 지나는 단계와 그것이 깨지는 대목까지 차례로 봅니다.
사람이 읽는 코드와 기계가 읽는 코드
소스 코드는 사람이 읽고 고치라고 쓴 글입니다. 프로세서는 이 글을 모릅니다. 프로세서가 읽는 것은 기계어뿐입니다. 기계어는 프로세서가 바로 해석하는 숫자 명령의 나열입니다.
그래서 누군가는 소스 코드를 기계어로 옮겨 줘야 합니다. 그 일을 하는 프로그램이 컴파일러입니다. C 로 쓴 파일은 컴파일러를 거쳐 실행 파일이 된 뒤에야 돌아갑니다.
언어마다 어디까지 옮기는지가 다릅니다. Java 나 Kotlin 은 기계어가 아니라 바이트코드까지만 갑니다. 바이트코드는 특정 프로세서가 아니라 가상 머신이 읽도록 만든 중간 명령어입니다.
가상 머신은 그 명령어를 받아 지금 쓰는 프로세서의 기계어로 옮겨 실행해 주는 프로그램입니다. 기계어로 옮기는 마지막 몫이 프로그램을 돌릴 때로 미뤄지는 셈입니다.
Python 처럼 인터프리터가 소스를 바로 읽어 실행하는 언어도 있습니다. 이런 언어에도 빌드는 남습니다. 남이 만든 코드를 받아 오고 실행에 필요한 파일을 한 묶음으로 담는 일이 그대로 있기 때문입니다.
세 갈래를 나란히 놓으면 기계어로 옮기는 몫이 어디에 떨어지는지가 보입니다.
flowchart TD
subgraph BT["빌드 때"]
S1["C 소스"] -->|컴파일러| M["기계어 · 실행 파일"]
S2["Java · Kotlin 소스"] -->|컴파일러| B["바이트코드"]
S3["Python 소스"]
end
subgraph RT["실행 때"]
R1["프로세서가 바로 돈다"]
R2["기계어로 옮기며 돈다"]
R3["소스를 읽으며 돈다"]
end
M --> R1
B -->|가상 머신| R2
S3 -->|인터프리터| R3
빌드 때 기계어까지 가는 것은 첫 갈래뿐입니다. 나머지 둘은 마지막 옮김을 실행 때로 미룹니다.
빌드가 지나는 세 단계
빌드는 대개 세 단계를 지납니다. 컴파일, 링크, 패키징입니다. 단계를 나눠 두면 고친 파일만 다시 처리하고 나머지는 앞 단계가 남긴 결과를 쓸 수 있습니다.
flowchart TD
S["소스 파일 여러 개"] -->|컴파일| O["목적 파일 · 아직 혼자서는 못 돈다"]
O -->|링크| E["실행 파일 · 받아 온 코드까지 이어 붙었다"]
E -->|패키징| A["배포 묶음 · 설정과 자원을 함께 담았다"]
컴파일은 소스 파일을 하나씩 기계가 읽는 형태로 옮기는 단계입니다. 결과는 목적 파일입니다. 목적 파일에는 그 파일이 가진 함수만 들어 있습니다. 다른 파일의 함수를 부르는 대목은 아직 빈칸으로 남습니다.
링크는 그 빈칸을 메우는 단계입니다. 흩어진 목적 파일과 받아 온 코드에서 이름을 찾아 이어 붙입니다. 이 일을 하는 프로그램이 링커입니다. 이어 붙일 이름을 못 찾으면 빌드가 실패합니다.
패키징은 실행에 필요한 것을 배포할 묶음 하나에 담는 단계입니다. 실행 파일만으로는 모자랄 때가 많습니다. 설정 파일이나 이미지 같은 자원이 함께 들어갑니다. JAR(Java Archive, 자바 아카이브) 파일이나 컨테이너 이미지가 그런 묶음입니다.
작은 프로그램을 한 번 빌드하면 파일이 이렇게 남습니다.
src/main.c # 사람이 쓴 소스
build/main.o # 컴파일이 낸 목적 파일
build/app # 링크가 낸 실행 파일
dist/app.tar.gz # 패키징이 낸 배포 묶음
아래로 갈수록 사람이 읽을 것은 줄고 컴퓨터가 쓸 것만 남습니다. 남에게 건네는 것은 맨 아래 묶음 하나입니다.
남이 만든 코드를 끌어오는 의존성
거의 모든 프로그램은 남이 만들어 둔 코드를 씁니다. 암호를 다루는 OpenSSL 이나 날짜를 계산하는 코드를 직접 짜지 않습니다. 이렇게 가져다 쓰는 코드 묶음을 라이브러리라고 하고, 내 프로그램이 그 라이브러리에 기대는 관계를 의존성이라고 합니다.
의존성은 소스 코드 안이 아니라 따로 적은 목록에 씁니다. 빌드 도구는 그 목록을 읽어 npm · Maven Central 같은 패키지 저장소에서 라이브러리를 내려받습니다. 패키지 저장소는 공개된 라이브러리를 버전마다 모아 두고 내려받게 해 주는 서버입니다.
내려받은 라이브러리가 또 다른 라이브러리에 기대는 일이 흔합니다. 그래서 목록에 셋만 적어도 받아 오는 것은 수십 개가 되기도 합니다.
flowchart TD
A["내 프로그램"]
subgraph D1["목록에 내가 적은 셋"]
B1["암호"]
B2["날짜"]
B3["압축"]
end
subgraph D2["그 셋이 다시 끌어오는 것"]
C1["인코딩"]
C2["로깅"]
C3["나머지 수십 개"]
end
A --> B1
A --> B2
A --> B3
B1 --> C1
B2 --> C2
B3 --> C3
내가 고른 것은 위 한 층뿐이고 아래 층은 저절로 따라옵니다. 어느 버전을 받았는지 package-lock.json 같은 잠금 파일에 적어 두면 다음 빌드도 같은 것을 받습니다.
입력이 같아야 출력이 같습니다. 라이브러리가 조용히 올라가면 어제 성공한 빌드가 오늘 깨집니다. 고친 적 없는 코드에서 원인을 찾게 됩니다.
순서를 정하는 빌드 도구
빌드에는 순서가 있습니다. 컴파일이 끝나야 링크를 하고 링크가 끝나야 묶습니다. 이 순서를 사람이 매번 손으로 짚는 대신 Make · Gradle 같은 빌드 도구에 맡깁니다.
빌드 도구는 할 일을 태스크라는 덩이로 나눠 둡니다. 태스크 하나는 코드를 컴파일하는 일이나 배포 묶음을 만드는 일처럼 독립된 작업 한 덩이입니다. 태스크마다 무엇이 먼저 끝나야 하는지를 적어 둡니다.
flowchart TD
C1["코드 컴파일"] --> P["배포 묶음 만들기"]
C2["설정 파일 복사"] --> P
C1 --> T["테스트 실행"]
T --> P
화살표는 「먼저 끝나야 한다」는 관계입니다. 코드 컴파일과 설정 파일 복사는 서로를 기다릴 이유가 없어 동시에 돌 수 있습니다. 배포 묶음 만들기는 앞의 셋이 다 끝나야 시작합니다.
이 앞뒤 관계를 모으면 DAG(Directed Acyclic Graph, 방향 비순환 그래프)가 됩니다. 방향 비순환 그래프는 화살표에 방향이 있고 어느 길을 따라가도 제자리로 돌아오지 않는 그림입니다. 돌아오지 않으니 순서를 하나로 줄 세울 수 있고, 서로 안 걸리는 태스크는 함께 돌릴 수 있습니다.
앞 결과가 남아 있고 입력이 안 바뀌었으면 빌드 도구는 그 태스크를 건너뜁니다. 이렇게 바뀐 곳만 다시 하는 빌드를 증분 빌드라고 합니다. 건너뛰기는 잘못 판단하기도 합니다. 그때는 낡은 결과가 남습니다. 그래서 결과물을 지우고 처음부터 도는 클린 빌드를 따로 둡니다.
빌드 때 정해지는 것과 실행 때 정해지는 것
프로그램의 일생은 두 때로 갈립니다. 빌드가 도는 때와 그 결과물이 떠서 도는 때입니다. 앞을 컴파일 타임, 뒤를 런타임이라고 합니다.
결과물이 떠서 도는 동안의 그 프로그램을 프로세스라고 합니다. 프로세스는 운영체제가 프로그램 하나를 띄워 놓고 돌리는 단위입니다.
flowchart TD
subgraph B["빌드 때"]
S["소스 코드"] --> C["문법 검사 · 타입 검사"]
C --> A["결과물"]
end
subgraph R["실행 때"]
P["프로세스"] --> E["설정 읽기 · 파일 열기 · 서버 연결"]
end
A --> P
빌드 때 정해지는 것은 코드만 보고 알 수 있는 것들입니다. 문법이 맞는지, 부른 함수가 있는지, 넘긴 값의 타입이 맞는지가 그렇습니다. 타입은 그 값이 숫자인지 글자인지처럼 값의 종류를 말합니다. 여기서 걸리면 결과물이 아예 안 나옵니다.
실행 때 정해지는 것은 코드만 봐서는 알 수 없는 것들입니다. 설정에 무엇이 적혀 있는지, 열려는 파일이 있는지, 서버가 응답하는지가 그렇습니다. 빌드가 아무리 성공해도 이런 것은 띄워 봐야 압니다.
이 경계를 어디에 둘지는 백엔드 개발자가 자주 고르는 대목입니다. 접속 주소를 코드에 박으면 빌드 때 정해져서 환경마다 다시 빌드해야 합니다. 실행 때 환경 변수에서 읽게 두면 같은 결과물을 여러 환경에 올릴 수 있습니다. 환경 변수는 프로그램이 뜰 때 바깥에서 건네주는 이름과 값의 짝입니다.
빌드가 깨지는 원인
빌드는 성공 아니면 실패로 끝납니다. 깨지는 대목은 대개 셋입니다.
첫째는 코드입니다. 문법이 틀렸거나 없는 함수를 부르거나 타입이 안 맞으면 컴파일이나 링크에서 멈춥니다. 오류 메시지가 어느 파일 몇 번째 줄인지 알려 주므로 고치기 쉬운 편입니다.
둘째는 의존성입니다. 목록에 적은 라이브러리를 저장소에서 못 찾을 때 멈춥니다. 두 라이브러리가 같은 것의 서로 다른 버전을 요구해 하나를 고를 수 없을 때도 멈춥니다.
셋째는 환경입니다. 빌드를 돌리는 컴퓨터마다 깔린 도구와 운영체제가 달라서 한 사람의 노트북에서만 성공하는 빌드가 나옵니다. 「내 컴퓨터에서는 되는데」가 이때 나오는 말입니다.
그래서 빌드를 각자의 컴퓨터가 아니라 정해진 서버 한 곳에서 돌립니다. 코드를 올릴 때마다 서버가 자동으로 빌드하고 테스트까지 돌리는 방식을 지속적 통합이라고 부릅니다. 깨진 빌드를 일찍 찾아내는 것이 목적입니다.
관련 항목
빌드가 지나는 처리 단계
전처리 · 컴파일 · 컴파일러 · 링크 · 링커 · 정적 링크 · 동적 링크 · 패키징 · 코드 서명
빌드가 내놓는 결과물
실행 파일 · 목적 파일 · 바이트코드 · JAR · 컨테이너 이미지 · 패키지 · 아티팩트 · 아티팩트 저장소
빌드를 돌리는 도구
빌드 도구 · Make · Gradle · Maven · javac · kotlinc · 빌드 스크립트
빌드를 이루는 실행 단위
태스크 · DAG · 의존성 그래프 · 증분 빌드 · 클린 빌드 · 빌드 캐시 · 캐싱
빌드가 끌어오는 의존성
의존성 · 라이브러리 · OpenSSL · 패키지 저장소 · npm · Maven Central · 잠금 파일 · 의존성 관리 · 시맨틱 버저닝 · 클래스패스 · 공유 라이브러리
빌드와 맞세워지는 실행 시점
컴파일 타임 · 런타임 · 로더 · 동적 링커 · 인터프리터 · 가상 머신 · 환경 변수
빌드를 자동으로 돌리는 절차
지속적 통합 · 지속적 전달 · 지속적 배포 · 배포 · 커밋에서 배포까지 · 릴리스 엔지니어링 · 자동화 · 풀 리퀘스트
빌드의 결과를 믿게 해 주는 성질
다른 이름: build · 빌드하기