라이브러리 비용은 설치 때가 아니라 바꿀 때 온다
이 글은 혼자서 서비스를 만드는 개발자를 위한 것입니다. 읽고 나면 얻는 것이 하나 있습니다. 라이브러리의 진짜 비용이 언제 청구되는지, 그리고 그 청구 시기를 우리 쪽으로 가져오는 습관입니다.
결론부터 말합니다. 의존성의 비용은 설치하는 순간이 아닙니다. 바뀌는 순간, 업데이트하고 교체하고 제거할 때 청구됩니다. 그 청구서를 내는 사람은 언제나 우리입니다.
반복되는 장면이 있습니다. 날짜 처리나 파일 업로드, 인증 같은 작은 문제를 위해 레지스트리를 검색해 별점이 괜찮은 라이브러리를 골라 5분 만에 설치합니다. 첫 예제가 그대로 돌아가고 그날 밤은 잘 삽니다. 열여덟 달 뒤, 같은 라이브러리는 더 이상 작은 문제 하나만 맡지 않습니다. 설정 파일과 배포 스크립트, 테스트까지 그걸 참조하게 됩니다. 몇 시간 아끼려고 빌려온 것이, 건드릴 때마다 몇 시간을 갚아야 하는 존재가 된 것입니다. 이 글은 그 차이를 만드는 구조를, 그리고 그 대가를 우리 쪽으로 빼내는 방법을 논증합니다.
글의 핵심 개념을 형상화했습니다.
대가를 치르는 순간은 설치 때가 아니다
설치 순간에는 아무 일도 일어나지 않습니다. 그래서 결정이 공짜로 느껴집니다. 실제로는 계약이 서명됐을 뿐이고 대가는 네 갈래로 나뉘어 미래에 예약돼 있습니다.
첫째는 유지보수 대가입니다. 그날부터 매 릴리스마다 체인지로그를 읽고 보안 고지를 확인하고 깨진 부분을 고칩니다. 버전을 제자리에 두는 비용이 제로인 것은 없습니다. 그 비용은 미래로 넘어가며 자랄 뿐입니다.
둘째는 학습 대가입니다. 문서는 어떻게 쓰는지만 가르쳐 주지만 깨졌을 때 뭘 할지는 직접 익혀야 합니다. 셋째는 보안 대가입니다. 라이브러리마다 문이 하나씩 생기고 그 열쇠는 우리가 갖지 못합니다. 방치된 패키지가 교체되는 순간 공격 표적이 된다는 일은 실제로 일어나고 있습니다. 넷째는 라이선스 대가입니다. 안 읽은 조항에 묶일 수 있고 개인 사용은 괜찮아도 상업적 재배포는 이야기가 다를 수 있습니다.
네 가지가 한꺼번에 보이지 않는 것이 함정입니다. 학습 대가는 가장 먼저 오고 유지보수 대가는 몇 달 뒤, 보안 대가는 수년 뒤에 나타납니다. 결정 순간에는 전부 공짜처럼 보일 수밖에 없습니다. 보이지 않는 대가는 통제가 되지 않습니다. 기록해야 통제가 되는데, 이 글의 나머지 부분은 바로 그 기록의 틀을 세우는 작업입니다.
NotebookLM이 소스를 종합해 생성한 인포그래픽입니다.
설치는 계약이다, 구매가 아니다
의존성의 가장 짧은 정의는 이렇습니다. 쓰고 있지만 통제하지 못하는 코드. 다음 버전이 언제 올지, 유지보수를 멈출지, 조항을 바꿀지, 질문에 답할지 전부 상대방이 정합니다. 우리는 내미는 것을 그대로 써야 합니다.
그러니까 설치는 짧은 계약의 서명입니다. 조항은 네 개뿐인데, 하나하나가 아프습니다. 상대방은 메이저 버전으로 언제든 우리 빌드를 깨뜨릴 수 있습니다. 유지보수를 그만둘 수 있고 조항을 더 엄하게 바꿀 수 있습니다. 상대방이 사라지면, 정리 비용은 우리가 냅니다.
조항을 안 읽은 채 서명하는 것이 대다수입니다. 체인지로그도, 이슈 목록도 안 보고 설치합니다. 설치 순간에는 아무 일도 안 일어나지만, 의무는 그 순간 이미 시작됩니다. 이후 이 라이브러리는 매 릴리스, 매 취약성 고지, 매 배포마다 확인해야 하는 존재가 됩니다.
1인 팀에게는 이 계약이 더 무겁습니다. 팀이 있으면 업데이트는 이슈 하나를 건네는 일이고 깨진 빌드는 누군가의 아침입니다. 1인 팀에서는 같은 일이 전부 내일입니다. 대가는 인원으로 나눌 수 없고, 왜 이 라이브러리를 골랐는지 아는 사람도 나 하나입니다. 갈아타기가 다음 기능 포기로 보이는 것도 1인 팀의 현실입니다.
그래서 1인 팀에는 대가의 합보다, 대가가 오는 시기가 중요합니다. 한 번에 오는 대가는 견딜 수 있지만 매 분기 조금씩 오는 대가는 쌓입니다. 모르는 채로 빌리면 부채이고, 아는 채로 빌리면 투자입니다. 그 경계선은 대가를 기록하는 행위, 바로 그 자체입니다.

