Home 21. 의존성 관리
Post
Cancel

21. 의존성 관리

의존성 관리란 우리가 통제하지 못하는 라이브러리, 패키지, 그 외 의존성들의 네트워크를 관리하는 일을 말한다.

의존성 관리와 관련이 가장 깊은 주제는 소스 버전 관리이다. 의존성 관리는 시간과 확장 양 축 모두에서 복잡도를 키운다. 소스 관리에서는 코드를 변경할 때 테스트를 수행하고 기존 코드를 손상시키지 않아야 한다. 코드베이스가 공유되어 있어 무엇이 어떻게 이용되는지 볼 수 있고 우리가 직접 빌드하고 테스트할 수 있다. 반면 의존성 관리는 우리에게 접근 권한이 없거나 속을 들여다볼 수 없는, 조직 외부에서 이루어지는 변경 때문에 일어나는 문제를 다룬다.

소스 관리와 의존성 관리는 ‘이 하위 프로젝트의 개발, 업데이트, 관리를 우리 조직이 통제하는가?’ 라는 질문으로 구분지을 수 있다. 예) 리포지터리, 목표, 개발 프로세스를 팀마다 다르게 가져간다면 이 팀들이 생산하는 코드끼리의 연동과 조율은 의존성 관리에 가깝다.

21.1 의존성 관리가 어려운 이유

의존성 하나를 관리하는 방법이 중요한 게 아니다. 수많은 의존성들로 구성된 네트워크와 그 네트워크에 미래에 일어날 변화까지 고려해 관리하는 방법을 강구해야 한다.

21.1.1 요구사항 충돌과 다이아몬드 의존성

의존성 관리에서는 의존성 네트워크를 중심에 두고 생각해야 한다. 대부분의 난제는 하나의 문제에서 비롯된다 ‘의존성 네트워크상의 두 의존성 사이에서 요구사항 충돌할 때 어떻게 해결하는가?’ 이런 문제는 다양한 이유로 발생한다. 그 중 기존 버전과 호환되지 않아서일 수 있는데 대표적인 예로 다이아몬드 의존성 문제이다.

요구사항 충돌 문제의 대부분은 의존성 그래프에 특정 조건을 요구하는 노드가 추가되어 일어난다.

554~556p

21.2 의존성 임포트하기

프로그래밍 측면에서 보면 직접 처음부터 새로 짜는 것보다 기존 인프라를 재활용하는 게 분명히 더 낫다.

21.2.1 호환성 약속

개발 비용을 줄일 수 있다고 해서 의존성을 임포트하는 게 꼭 옳은 선택은 아니다. 특히 소프트웨어 엔지니어링 조직이라면 시간과 변경을 항시 염두에 두어야 한다.

다음의 요인들을 고려하면 의존성의 유지보수 비용을 조금 더 정확하게 계산할 수 있다.

  • 호환성이 얼마나 잘 지켜지나요?
  • 진화가 얼마나 빠르게 이루어지나요?
  • 변경 처리 방법은 무엇일까요?
  • 각 버전의 지원 기간은 어떻게 되나요?

C++

C++ 표준 라이브러리는 거의 무한대의 하위 호환성을 제공하는 예이다. 구버전 표준 라이브러리용으로 빌드한 바이너리는 신버전에서도 빌드되고 링크된다. API 호환성뿐 아니라 바이너리 아티팩트들과의 하위 호환성도 꾸준히 제공해준다. 이를 ABI 호환성 이라 한다.

GO

GO 언어는 대부분의 릴리스에서 소스코드가 호환되게 해주었지만 바이너리를 그렇지 않다. GO 언어에서는 빌드한 버전이 다른 바이너리끼리는 링크할 수 없다.

Abseil

GO와 많이 비슷하면서 시간에 대한 경고를 추가한 버전이라고 할 수 있다. Abseil는 온전한 ABI 호환성 대신 API 호환성을 다소 제한된 형태로 약속한다. 정확하게는 호환되지 않는 API 변경도 있을 수 있으나, 이때는 반드시 새 API로 마이그레이션 해주는 자동 리팩터링 도구를 함께 제공할 것을 약속한다.

Boost

C++ 라이브러리인 Boost는 버전 간 호환성을 보장하지 않는다. 따라서 Boost 이용자들은 호환성 문제가 생기기 전까지만 업그레이드하는게 바람직하다.

Boost의 특정 릴리스가 운 좋게 완벽하게 안정적이라서 많은 프로젝트에서 사용하기에 적합할 수 있겠지만 다른 버전과의 호환성은 우선순위가 높지 않다. 그래서 수명이 긴 프로젝트에서 Boost를 계속 최신으로 유지하려다가는 언젠가 호환성 문제를 겪을 것이다.

