패키징
고친 사람 github-actions[bot]
패키징은 컴파일이 만든 결과물을 받는 쪽이 바로 쓸 수 있게 묶음 하나로 담는 일입니다. 실행할 코드 옆에 설정과 자원까지 함께 넣습니다. 서버나 사용자 기기로 옮겨 가는 것은 이 묶음입니다. 소스 코드에서 묶음까지 가는 빌드 전체로 보면 패키징이 마지막 단계입니다.
쉽고 빠른 이해
패키징은 프로그램을 들고 다닐 수 있는 파일 하나로 묶는 일입니다. 자바로 만든 서버라면 컴파일한 코드와 설정 파일을 파일 하나에 담아 서버에 올립니다.
파일 수백 개를 하나씩 옮기면 하나쯤 빠지거나 옛 파일과 새 파일이 섞입니다. 묶음 하나로 다루면 한 버전을 한꺼번에 옮기고 한꺼번에 되돌릴 수 있습니다.
- 컴파일이 코드를 실행할 수 있는 형태로 바꿉니다
- 그 결과에 설정 파일과 이미지 같은 자원, 이름과 버전을 붙여 묶음 하나로 만듭니다
- 묶음을 보관소에 올립니다. 서버나 기기는 거기서 받아 설치합니다
같은 프로그램을 여러 서버에 깔거나 남에게 건넬 때 패키징이 필요합니다. 내 컴퓨터에서 혼자 돌리는 스크립트라면 묶을 이유가 없습니다.
대가도 있습니다. 묶음에 많이 넣을수록 파일이 커집니다. 작은 수정에도 묶음을 새로 만들어야 합니다. 덜 넣으면 설치할 때 빠진 것을 따로 받아야 합니다.
상세
이사할 때는 짐을 낱개로 트럭에 싣지 않습니다. 상자에 나눠 담아 테이프로 봉합니다. 겉에는 무엇이 들었는지 적습니다. 받는 사람은 상자 수만 세어 보면 빠진 짐이 없는지 압니다.
패키징은 프로그램에 이 일을 해 줍니다. 프로그램을 돌리는 데 필요한 파일을 모아 정해진 형식의 묶음 하나로 만듭니다. 이렇게 만든 묶음을 흔히 패키지라고 부릅니다.
자바로 만든 웹 서버를 예로 듭니다. 컴파일을 마친 코드는 클래스 파일 수백 개로 흩어져 있습니다. 클래스 파일은 자바 컴파일러가 소스 파일마다 내놓는 결과물입니다.
패키징은 이 파일들과 설정 파일을 JAR(Java Archive, 자바 아카이브) 파일 하나에 담습니다. JAR 는 여러 파일을 압축해 하나로 묶은 파일입니다. 이렇게 파일 여럿을 하나로 묶은 파일을 아카이브라고 합니다. 서버에는 이 파일 하나만 올리면 됩니다.
빌드 안에서 패키징은 맨 끝에 섭니다. 앞 단계인 컴파일은 코드를 기계가 돌릴 수 있는 형태로 바꿉니다. 패키징은 그 결과를 받아 옮길 수 있는 형태로 바꿉니다.
반도체 업계에서 말하는 패키징은 칩을 감싸 기판에 붙이는 공정입니다. 소프트웨어의 패키징과는 다른 일입니다.
묶음이 없으면 생기는 일
컴파일 결과를 묶지 않고 파일째 서버에 복사한다고 해 봅니다. 파일이 수백 개면 하나쯤 빠뜨리기 쉽습니다. 복사가 중간에 끊기면 서버에 옛 파일과 새 파일이 섞입니다. 어느 쪽이든 프로그램이 이상하게 돌 뿐, 어디가 틀렸는지 바로 드러나지 않습니다.
묶음 하나로 다루면 이 문제가 사라집니다. 옮기는 단위가 파일 하나라서 다 왔거나 안 왔거나 둘 중 하나입니다. 묶음에 버전을 붙여 두면 「지금 서버에서 도는 것이 어느 버전인가」에도 바로 답할 수 있습니다. 문제가 생기면 앞 버전 묶음을 다시 올려 롤백합니다.
소스를 서버에서 바로 받아 돌리는 방식도 있습니다. 이때는 서버마다 받은 시점이 달라서 모두 같은 버전인지 보장하기 어렵습니다. 그래서 같은 프로그램을 여러 서버에 깔거나 남에게 건네야 할 때부터 패키징이 필요해집니다. 내 컴퓨터에서 혼자 돌리는 스크립트라면 묶을 이유가 없습니다.
묶음에 담기는 세 가지
묶음 안에는 성격이 다른 것 셋이 함께 들어갑니다. 앞의 둘은 프로그램이 돌 때 쓰는 파일입니다. 셋째는 묶음 자신을 설명하는 정보입니다.
| 담기는 것 | 무엇인가 | 예 |
|---|---|---|
| 실행할 코드 | 기계나 런타임이 돌리는 파일 | 실행 파일 · 클래스 파일 · 스크립트 |
| 자원 | 코드가 돌면서 읽는 파일 | 설정 파일 · 이미지 · 화면 템플릿 |
| 메타데이터 | 묶음 자신을 설명하는 정보 | 이름 · 버전 |
셋째 줄이 패키징에서 새로 생기는 것입니다. 메타데이터는 묶음 안의 내용이 아니라 묶음 자체에 대한 정보입니다. 받는 쪽은 묶음을 풀어 보지 않고도 이 정보로 무엇이 들었는지 압니다.
메타데이터에 꼭 들어가는 것 하나가 의존성 목록입니다. 의존성은 이 프로그램이 돌려면 함께 있어야 하는 다른 패키지입니다. 목록이 있어야 받는 쪽이 모자란 것을 알아채고 채울 수 있습니다.
또 하나는 진입점입니다. 진입점은 묶음을 실행할 때 맨 처음 부를 코드입니다. 클래스 파일이 수백 개여도 어디서 시작할지는 하나로 정해 두어야 합니다.
JAR 파일 안을 열어 보면 세 가지가 이렇게 놓여 있습니다.
app.jar
├── META-INF/MANIFEST.MF # 메타데이터
├── com/example/App.class # 코드
└── config.properties # 자원
맨 위의 MANIFEST.MF 가 메타데이터를 담은 파일입니다. 진입점이 될 클래스 이름과 버전 같은 정보를 글자로 적어 둡니다. 이렇게 묶음의 내용을 적어 둔 파일을 매니페스트라고 부릅니다.
의존성을 넣는 방식과 적는 방식
자기 코드와 자원은 어느 경우에나 묶음에 넣습니다. 갈리는 것은 남이 만든 라이브러리입니다. 이것을 묶음 안에 넣느냐, 이름만 적어 두느냐에 따라 패키징이 두 방식으로 나뉩니다.
넣는 방식은 가져다 쓰는 라이브러리까지 묶음에 다 담습니다. 받는 쪽은 묶음 하나만 있으면 프로그램을 돌릴 수 있습니다. 자바에서는 이렇게 만든 묶음을 팻 JAR(fat JAR)라고 부릅니다.
적는 방식은 묶음에 자기 코드만 담습니다. 필요한 라이브러리는 이름과 버전을 메타데이터에 적어 둡니다. 설치할 때 패키지 매니저가 그 목록을 읽고 라이브러리를 따로 받아 옵니다. 패키지 매니저는 패키지를 찾아 받고 설치하는 도구입니다.
| 넣는 방식 | 적는 방식 | |
|---|---|---|
| 묶음 크기 | 크다 | 작다 |
| 설치할 때 | 묶음 하나로 끝난다 | 목록의 패키지를 더 받아야 한다 |
| 여러 프로그램이 같은 라이브러리를 쓸 때 | 프로그램마다 한 벌씩 든다 | 한 벌을 나눠 쓴다 |
| 라이브러리에 보안 수정이 나왔을 때 | 묶음을 새로 만들어야 한다 | 라이브러리만 갈면 된다 |
| 버전 충돌 | 생기지 않는다. 자기 묶음 안의 것만 쓴다 | 두 프로그램이 서로 다른 버전을 원하면 부딪힌다 |
넣는 방식은 받는 쪽의 수고를 덜어 줍니다. 대신 묶음이 커집니다. 적는 방식은 묶음을 가볍게 합니다. 대신 설치할 때의 수고가 받는 쪽으로 넘어갑니다.
컨테이너 이미지는 넣는 방식의 끝에 있습니다. 라이브러리뿐 아니라 커널을 뺀 운영체제 파일까지 묶음에 담습니다. 그래서 어느 서버에 올려도 묶음 안의 환경이 같습니다.
받는 쪽마다 다른 묶음 형식
묶음의 형식은 그것을 받아 여는 쪽이 정합니다. 받는 쪽이 읽을 줄 아는 형식이어야 풀어서 쓸 수 있기 때문입니다.
| 받는 쪽 | 묶음 형식 | 어떤 묶음인가 |
|---|---|---|
| JVM(Java [[가상 머신 | Virtual Machine]], 자바 가상 머신) | JAR · WAR(Web Application Archive, 웹 애플리케이션 아카이브) |
| [[Linux | 리눅스]] 배포판의 패키지 매니저 | deb · RPM(Red Hat Package Manager, 레드햇 패키지 매니저) |
| [[Python | 파이썬]] 패키지 저장소 | wheel · 소스 배포판 |
| 컨테이너 런타임 | 컨테이너 이미지 | 컨테이너 런타임은 이미지를 받아 컨테이너로 띄우는 프로그램 |
같은 프로그램을 받는 쪽에 맞춰 여러 형식으로 묶기도 합니다. 한 서버에는 JAR 로 올립니다. 다른 환경에는 같은 프로그램을 컨테이너 이미지로 내보냅니다. 형식은 달라도 담기는 세 가지는 같습니다.
게임 개발의 패키징
게임 개발에서 패키징은 플레이어에게 보낼 게임 한 벌을 만드는 일 전체를 가리킵니다. 코드를 빌드하는 데서 끝나지 않습니다. 개발하며 쓰던 그림과 3D 모델 같은 에셋도 출시용 형식으로 바꿔 함께 담습니다.
에셋을 바꾸는 일을 쿠킹이라고 부릅니다. 원본 에셋은 사람이 고치기 쉬운 형식으로 저장되어 있습니다. 쿠킹은 이것을 기기가 바로 읽는 형식으로 바꿉니다. 그림이라면 기기가 바로 읽는 압축 형식으로 바뀝니다.
바꾼 에셋은 큰 아카이브 파일 몇 개에 이어 붙입니다. 작은 파일 수만 개를 하나씩 열면 찾고 여는 비용이 쌓이기 때문입니다. 아카이브 앞머리에는 목차를 두어, 원하는 에셋이 몇 번째 바이트에서 시작하는지 찾게 합니다.
아래 그림은 코드와 에셋이 각자 다른 길을 거쳐 게임 한 벌로 모이는 모습입니다.
flowchart TD
A["원본 에셋"] -->|쿠킹| B["기기용 에셋"]
B -->|이어 붙이기| C["아카이브 파일"]
S["코드"] -->|컴파일| E["실행 파일"]
C --> G["게임 한 벌"]
E --> G
PC와 게임기와 휴대폰은 읽을 수 있는 그림 압축 형식이 서로 다릅니다. 그래서 쿠킹 결과가 기기마다 달라집니다. 패키징도 기기마다 따로 합니다. 같은 게임이라도 기기 수만큼 묶음이 나옵니다.
패키징과 배포의 경계
패키징은 묶음을 만드는 데서 끝납니다. 그 묶음을 서버로 옮겨 실제로 돌리는 일은 배포입니다. 두 일 사이에 묶음을 보관해 두는 곳이 있습니다.
빌드를 거쳐 나온 파일을 아티팩트라고 부릅니다. 패키지는 그 대표입니다.
아티팩트를 버전별로 보관하는 서버가 아티팩트 저장소입니다. 앞에서 말한 보관소가 이것입니다. 배포는 이 저장소에서 원하는 버전의 묶음을 꺼내 갑니다.
flowchart TD
S["소스 코드"] -->|컴파일 · 테스트| B["컴파일 결과"]
B -->|패키징| P["패키지"]
P -->|버전을 붙여 올린다| R["아티팩트 저장소"]
R -->|같은 묶음| T["시험 서버"]
R -->|같은 묶음| O["운영 서버"]
그림의 아래 두 화살표가 이 절의 요점입니다. 한 번 만든 묶음은 고치지 않고 여러 곳에 보냅니다. 시험 서버에서 확인한 묶음이 손대지 않은 채 운영 서버로 갑니다. 운영에 보낼 때 다시 빌드하면 그 사이 바뀐 라이브러리가 섞여, 시험한 것과 다른 것이 나갈 수 있습니다.
그래서 환경마다 다른 값은 묶음 밖에 둡니다. 데이터베이스 주소나 비밀번호는 환경 변수나 바깥 설정 파일로 넣습니다. 이런 값을 묶음 안에 박아 두면 환경마다 묶음을 따로 만들어야 합니다. 그러면 시험한 묶음과 운영 묶음이 달라집니다.
묶음을 믿고 받게 하는 장치
받는 쪽은 묶음이 오는 길에 깨지거나 바뀌지 않았는지 알고 싶어 합니다. 그래서 묶음과 함께 체크섬을 냅니다. 체크섬은 파일 내용으로 계산한 짧은 요약값이라 한 바이트만 달라져도 값이 바뀝니다.
체크섬은 누가 만들었는지까지는 알려 주지 않습니다. 이것까지 확인하려면 묶음에 코드 서명을 붙입니다. 서명이 맞으면 만든 쪽이 내보낸 묶음에서 아무것도 바뀌지 않았다는 뜻입니다.
같은 소스에서 언제 패키징해도 바이트 하나 다르지 않은 묶음이 나오게 만들기도 합니다. 이것을 재현 가능한 빌드라고 부릅니다. 이렇게 해 두면 누구든 소스로 다시 묶어 볼 수 있습니다. 그 결과를 받은 묶음과 바이트 단위로 견줍니다.
관련 항목
패키징을 앞뒤로 둔 빌드 단계
빌드 · 컴파일 · 링크 · 테스트 · 배포 · 빌드 파이프라인 · 지속적 통합 · 지속적 배포
패키징이 만드는 묶음 형식
JAR · WAR · 팻 JAR · 컨테이너 이미지 · deb · RPM · wheel · tarball · 아카이브 · 설치 프로그램
패키징이 묶음에 적는 정보
메타데이터 · 매니페스트 · 의존성 · 진입점 · 시맨틱 버저닝 · 락 파일
패키징을 맡는 도구
빌드 도구 · Maven · Gradle · 패키지 매니저 · Docker · 번들러
묶음을 보관하고 나르는 저장소
아티팩트 · 아티팩트 저장소 · 패키지 저장소 · 컨테이너 레지스트리
게임 개발에서 패키징이 거치는 단계
에셋 · 쿠킹 · 에셋 파이프라인 · 텍스처 압축 · 청킹 · 빌드 프로파일
묶음을 믿고 받게 하는 장치
체크섬 · 코드 서명 · 재현 가능한 빌드 · SBOM
패키징과 헷갈리는 이웃
다른 이름: packaging · 소프트웨어 패키징