설치 전 가장 중요한 질문, 제거 비용은 얼마인가
가장 흔히 보는 기준, 다운로드 수는 가장 믿을 수 없는 기준입니다. 얼마나 많은 사람이 써 봤는지를 재지, 지금도 안전하게 쓰는 사람이 몇 명인지 재지 못합니다. 한때 인기였다가 방치된 라이브러리도 다운로드 수는 그대로 높습니다. 오늘 설치하는 것은, 다음 업데이트가 언제 올지 모르는 것에 대한 베팅입니다.
대신 볼 수 있는 바깥 신호가 여섯 가지 있습니다. 릴리스 주기가 어떤 리듬으로 와는지, 이슈에 유지자가 답을 하는지, 체인지로그에 무엇을 깨뜨렸고 어떻게 이주하는지 적혀 있는지, 롤백이 잦은지, 유지자가 한 명인지 팀인지, 그리고 이 라이브러리가 다른 의존을 몇 개 끌고 오는지입니다. 소스를 읽지 않아도 전부 확인할 수 있습니다.
바깥 신호보다 결정적인 것은 안쪽 질문입니다. 설치 전에 답을 써야 할 질문이 다섯 개인데, 그중 가장 중요한 것은 하나입니다. 제거 비용은 얼마인가. 라이브러리의 API 표면이 작으면 제거는 쉽습니다. 그런데 타입이 우리 도메인 모델로 흘러들고, 앱의 라이프사이클을 라이브러리가 관리하기 시작하면, 제거는 프로젝트가 됩니다. 이 질문 하나가 도구를 식구로 구분합니다.
이 질문에 답하는 가장 확실한 방법은, 스크래치 브랜치에서 30분 동안 써 보는 것입니다. 핵심 용례 두세 개만 구현하고 빌드 시간과 번들 크기를 라이브러리 없는 상태와 비교합니다. 그리고 가장 중요한 단계, 라이브러리를 지워봅니다. 임포트 몇 개만 지우면 끝난다면 잠금은 낮습니다. 타입 이름과 설정 키까지 함께 바뀌면 잠금은 높습니다. 실제 빌림 전에 제거 비용을 재 본 셈입니다.
이 30분은 뒤의 수개월을 사는 시간입니다. 판정도 여기서 갈립니다. 핵심 용례가 돌고 제거가 쉬우면 들여옵니다. 핵심은 되는데 제거가 어렵다면, 그 잠금을 사는 값인지를 먼저 묻습니다. 기능이 좋은지는 그 다음 문제입니다. 핵심 용례 자체가 안 되면, 문서를 아무리 정성 들인 후보도 배제합니다.

