패키지 매니저
고친 사람 github-actions[bot]
패키지 매니저는 남이 만든 코드를 내 프로젝트로 받아 와 쓰게 해 주는 도구입니다. 필요한 라이브러리의 이름과 버전을 파일에 적어 두면 이 도구가 대신 찾아서 내려받습니다. 그 라이브러리가 또 필요로 하는 코드도 줄줄이 따라가 함께 받습니다. 운영체제에 프로그램을 설치해 주는 도구도 같은 이름으로 부릅니다.
쉽고 빠른 이해
무슨 일을 하는 물건인가 — 프로젝트가 쓰는 남의 코드를 받아 오고 버전을 맞춥니다. 자바 프로젝트의 build.gradle 에 적는 dependencies 목록이 이 도구가 읽는 목록입니다. 자바스크립트 프로젝트에서는 package.json 이 그 목록입니다.
왜 이렇게 하나 — 이 도구가 없으면 라이브러리 파일을 하나씩 찾아 받아서 폴더에 넣어야 합니다. 그 라이브러리가 쓰는 라이브러리까지 사람이 찾아야 합니다. 팀원마다 받은 버전이 달라져서 내 컴퓨터에서만 돌아가는 코드가 생깁니다.
어떻게 도나
- 목록 파일에서 필요한 라이브러리와 받아들일 버전을 읽습니다
- 그 라이브러리가 또 필요로 하는 것까지 따라가며 설치할 버전을 하나씩 정합니다
- 패키지 저장소에서 내려받아 프로젝트에 놓습니다
- 다음에도 같은 버전을 받도록 고른 버전을 따로 적어 둡니다
언제 쓰나 — 남이 만든 라이브러리를 하나라도 쓰는 프로젝트라면 거의 늘 씁니다. 표준 라이브러리로 되는 일에는 패키지를 들이지 않는 팀도 있습니다.
대가 — 내가 적지 않은 라이브러리가 많이 따라 들어옵니다. 그중 하나에 악성 코드가 섞이면 내 프로그램도 오염됩니다. 패키지 저장소가 멈추면 빌드도 멈춥니다.
상세
「카레」라고만 적은 쪽지를 받은 장보기 대행은 카레 요리법을 펼쳐 감자, 고기, 카레 가루를 줄줄이 사 옵니다. 카레 가루는 상표만 보고 그날 진열대에서 가장 새 제품을 집습니다. 그래서 다음 주에는 같은 쪽지로 다른 물건이 오기도 합니다. 쪽지는 프로젝트가 쓸 라이브러리의 목록, 대행은 패키지 매니저, 가게는 패키지 저장소입니다.
이 절은 백엔드 프로젝트 하나가 라이브러리를 받아 오는 과정을 따라갑니다. 주로 프로그래밍 언어마다 있는 패키지 매니저를 다룹니다. 운영체제의 패키지 매니저는 끝에서 견줍니다.
패키지
남이 만든 코드를 가져다 쓰려면 그 코드가 건네받을 수 있는 모양이어야 합니다. 코드에 이름과 버전 번호, 이 코드가 돌려면 필요한 다른 코드의 목록을 붙여 한 파일로 묶은 것을 패키지라고 합니다. 날짜를 계산해 주는 라이브러리를 누군가 공개했다면, 그 라이브러리의 한 버전이 패키지 하나입니다.
「패키지」는 자바의 package 선언처럼 코드 안에서 이름을 나누는 묶음을 가리키기도 합니다. 패키지 매니저가 다루는 것은 그쪽이 아닙니다. 남에게 건네는 배포 묶음입니다.
패키지 저장소
패키지를 모아 두고 누구나 받아 가게 하는 서버를 패키지 저장소라고 합니다. 레지스트리라고도 부릅니다. 라이브러리를 만든 사람은 새 버전을 이 저장소에 올립니다. 쓰는 사람은 이름과 버전만 대고 받아 갑니다.
저장소가 있으면 라이브러리를 찾으러 웹사이트를 돌아다닐 필요가 없습니다. 언어마다 대표 공개 저장소가 하나씩 있습니다. 회사는 사내 코드를 올리는 저장소를 따로 두기도 합니다.
매니페스트 파일
한 코드가 돌려면 다른 코드가 있어야 하는 관계를 의존성이라고 합니다. 내 프로젝트가 날짜 라이브러리를 부른다면 내 프로젝트는 그 라이브러리에 의존합니다. 흔히 그 라이브러리 자체도 의존성이라고 부릅니다.
프로젝트가 직접 쓰는 의존성은 파일 하나에 적습니다. 이 파일을 매니페스트 파일이라고 합니다. 한 줄에 라이브러리 이름과 받아들일 버전을 적습니다. 자바스크립트 프로젝트에서는 package.json 이 이 파일입니다.
{
"dependencies": {
"date-lib": "^2.1.0"
}
}
date-lib 는 설명을 위해 지은 이름입니다. ^2.1.0 은 버전 하나가 아니라 받아들일 버전의 범위입니다. 이 표기는 아래 「버전 범위」에서 풉니다.
자바 프로젝트는 컴파일까지 맡는 빌드 도구인 Gradle 이나 Maven 의 빌드 파일에 같은 목록을 적습니다. Gradle 에서는 build.gradle 의 dependencies 블록이 그 목록입니다. implementation 은 이 라이브러리를 컴파일과 실행에 쓴다는 표시입니다.
dependencies {
implementation 'com.example:date-lib:2.1.0'
}
자바 쪽은 그룹(만든 조직의 이름), 이름, 버전 세 칸을 콜론으로 이어 라이브러리 하나를 가리킵니다. 두 파일은 모양이 달라도 담은 내용이 같습니다. 무엇을 어느 버전으로 쓸지입니다.
따라 들어오는 의존성
라이브러리도 다른 라이브러리를 씁니다. 웹 프레임워크 하나를 매니페스트에 적으면, 그 프레임워크가 쓰는 템플릿 엔진과 로깅 라이브러리도 있어야 프로그램이 돕니다. 이렇게 내가 적지 않았는데 따라 들어오는 의존성을 전이 의존성이라고 합니다.
패키지 매니저는 패키지를 받을 때 그 패키지의 의존성 목록도 읽습니다. 목록에 있는 것을 받습니다. 받은 것의 목록을 또 읽습니다. 더 따라갈 것이 없을 때까지 되풀이합니다. 아래 그림은 웹 프레임워크와 날짜 라이브러리 둘만 적은 프로젝트입니다.
flowchart TD
P["내 프로젝트"] --> W["웹 프레임워크"]
P --> D["날짜 라이브러리"]
W --> T["템플릿 엔진"]
W --> L["로깅 라이브러리"]
D --> L
D --> Z["시간대 데이터"]
그림에서 사람이 적은 것은 윗줄 둘입니다. 템플릿 엔진, 로깅 라이브러리, 시간대 데이터 셋은 도구가 목록을 따라가 찾아낸 것입니다. 직접 적은 의존성이 스무 개여도 받는 패키지는 수백 개가 되기도 합니다.
로깅 라이브러리에는 화살표가 두 개 들어옵니다. 웹 프레임워크와 날짜 라이브러리가 둘 다 이것을 씁니다. 이 모양이 아래 「버전이 부딪칠 때」의 문제가 됩니다.
버전 범위
라이브러리 버전은 흔히 2.1.0 처럼 점으로 나눈 숫자 셋으로 적습니다. 이 숫자 셋에 뜻을 정해 둔 약속이 시맨틱 버저닝입니다. 아래 표는 앞에서부터 칸마다 언제 숫자를 올리는지 보입니다.
| 칸 | 숫자를 올리는 때 | 쓰던 코드는 |
|---|---|---|
| 주 버전(major) | 쓰던 기능이 바뀌거나 없어질 때 | 고쳐야 할 수 있다 |
| 부 버전(minor) | 기능을 더할 때 | 손대지 않아도 돈다 |
| 수 버전(patch, 버그 수정 번호) | 버그만 고칠 때 | 손대지 않아도 돈다 |
이 약속이 지켜진다면 부 버전과 수 버전은 올려 받아도 쓰던 코드가 깨지지 않습니다. 그래서 매니페스트에는 버전 하나 대신 범위를 적는 일이 많습니다. 범위로 적어 두면 버그를 고친 새 버전이 나와도 매니페스트를 안 고치고 받습니다.
범위를 적는 표기는 도구마다 다릅니다. 아래는 자바스크립트 쪽 표기입니다.
| 적은 값 | 받아들이는 버전 |
|---|---|
2.1.0 |
2.1.0 하나 |
~2.1.0 |
2.1.0 이상 2.2.0 미만 |
^2.1.0 |
2.1.0 이상 3.0.0 미만 |
^ 는 주 버전이 같은 동안은 새 버전을 받겠다는 뜻입니다. ~ 는 수 버전만 올려 받습니다. 앞의 package.json 은 ^2.1.0 이었으니 2.x 안에서 가장 높은 버전을 받습니다.
버전이 부딪칠 때
패키지 매니저는 내려받기 전에 모든 범위를 모아 설치할 버전을 하나씩 정합니다. 이 일을 의존성 해석이라고 합니다. 한 패키지를 여러 곳에서 부르면 모든 범위를 함께 만족하는 버전을 찾아야 합니다.
앞 그림에서 로깅 라이브러리는 두 곳에서 불립니다. 웹 프레임워크는 1.x 를, 날짜 라이브러리는 2.x 를 원한다고 해 봅시다. 두 범위를 함께 만족하는 버전은 없습니다. 이 상태를 의존성 충돌이라고 합니다.
도구가 이것을 푸는 길은 둘입니다. 하나는 두 버전을 모두 설치해 각자 원한 버전을 쓰게 하는 것입니다. 자바스크립트의 npm(Node Package Manager, 노드 패키지 매니저)이 이렇게 합니다. 한 프로그램 안에 같은 라이브러리의 두 버전이 함께 올라가도 되는 환경이라서 가능합니다.
다른 하나는 버전 하나만 골라 모두가 그것을 쓰게 하는 것입니다. 자바 프로그램은 실행 중에 클래스 파일을 읽어 메모리에 올립니다. 보통은 같은 이름의 클래스를 한 벌만 올리므로 버전도 하나로 정해야 합니다.
어느 버전을 고르는지는 도구마다 다릅니다. Maven 은 내 프로젝트에서 그 패키지까지 몇 단계를 거쳐 닿는지 셉니다. 단계가 적은 쪽이 원한 버전이 이깁니다. 단계가 같으면 매니페스트에 먼저 적힌 쪽을 따릅니다.
앞 그림의 로깅 라이브러리는 웹 프레임워크를 거쳐도, 날짜 라이브러리를 거쳐도 두 단계 아래에 있습니다. 매니페스트에 웹 프레임워크를 먼저 적었다면 Maven 은 1.x 를 고릅니다. Gradle 은 기본으로 가장 높은 버전을 고르므로 2.x 가 됩니다.
골라지지 않은 쪽 라이브러리는 자기가 기대하지 않은 버전과 함께 돕니다. 기대한 메서드가 그 버전에 없으면 실행 중에 NoSuchMethodError 같은 오류가 납니다. 컴파일은 통과하고 실행해야 드러나서 찾기 어렵습니다.
잠금 파일
범위로 적으면 편한 대신 문제가 하나 생깁니다. 오늘 설치하면 2.1.0 이 옵니다. 다음 달에 설치하면 그사이 나온 2.3.0 이 옵니다. 매니페스트는 안 바뀌었는데 받는 코드가 달라집니다.
내 컴퓨터에서는 되는데, 코드를 받아 자동으로 빌드하는 빌드 서버에서는 안 되는 일이 이렇게 생깁니다. 두 곳이 서로 다른 날 설치했기 때문입니다.
그래서 패키지 매니저는 해석을 마친 결과를 파일에 따로 적어 둡니다. 그 파일이 잠금 파일입니다. 전이 의존성까지 포함해 받은 모든 패키지의 버전이 하나씩 적힙니다. 다음 설치 때는 범위를 다시 풀지 않고 이 파일에 적힌 버전을 받습니다.
잠금 파일에는 대개 패키지마다 내려받은 파일의 해시도 적힙니다. 해시는 파일 내용으로 계산한 짧은 값입니다. 내용이 한 바이트만 달라도 값이 바뀝니다.
다음에 받은 파일의 해시가 적어 둔 값과 다르면 도구는 설치를 멈춥니다. 저장소에서든 내려받는 길 중간에서든 파일이 바꿔치기됐는지 알아내려는 것입니다.
잠금 파일은 매니페스트와 함께 커밋합니다. 그래야 팀원과 빌드 서버가 같은 버전을 받습니다. 자바스크립트의 package-lock.json, 파이썬 Poetry 의 poetry.lock, 러스트의 Cargo.lock 이 잠금 파일입니다.
설치 명령 한 번의 흐름
지금까지 본 조각을 설치 명령 한 번으로 이으면 아래 순서가 됩니다. 받을 패키지마다 요청이 되풀이됩니다. 잠금 파일이 있으면 해시 확인도 그때마다 합니다.
sequenceDiagram
participant 개발자
participant 매니저 as 패키지 매니저
participant 저장소 as 패키지 저장소
participant 폴더 as 프로젝트 폴더
개발자->>매니저: 설치 명령
매니저->>폴더: 매니페스트와 잠금 파일을 읽는다
Note over 매니저: 잠금 파일이 없으면 범위를 풀어 버전을 정한다
loop 패키지마다
매니저->>저장소: 정한 버전을 요청한다
저장소-->>매니저: 패키지 파일
opt 잠금 파일이 있으면
Note over 매니저: 적어 둔 해시와 대조한다
end
매니저->>폴더: 패키지를 놓는다
end
매니저->>폴더: 고른 버전과 해시를 잠금 파일에 적는다
잠금 파일이 이미 있으면 버전을 정하는 단계가 빠집니다. 적힌 버전을 받아 해시만 확인합니다. 그래서 같은 잠금 파일로 설치하면 누가 언제 하든 같은 결과가 나옵니다.
받은 패키지를 두는 위치
받은 패키지를 어디에 두는지는 도구마다 다릅니다. 방식은 크게 둘입니다.
하나는 프로젝트 폴더 안에 두는 방식입니다. npm 은 프로젝트마다 node_modules 폴더를 만들어 거기에 받습니다. 프로젝트끼리 서로 다른 버전을 써도 부딪치지 않습니다. 대신 같은 패키지가 프로젝트 수만큼 디스크에 쌓입니다.
다른 하나는 컴퓨터에 한 벌만 받아 두고 여러 프로젝트가 나눠 쓰는 방식입니다. Maven 과 Gradle 은 사용자 홈 폴더 아래 공용 폴더에 받아 둡니다. 버전마다 따로 저장하므로 프로젝트끼리 버전이 달라도 괜찮습니다. 빌드할 때 필요한 버전을 골라 씁니다.
파이썬의 pip 은 지금 켜진 파이썬 환경 하나에 설치합니다. 두 프로젝트가 같은 환경을 쓰면 버전이 섞입니다. 그래서 프로젝트마다 환경을 따로 만들어 씁니다. 이 따로 만든 환경을 가상 환경이라고 합니다.
언어마다 있는 패키지 매니저
주요 언어는 저마다 패키지 매니저와 공개 저장소를 갖고 있습니다. 아래 표는 백엔드에서 자주 만나는 넷을 모았습니다.
| 언어 | 패키지 매니저 | 매니페스트 파일 | 대표 공개 저장소 |
|---|---|---|---|
| 자바스크립트 | npm · Yarn · pnpm | package.json |
npm 레지스트리 |
| 파이썬 | pip · Poetry | requirements.txt · pyproject.toml |
PyPI |
| 자바 · [[Kotlin | 코틀린]] | Maven · Gradle | pom.xml · build.gradle |
| 러스트 | Cargo | Cargo.toml |
crates.io |
한 언어에 도구가 여럿이기도 합니다. 자바스크립트의 npm, Yarn, pnpm 은 같은 저장소에서 같은 패키지를 받습니다. 설치 속도와 node_modules 를 꾸리는 방식이 다릅니다.
자바 쪽 두 도구는 결이 조금 다릅니다. Maven 과 Gradle 은 컴파일, 테스트, 패키징을 맡는 빌드 도구입니다. 의존성을 받아 오는 일은 그 안의 한 기능입니다.
운영체제의 패키지 매니저
운영체제에도 패키지 매니저가 있습니다. 리눅스 서버에서 apt install nginx 를 치면 apt 가 nginx 와 그것이 쓰는 시스템 라이브러리를 함께 설치합니다. 맥에서는 Homebrew 가 같은 일을 합니다. 받아 오고 의존성을 따라가는 원리는 언어의 패키지 매니저와 같습니다.
다른 점은 무엇을 어디에 설치하느냐입니다. 아래 표가 두 쪽을 나란히 놓습니다.
| 언어 패키지 매니저 | 운영체제 패키지 매니저 | |
|---|---|---|
| 무엇을 설치하나 | 코드에서 부르는 라이브러리 | 실행 프로그램과 시스템 라이브러리 |
| 어디에 설치하나 | 프로젝트 하나, 또는 한 사용자의 공용 폴더 | 컴퓨터 전체 |
| 한 패키지의 버전 | 프로젝트마다 다를 수 있다 | 대개 컴퓨터에 하나 |
| 설치 권한 | 보통 사용자 권한 | 대개 관리자 권한 |
| 예 | npm · pip · Maven · Cargo | apt · dnf · Homebrew |
백엔드 개발자는 두 쪽을 한 파일에서 만나기도 합니다. Docker 이미지를 만드는 Dockerfile 이 그렇습니다. apt-get install 로 시스템 도구를 깝니다. 이어서 npm install 로 애플리케이션의 라이브러리를 받습니다.
대가
패키지 매니저는 남의 코드를 내 프로그램 안으로 들입니다. 전이 의존성까지 치면 내가 이름도 모르는 사람들의 코드가 함께 돕니다. 그중 한 패키지에 악성 코드가 들어가면 내 프로그램도 오염됩니다. 의존성을 통로로 삼는 이런 공격을 공급망 공격이라고 합니다.
설치하는 순간에 코드가 돌기도 합니다. 여러 패키지 매니저는 패키지 안에 든 스크립트를 설치 과정에서 실행하게 해 줍니다. 라이브러리를 한 줄도 부르지 않았는데 설치만으로 남의 코드가 내 컴퓨터에서 돕니다.
이름을 노리는 수법도 있습니다. 인기 패키지와 한 글자 다른 이름으로 악성 패키지를 올려 두고 오타를 기다립니다. requests 를 노려 reqeusts 라는 이름으로 올리는 식입니다. 이 수법을 타이포스쿼팅이라고 합니다.
패키지 저장소에 기대는 것도 대가입니다. 저장소가 멈추거나 패키지가 내려가면 그 패키지를 쓰는 빌드도 멈춥니다. 그래서 회사는 받은 패키지를 사내 아티팩트 저장소에 쌓아 두고 거기서 받게 하기도 합니다.
들인 의존성은 일감으로 남습니다. 새 버전과 보안 수정이 나올 때마다 올릴지 정해야 합니다. 올리지 않고 두면 알려진 취약점도 함께 남습니다. 그래서 표준 라이브러리로 되는 일에는 패키지를 들이지 않는 팀도 있습니다.
관련 항목
패키지 매니저가 받아 오고 다루는 대상
패키지 · 라이브러리 · 의존성 · 전이 의존성 · 매니페스트 파일 · 잠금 파일
패키지 매니저가 패키지를 내려받는 저장소
패키지 저장소 · npm 레지스트리 · PyPI · Maven Central · crates.io · 아티팩트 저장소
패키지 매니저가 버전을 고르는 규칙
시맨틱 버저닝 · 버전 범위 · 의존성 해석 · 의존성 충돌 · 의존성 지옥 · 해시
언어별로 구현한 패키지 매니저
npm · Yarn · pnpm · pip · Poetry · Maven · Gradle · Cargo · Bundler · Go 모듈
운영체제에 프로그램을 설치하는 패키지 매니저
apt · dnf · Homebrew · pkg · ports
패키지 매니저를 노리는 공격
공급망 공격 · 타이포스쿼팅 · 의존성 혼동 · 설치 스크립트 · 코드 서명
패키지 매니저와 함께 도는 개발 도구
빌드 도구 · 번들러 · 가상환경 · Docker · 지속적 통합 · node_modules
패키지 매니저가 속하는 상위 분류
다른 이름: package manager · 패키지 관리자