21.2.2 임포트시 고려사항

프로그래밍 프로젝트라면 의존성 임포트는 거의 공짜에 가깝다. 하지만 소프트웨어 엔지니어링으로 발을 옮기는 순간 똑같은 의존성들이 미묘하게 더 삐싸진다.

구글에서 의존성을 임포트하려는 엔지니어들에게 던지는 질문 (560p)

21.2.3 의존성 임포트하기 @구글

구글 프로젝트들이 이용하는 의존성들의 압도적 다수를 구글이 직접 개발했다. 구글의 내부 의존성 관리의 대다수는 진정한 의존성 관리가 아니다.(설계상 소스코드 버전 관리에 해당)

구글은 오픈소스 생태계나 상용 파트너사로부터 임포트하는 의존성들은 모노리포의 별도 디렉터리에 추가한다.

예시 (561~563p)

21.3 (이론상의) 의존성 관리

안정적인 의존성 관리 체계란 시간과 규모 모든 면에서 유연해야 한다. 의존성 그래프의 어느 노드라도 영원히 변치 않으리라 가정해서는 안 된다. 마찬가지로 새로운 의존성이 더해질 일은 없으리란 기대도 금물이다.

21.3.1 변경 불가(정적 의존성 모델)

애초부터 변경 자체를 허용하지 않으면 기존 의존성 때문에 불안해할 일이 사라진다. 사용자 코드 동작에 영향을 주지 않는 선에서의 버그 수정만 유일하게 허용된다.

변경 불가는 신생 조직에게는 적합한 모델일 수 있다. 프로젝트가 오래 살아남을수록 언제까지 유효할지를 정확하게 알 수 있는 지표가 없다.

21.3.2 유의적 버전(SemVer)

유의적 버전은 오늘날 의존성 네트워크를 관리하는 가장 대표적인 방법이다. SemVer는 의존성 버전을 표기하는 보편적인 방식으로 숫자 세 개로 표현한다. 메이저.마이너.패치버전을 의미하며 각각 다음과 같은 경우 증가한다.

  • 메이저: API가 변경되어 의존성을 이용하던 기존 코드를 깨뜨릴 수 있음
  • 마이너: 순수하게 기능 추가만 있음
  • 패치: API에 영향을 주지 않는 내부 구현 개선과 버그 수정

메이저 버전이 달라지면 일반적으로 호환성이 크게 떨어진다. 기존 기능들이 달라지거나 심지어 사라졌을 수도 있어서 이를 이용하던 모든 코드가 잠재적으로 문제를 겪을 수 있다.

의존성 네트워크 전체를 대상으로 어떤 알고리즘을 수행하여 모든 버전 요구사항을 충족하는 의존성 버전을 찾는 행위를 버전 선택이라 한다. 그리고 만족스러운 버전 조합이 네트워크에 존재하지 않는 상황을 소위 의존성 지옥이라 한다.

21.3.3 하나로 묶어 배포하기

하나로 묶어 배포하기는 애플리케이션 구동에 필요한 의존성들을 모두 찾아서 애플리케이션과 함께 배포하는 방법이다.

이 모델에서는 새로운 역할인 배포자가 등장한다. 개별 의존성의 메인데이터들은 다른 의존성들에 대해서는 거의 몰라도 된다. 더 거시적인 관리를 책임지는 배포자가 상호 호환되는 버전들을 찾고 패치하고 검증하는 일을 수행해주기 때문이다.

외부 사용자 입장에서는 적당한 특정 배퐌 하나에만 의존하면 되니 매우 편리하다.

21.3.4 헤드에서 지내기

이 방법은 이론적으로는 멋지지만 의존성 네트워크 참여자들에게 비싼 부담을 새로 지운다는 단점이 있다. 헤드에서 지내기는 트렁크 기반 개발을 의존성 관리 영역까지 확장한 걸로 보면 된다. 소스 관리 정책인 트렁크 기반 개발을 업스트림 의존성에까지 적용한 모델이다.

헤드에서 지내기는 의존성 관리에서 시간과 선택이라는 요소를 제거하려는 시도이다. 모든 컴포넌트가 항상 최신 버전에 의존하며, 의존하는 쪽에서 수용하기 어려운 형태의 변경은 절대 허용하지 않는다. 의도치 않게 API나 행위가 달라지게 하는 변경은 일반적으로 다운스트림 의존성의 CI에서 포착되므로 커밋되지 않도록 한다.

헤드에서 지내기 모델에서는 더 이상 SemVer에게 이 버전은 안전할까? 라고 묻지 않는다. 대신 테스트와 CI 시스템을 이용해서 시야에 들어오는 모든 사용자들에게 안전한 지를 실험을 통해 확인한다.

