Clojure 의존성 확장(Dep Expansion)

Clojure 의존성 확장(Dep Expansion)

Clojure 프로젝트의 의존성을 다루다 보면, 겉으로 선언한 몇 개의 루트 의존성(root deps)이 어떻게 실제로는 수십 개의 전이 의존성(transitive deps)으로 불어나는지 궁금해질 때가 있어요. 이번 글은 Clojure 도구와 tools.deps가 그 루트 의존성 집합을 전이 의존성 집합으로 확장하는 알고리즘을 깊이 파고드는 내용이에요. 이 알고리즘은 Maven의 의존성 확장과 비슷한데요, 둘 다 너비 우선 트리 확장(breadth-first tree expansion)이지만 버전 선택에서 서로 다른 선택을 해요.

출처: Clojure 공식문서

본문

확장(Expansion)

확장의 입력은 이렇게 세 가지예요.

  • 초기 의존성(initial dependencies) — 각 의존성은 lib(심볼)와 coordinate(maven, git, local 등)로 정의돼요.
  • default-deps — coordinate가 주어지지 않았을 때 쓸 lib → coord 매핑이에요.
  • override-deps — lib가 발견되면 그 lib → coord 매핑으로 대체해 쓸 값이에요.

확장 과정은 트리의 경로(path)들을 담은 큐(queue) 를 도는 루프예요. 초기 큐에는 루트 의존성으로 가는 단일 경로들이 들어 있어요.

루프의 각 단계에서는 큐에서 경로(의존성 하나로 끝나는)를 하나 꺼내서 검토해요. 필요하면 default/override deps로 그 coordinate를 대체해 사용할 coordinate를 정하죠. 그다음 이 lib/coord를 놓고 선택에 포함할지 배제할지 결정하고, 나중에 이해할 수 있도록 사유 코드(reason code) 를 남겨요. 만약 그 노드에 자식 노드가 있으면 큐에 추가해요.

루프 내내 우리는 버전 맵(version map)에 모든 lib·본 버전·선택 결과를, exclusion 맵에 배제를 기록해요. 원하면 각 검토와 그 노드의 포함 여부·이유를 확장 트레이스(expansion trace) 로 기록할 수도 있어요.

모든 선택은 트리를 한 번 통과하는 동안에 이뤄져요. 메인라인 확장은 단일 스레드로 진행되지만, 의존성의 자식 노드를 가져오려면 Maven 저장소에서 pom을 요청하는 것처럼 외부 네트워크에서 가져와야 할 때가 있어요. 성능을 높이기 위해, 그 메타데이터를 실제로 필요해지기 전에 미리 스레드 풀을 이용해 병렬로 가져와요.

의존성 선택(Dep selection)

어떤 노드를 선택 대상으로 검토할 때, 그 lib는 다음 조건을 모두 만족하면 포함돼요.

  • top dep이거나(루트 의존성의 버전은 항상 이겨요), 새로운 lib거나 이미 아는 lib의 더 새로운 버전일 것. (Maven은 버전의 새로움과 관계없이 처음 찾은 버전만 유지하는 점이 달라요.)
  • 그리고 이 노드 경로의 부모 중 하나에서 배제되지 않았을 것(아래 Exclusions 항목 참고).
  • 그리고 경로의 모든 부모 노드가 선택되었을 것(아래 Orphans 항목 참고).

lib/버전이 포함되면 버전 맵에 선택된 것으로 표시돼요.

배제(Exclusions)

배제는 트리의 어느 노드에서 자식 노드에 표시되고, 그 자식 의존성과 그 아래 서브 노드들에 적용돼요. 배제는 exclusion 맵에 기록되고, 트리에서 그 지점 아래의 lib를 포함할지 판단할 때 사용돼요.

특히 까다로운 경우가 하나 있어요. 같은 lib·같은 버전이 트리의 서로 다른 지점에, 서로 다른 배제 집합으로 나타나는 경우예요. 이런 경우 그 lib에 적용되는 배제는 두 배제 집합의 교집합(intersection) 이 돼요.

예를 들어 이런 트리를 볼게요.

A
  B
    C (excl X, Y)
      X
      Y
      Z
  D
    C (excl X)
      X
      Y
      Z