잠금의 척도, 쓸 수 있는 것과 안 쓸 수 없는 것의 차이
잠금은 API를 얼마나 쓰느냐의 문제가 아닙니다. 라이브러리의 개념이 우리 코드로 새어나오느냐의 문제입니다. 새어 나오는 곳은 이름, 데이터 모양, 프로세스, 설정 네 군데입니다.
가장 흔한 새는 곳은 이름입니다. 우리 타입 이름에 라이브러리 접두사가 붙기 시작하면, 도메인 한가운데에 라이브러리 이름이 앉게 됩니다. 더 큰 곳은 데이터 모양입니다. 테이블 구조가 라이브러리의 기대에 맞춰지면, 데이터에 지문이 남습니다. 가장 깊은 곳은 프로세스입니다. 라이브러리가 움직이면 우리가 움직이고 멈추면 우리가 멈추는 상태. 의존성은 쓸 수 있는 것이고, 잠금은 안 쓸 수 없는 것입니다.
이 차이를 등급으로 두면, 아래와 같은 척도가 됩니다.
| 잠금 단계 | 예 | 지문이 남는 곳 | 교체 비용 |
|---|---|---|---|
| 낮음, 순수 유틸리티 | 포맷 함수, 검증 함수 | 임포트만 | 시간 단위 |
| 중간, ORM이나 HTTP 클라이언트 | 쿼리 빌더, 데이터 모양 | 타입, 스키마, 설정 | 일 단위 |
| 높음, 프레임워크나 런타임 | 웹 프레임워크, 앱 라이프사이클 | 프로세스, 시작과 종료 | 주 단위 |
위쪽을 피하라는 뜻은 아닙니다. 프레임워크에는 가치가 있습니다. 다만 위쪽을 고르는 순간, 교체 비용은 주 단위로 재어집니다. 시간 단위와는 차원이 다릅니다. 핵심 용례가 같은 두 후보라면, 잠금 한 단계를 아래로 고르는 것이 안전한 선택입니다. 잠금이 높을수록, 통합 방식도 단단하게 해야 합니다.

