의존성 확장

의존성 확장 (Dep Expansion)

이 페이지는 Clojure 도구(tools)와 tools.deps가 루트 의존성 집합을 전이적 의존성 집합으로 확장(expand)하는 방식을 깊이 다루는 글이에요. 이 알고리즘은 Maven의 의존성 확장과 유사해요(둘 다 너비 우선 트리 확장인데), 버전 선택에 있어서는 다른 선택을 해요.

출처: Dep Expansion

확장 (Expansion)

확장의 입력은 다음과 같아요:

  • 초기 의존성(initial dependencies) — 각 의존성은 라이브러리(symbol)와 좌표(coordinate: maven, git, local 등)로 정의돼요.
  • 기본 의존성(default-deps) — 좌표가 지정되지 않은 경우 사용할 lib→coord 맵이에요.
  • 재정의 의존성(override-deps) — 해당 lib가 발견되면 사용할 lib→coord 맵이에요.

확장 과정은 트리 안의 경로(path) 큐에 대한 루프예요. 초기 큐는 루트 의존성들로 가는 개별 경로들로 구성돼요.

루프의 각 단계에서 큐에서 경로(의존성으로 끝나는) 하나를 꺼내 고려해요. 기본/재정의 의존성을 (필요하면) 사용해 그 의존성에 사용할 좌표를 대체해요. 그다음 이 lib/coord를 고려해 선택에 포함할지 배제할지를 결정하고, 나중에 이해할 수 있도록 이유 코드(reason code)를 기록해요. 노드에 자식 노드가 있으면 그것들을 큐에 추가해요.

루프 내내 모든 lib, 본 버전, 선택 선택지를 버전 맵(version map)에, 배제 항목을 배제 맵(exclusions map)에 추적해요. 원한다면 각 고려와 그 노드의 포함 여부·이유가 확장 추적(expansion trace)에 기록돼요.

모든 선택은 트리를 한 번 통과하는 동안 이뤄져요. 본선(mainline) 확장은 단일 스레드지만, 의존성의 자식 노드를 가져오려면 외부 네트워크 소스(예: Maven 저장소에서 pom 요청)에서 가져와야 할 수 있어요. 성능 향상을 위해, 필요해지기 전에 메타데이터를 병렬로 미리 가져오는 스레드 풀이 사용돼요.

의존성 선택 (Dep selection)

노드가 선택 고려 중일 때 lib는 다음 조건을 만족하면 포함돼요:

  • 최상위 의존성(top dep, 최상위 의존성 버전은 항상 이김)이거나, 새 lib이거나, 알려진 lib의 더 새로운 버전일 때 (Maven은 신구 여부와 무관하게 처음 발견된 버전만 유지함)
  • 그리고 이 노드 경로의 부모 중 하나에서 배제되지 않았을 때 (아래 배제 섹션 참고)
  • 그리고 경로의 모든 부모 노드가 선택됐을 때 (아래 고아 섹션 참고)

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

배제 (Exclusions)

배제는 트리의 노드에 있는 자식 노드에 표시되며, 그 자식 의존성과 그 하위 노드에 적용돼요. 배제는 배제 맵에 기록되고, 트리에서 그 지점 아래의 lib을 포함할지 검사할 때 사용돼요.

특히 까다로운 경우는 같은 lib과 버전이 다른 배제 집합을 가지고 트리의 서로 다른 지점에 나타날 때예요. 이런 경우 그 lib에 사용되는 배제는 두 배제 집합의 **교집합(intersection)**이 돼요.

예를 들어 다음과 같은 트리가 있다고 하면:

A
  B
    C (배제 X, Y)
      X
      Y
      Z
  D
    C (배제 X)
      X
      Y
      Z

C는 오직 X만 배제하게 돼요 (#{X Y}와 #{X}의 교집합). 그 이유는 A-D-C 분기가 그 의존성을 필요로 하기 때문이에요!

또한 중요한 것은, 경로 A-B-C가 고려될 때 자식 의존성으로 오직 Z만 큐에 넣는다는 점이에요(X와 Y가 배제되므로). 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 포함, 최상위 의존성
  • 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 포함, 최상위 의존성
  • 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 옵션을 쓰면 의존성 추적이 활성화되어 현재 디렉터리에 추적 데이터 파일 trace.edn을 생성해요. 그 데이터를 검사하면 다음 키를 가진 맵을 발견할 수 있어요:

  • :log — 고려된 lib 노드, 포함 여부, 각각의 이유 기록. 고려된 노드마다 하나씩인 맵의 벡터로, 각 맵은 :lib, :coord, :coord-id, :paths, :include, :reason 키와 그 외 키들을 가질 수 있어요.
  • :vmap — 버전 맵 (형식은 변경될 수 있음)
  • :exclusions — 배제 맵 (형식은 변경될 수 있음)

트리 출력 (Tree printing)

의존성은 clj -Stree 또는 프로그램 clj -X:deps tree를 사용해 출력할 수 있어요. 후자는 더 많은 옵션을 제공해요.

트리는 추적 로그로부터 만들어지며 고려된 모든 노드를 포함해요. 포함된 노드는 . 접두사로 표시되고, 배제된 노드는 X 접두사로 표시돼요. 줄 끝에는 이유 코드가 포함돼요(일부 코드는 숨겨짐). 현재 이유 코드 집합(변경될 수 있음)은 다음과 같아요:

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

더 알아보기