21.4 유의적 버전의 한계

헤드에서 지내기는 트렁크 기반 개발이라는 버전 관리 방식을 토대로 하지만 확장성이 좋은지까지는 입증이 더 필요하다. 현재까지 업계 표준 의존성 관리는 유의적 버전(SemVer)이다.

SemVer에서 점으로 구분한 버전 번호의 정확한 정의에 대해서 릴리스에 부여된 버전 숫자가 호환성을 확실하게 보장해주는지 알 수 없다. 간단한 버드 수정이라도 현실에서는 libbase를 설계 의도와 다르게 사용 중인 사용자 때문에 빌드가 깨질 수도 있고 단순한 API 추가 행위가 라이브러리가 엉뚱하게 동작하게 만드는 등 온갖 경우의 수가 있다. 근본적으로 자신의 API만 고려해서는 호환성에 대해 아무것도 증명할 수 없다.

21.4.1 지나치게 구속할 수 있다

libbase가 Foo와 Bar라는 단 두 개의 함수만 제공한다고 했을 때, 중간 단계 의존성인 liba와 libb는 모두 Foo만 사용한다. 이때 libbase의 메인테이너가 Bar에 파괴적인 변경을 진행했다. SemVer 규칙에 따라 메이저 버전을 올려야하는 상황인데 libbase 1.x에 의존 중인 liba, libb에게 SebVer 의존성 솔버는 2.x 버전은 허용하지 않을 것이다. 하지만 실제로는 2.x를 이용해도 아무런 문제가 없다. 파괴적인 변경이 있었으니 메이저 버전을 높여야 해 라는 표현을 독립적인 API 단위까지 적용하지 않는다면 지금 예처럼 정보가 손실된다.

21.4.2 확실하지 않은 약속일 수 있다

SemVer의 패치 버전은 내부 구현만 달라져서 이론적으로는 안전한 변경이다. 얼핏 사소해 보이는 변경도 하이럼의 법칙에서 자유로울 수 없다. 사용자 수가 늘어나면 누군가는 기존의 동작 방식에 의존하는 코드를 작성하게 된다.

572~573p

21.4.3 버전업 동기

21.4.4 최소 버전 선택

21.4.5 그래서 유의적 버전을 이용해도 괜찮은가?

21.5 자원이 무한할 때의 의존성 관리

현재 SemVer에 의지하는 이유는 다음의 세 가지이다.

  • 내부 정보만 있으면 된다.
  • 충분한 테스트를 수행할 컴퓨팅 자원, 테스트 결과를 모니터링해줄 CI 시스템이 존재하지 않아도 된다.
  • 관행이다.

특히 의존성 네트워크는 대체로 다음의 두 가지 환경에서만 만들어진다.

  • 하나의 조직 안
  • OSS 생태계 안

577~579p

21.5.1 의존성 내보내기

선한 의도로 라이브러리를 오픈 소스로 공개했는데 오히려 조직에 해를 끼치는 방식은 크게 두 가지 이다.

  1. 구현 품질이 떨어지거나 제대로 관리하지 못하면 조직의 평판을 떨어트린다.(예시 gflags 580~582p)
  2. 동기화를 유지할 수 없다면 선의의 릴리스가 엔지니어링 효율을 떨어트릴 것이다.

21.6 마치며

의존성 관리는 본질적으로 어렵다. 현재 의존성 네트워크를 관리하는 업계 표준 방법은 유의적 버전, 즉 SemVer이다. 하지만 SemVer는 변경에 수반되는 위험을 요약해 제공하다 보니 때로는 중요한 내용을 빠뜨린다는 한계가 있다.

21.7 핵심 정리

  • 의존성 관리보다는 되도록 버전 관리가 되도록 한다.
  • 소프트웨어 엔지니어링 프로젝트에서 의존성 추가는 공짜가 아니다.
  • 의존성 역시 하나의 계약이다. 주는게 있고 받는 게 있다.
  • SemVer는 변경이 얼마나 위험할지를 사람이 추정하는 간단하지만 정보가 일부 손실되는 표현법이다.
  • 테스트와 CI는 새로운 버전들이 잘 어울려 돌아가는지를 실제로 보여준다.
  • SemVer 기반 패키지 관리에 최소 버전 선택 전략을 가미하면 충실성이 올라간다.
  • 단위 테스크, 지속적 통합, 컴퓨팅 자원이 의존성 관리를 이해하고 처리하는 식에 변화를 가져올 수 있다.
  • 의존성을 제공하는 일 역시 공짜가 아니다. 공개한 의존성을 안정성 있게 지원한다면 마음대로 수정하는 것이 어려워진다.
This post is licensed under CC BY 4.0 by the author.

20. 정적 분석

22. 대규모 변경