변경의 시기를 우리 쪽에 둔다
고르는 것으로는 반이 끝납니다. 다른 반은 설치한 순간부터 시작됩니다. 첫 번째 규율은 파일 두 개의 차이를 아는 것입니다. 매니페스트, package.json 같은 파일은 의도입니다. 허용하는 버전의 범위를 적습니다. 2.x면 무엇이든 같은 것. 락파일은 계약입니다. 설치 당시 실제로 쓴 정확한 버전을 적습니다. 둘을 혼동하는 데서 대부분의 사고가 시작됩니다.
그러니까 락파일을 버전 관리에 커밋합니다. 1인 프로젝트여도, 내부 도구여도. 같은 매니페스트로 같은 날의 빌드가 서로 다르게 나오지 않게 하는 것은 락파일뿐입니다. 프로덕션도 락파일에 적힌 것을 그대로 설치하게 합니다. 배포할 때마다 최신으로 다시 해석되면, 언제 무엇이 바뀌었는지 알 수 없게 됩니다. 락파일과 매니페스트가 불일치하면, 락파일을 믿고 매니페스트를 고칩니다.
버전 번호는 힌트입니다. SemVer의 약속은 간단합니다. 패치는 버그만 고치고 마이너는 기능을 더하지만 깨뜨리지 않는다고 합니다. 그런데 실제로는 마이너에 깨진 변경이 숨어 들어옵니다. 유지자가 동작 변경을 개선했다고 여긴 실수도, 문서가 기대하지 말라고 말하지 않는 회색 지대도 있습니다. 버전 번호가 지켜주지 못하는 곳에서 지켜주는 것은, 우리 동작을 코드로 만든 테스트입니다. 업데이트 전에는 체인지로그를 읽고 후에는 테스트 스위트를 돌려, 실패의 diff를 읽습니다. 이 두 동작의 비용은 몇 분이면서 잡는 것은 몇 주입니다.
리듬도 필요합니다. 업데이트는 매 릴리스마다 할 일이 아니라, 정해둔 시간대에 배치하는 일입니다. 패치와 마이너는 월요일 아침 15분 같은 칸으로 몰아, 대기 중이던 것을 한꺼번에 올리고 전체 테스트를 돌려, 초록인 것만 하나의 커밋으로 넣습니다. 매일 하나씩 올리면, 컨텍스트 전환 비용이 업데이트 비용보다 커집니다. 메이저는 배치에 섞지 않습니다. 별도 브랜치를 열어 이주를 그 브랜치의 일로 만들고, 시간 상한을 둡니다. 상한 안에 안 끝나면 한 박자 물러서서 질문을 바꿉니다. 이주할 가치가 있는가. 매년 이주에 반나절을 쓰는 라이브러리는, 1인 팀에게는 무겁습니다.
교체를 싸게 만드는 일은, 라이브러리를 고를 때가 아니라 통합할 순간에 합니다. 우리 코드와 라이브러리 사이에, 우리 것인 얇은 인터페이스를 하나 둡니다. 어댑터든 래퍼든, 이름은 아무거나. 규칙은 하나입니다. 라이브러리의 타입은 우리 인터페이스를 넘지 못한다. 로깅 함수를 여기저기서 직접 호출하면 로깅 라이브러리에 잠깁니다. 작은 모듈 하나를 사이에 두면, 구체적인 호출은 그 파일 한 곳에만 남습니다. 바꾸려면 건드릴 파일이 하나이고 제거 비용이 상수로 고정됩니다. 단, 인터페이스는 우리 도메인의 언어를 말해야 합니다. 주문 확인을 보낸다고 쓰는 것이지, 라이브러리 메서드 이름을 그대로 옮기는 것이 아닙니다.