여기서 C는 X만 배제해요(#{X Y}와 #{X}의 교집합). 왜냐하면 A-D-C 분기가 정말로 그 X를 필요로 하기 때문이죠.

또 하나 중요해요. A-B-C 경로를 검토할 때는 X와 Y가 배제되어 있으므로 Z만 자식으로 큐에 넣어요. 그리고 A-D-C를 검토할 때는 C의 배제 집합을 좁혀서, Y를 C의 새 자식으로 포함시켜 확장하도록 표시해요. A-B-C와 A-D-C 중 무엇을 먼저 검토하느냐에 따라 확장 순서는 달라질 수 있지만, 포함에 관한 최종 선택은 항상 같아져요.

고아(Orphans)

큰 트리를 확장하는 동안, 특정 lib 버전의 자식들을 큐에 넣은 다음에, 나중에 그 lib의 더 새로운 버전을 찾아서 선택하게 될 수 있어요. 그 새 버전은 다른 의존성 집합을 가질 테니, 원래 lib 버전 노드는 선택 해제되지만 그 자식들은 여전히 큐에 남아 있을 수 있어요.

그 자식들을 마주치면, 모든 부모 노드가 여전히 선택된 경우에만 포함돼요.

예를 들어 이런 트리를 볼게요.

A
  B
    C1
      X
  D
    C2
      Y

트레이스는 이렇게 보일 수 있어요.

  • A — A 포함, top dep
  • A-B — B 포함, 새 lib
  • A-D — D 포함, 새 lib
  • A-B-C1 — C1 포함, 새 lib (X, Y 큐에 넣음)
  • A-D-C2 — C2 포함, 더 새로운 버전 (+ C1 선택 해제)
  • A-B-C1-X — X 배제, 부모가 빠짐
  • A-D-C2-Y — Y 포함

어떤 경우에는 부모 노드가 선택 해제되기 전에 자식 노드가 이미 버전 맵에 선택되어 있을 수 있어요. 이런 경우를 잡기 위해, 확장이 끝난 뒤 고아 검사(orphan check) 를 해서 선택된 모든 lib가 포함된 부모 노드를 갖고 있는지 확인해요. 그렇지 않으면 고아가 된 노드들을 잘라내요.

A
  B1
    X
  C
    B2
      Z

트레이스:

  • A — A 포함, top dep
  • A-B1 — B1 포함, 새 lib
  • A-C — C 포함, 새 lib
  • A-B1-X — X 포함, 새 lib
  • A-C-B2 — B2 포함, 더 새로운 버전 (+ B1 선택 해제)
  • A-C-B2-Z — Z 포함

확장 후에는 A, X, C, B2, Z가 선택되어 있을 거예요. 하지만 각 노드를 확인해보면 X의 부모 B1이 포함되지 않았으므로 X는 잘리게 돼요.

트레이스 데이터(Trace data)

커맨드 라인 도구를 쓸 때는 -Strace 옵션으로 트레이스(dep tracing)를 켤 수 있어요. 그러면 현재 디렉터리에 트레이스 데이터 파일 trace.edn이 만들어져요. 그 데이터를 열어보면 다음 키들을 가진 맵이 보여요.

  • :log — 검토한 lib 노드들의 로그로, 각 노드의 포함 여부와 그 이유를 담아요. 로그는 검토한 노드마다 하나씩인 맵의 벡터이고, 각 맵에는 :lib, :coord, :coord-id, :paths, :include, :reason 그리고 때로 다른 키들이 들어 있어요.
  • :vmap — 버전 맵 (형식은 바뀔 수 있어요)
  • :exclusions — 배제 맵 (형식은 바뀔 수 있어요)

트리 출력(Tree printing)

의존성 트리는 clj -Streeclj -X:deps tree로 출력할 수 있어요. 후자는 옵션이 더 많아요.

트리는 트레이스 로그로 만들어지고, 검토된 모든 노드를 포함해요. 포함된 노드 앞에는 .이 붙고, 배제된 노드 앞에는 X가 붙어요. 줄 끝에는 사유 코드가 나타나요(일부 코드는 생략돼요). 현재 사유 코드 집합(바뀔 수 있어요)은 이래요.

  • :new-top-dep — top dep로 포함됨 (생략)
  • :new-dep — 새 의존성으로 포함됨 (생략)
  • :same-version — 배제, 현재 선택된 의존성과 같음 (생략)
  • :newer-version — 포함, 이전에 선택된 것보다 더 새로운 버전
  • :use-top — 배제, top lib와 같지만 top이 아님
  • :older-version — 배제, 이전에 선택된 것보다 더 오래된 버전
  • :excluded — 배제, 부모 경로의 노드가 이 lib를 배제함
  • :parent-omitted — 배제, 부모 노드가 선택 해제됨
  • :superseded — 배제, 이 버전이 선택 해제됨

더 알아보기