제거는 소유의 시험이다
좋은 의존성은 잘 동작하는 것이 아닙니다. 빼고 싶을 때, 깨끗이 빼낼 수 있는 것입니다. 제거는 빌림의 마지막 시험입니다. 설치는 남의 코드를 들여오는 일이고, 제거는 우리 코드가 그 코드를 어디까지 알고 있는지 전부 드러내는 일입니다. 잘 하면, 시스템의 약한 곳이 한 장의 목록으로 나옵니다.
참조는 네 군데에 숨습니다. 임포트에는 드러나고 도구가 찾아줍니다. 설정에는 숨습니다. 라이브러리에서 온 이름의 키, 환경 변수, 라이브러리가 있을 때만 의미가 있는 플래그. 락파일에는 남습니다. 매니페스트 항목을 지워도, 다른 라이브러리가 그걸 의존하고 있으면 트리에 다시 올라옵니다. 이걸 고스트 의존성이라고 합니다. 그리고 가장 깊은 곳, 우리 머리에는 잔상이 남습니다. 라이브러리의 기대에 맞춰진 타입 이름과 데이터 모양은, 전체 검색으로도 안 보입니다.
제거는 다섯 단계로 합니다. 먼저 참조 매핑입니다. 라이브러리 이름으로 코드 전체와 설정, 배포 스크립트, 문서까지 검색하고 결과를 확인할 목록으로 붙여 둡니다. 그 다음 코드 제거입니다. 호출과 임포트를 지우되, 제거와 대체는 별개의 일이므로 동시에 하지 않습니다. 세 번째, 매니페스트 항목을 지우고 락파일을 재생성해, 의존 개수가 실제로 줄었는지 확인합니다. 안 줄었다면 고스트가 있는 것이니, 매핑으로 돌아갑니다. 네 번째, 클린 빌드입니다. 신선한 클론과 깨끗한 환경에서 빌드합니다. 증분 빌드는 오래된 산출물을 숨기기에, 로컬에서는 되면서 파이프라인에서는 깨진다는 사고의 흔한 원천입니다. 다섯 번째, 측정입니다. 빌드 시간과 번들 크기, 의존 개수를 이전과 비교해 기록합니다.
반쯤 지워진 제거는, 지우지 않은 것보다 나쁩니다. 회복 절차는 단순합니다. 먼저 제거 커밋을 되돌립니다. 동작하던 상태로 돌아가야, 다음 제거의 기준점이 생기니까요. 다음으로, 에러 메시지에 나온 파일 이름과 함수 이름이 놓친 참조의 장소라는 힌트를 뽑아, 참조 매핑을 다시 돌리고 힌트를 목록에 합쳐서 다시 제거합니다. 두려워해야 할 것은 반쯤 지워진 상태가 아니라, 되돌리지 않는 것입니다. 제거도 작은 커밋으로 나누면, 실패의 대가는 몇 분이 됩니다.
이 시험은 분기에 한 번, 30분으로 정기화합니다. 직접 의존성을 차례로 훑으며 들여올 때 맡은 일을 아직 하고 있느냐고 묻습니다. 서비스는 진화했고 라이브러리의 일은 사라졌지만 트리에만 남은 의존성이 대다수입니다. 각 의존성을 유지, 관찰, 교체, 퇴장으로 분류합니다. 분류의 정확도보다, 분류를 했느냐가 중요합니다. 아무도 안 보는 의존성은, 아무도 모르게 죽어 가는 의존성이기 때문입니다. 퇴장 판정은 같은 주에 제거 절차를 돌립니다. 표 위에 한 달 남아 있는 판정은, 돌아오지 않는 판정입니다.
빌려 쓰는 코드도 우리 코드다
빌려 쓰는 코드도 우리 코드입니다. 라이브러리의 소스가 우리 것이라는 뜻이 아닙니다. 쓰는 결과물이 우리 것이라는 뜻입니다. 업데이트의 비용, 학습의 비용, 보안의 비용, 제거의 비용, 전부 우리 계좌에 찍힙니다.
규율을 한 줄로 압축하면, 이렇습니다. 설치는 계약입니다. 계약 순간에 조항을 기록하는 것. 대가, 제거 비용, 잠금의 높이. 나머지는 유지입니다. 체인지로그를 읽고 테스트를 돌려, 표를 갱신하고 때 되면 빼는 것. 변화의 시기도, 변화의 비용도, 우리가 정합니다.
오늘 하나만 해도 됩니다. 매니페스트를 열어 직접 의존성 개수를 세고, 그중 제거 비용이 가장 큰 하나를 골라, 이름으로 코드 전체를 검색해 보는 것. 참조 매핑 단 하나면 됩니다. 그 목록이 오늘, 시스템이 어디에 약한지 알려줄 것입니다. 그리고 다음 설치에서, 가장 조심해야 할 것이 무엇인지도.
빌림은 공짜가 아닙니다. 다만, 대가를 기록한 빌림은 관리할 수 있는 빌림입니다. 이 글의 모든 단계를 예시와 함께 한 권으로 정리한 전자책 의존성의 규율도 함께 준비되어 있습니다. 더 깊이 파고들고 싶은 분은, 그 쪽으로 가 보시기 바랍니다.
참고 자료
본문의 SemVer 약속, 매니페스트와 락파일의 차이, 트리에 다시 올라오는 고스트 의존성, 보안 고지 확인은 아래 자료와 대조해 보실 수 있습니다.
- Semantic Versioning 2.0.0 (semver.org)
- package.json (npm docs)
- package-lock.json (npm docs)
- npm-ls (npm docs)
- npm-audit (npm docs)
NotebookLM이 소스를 종합해 생성한 인포그래픽입니다.