Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 708 – 의존성 혼동 공격을 완화하기 위한 저장소 API 확장

Author:
Donald Stufft <donald at stufft.io>
PEP-Delegate:
Paul Moore <p.f.moore at gmail.com>
Discussions-To:
Discourse thread
Status:
Rejected
Type:
Standards Track
Topic:
Packaging
Created:
20-Feb-2023
Post-History:
01-Feb-2023, 23-Feb-2023, 02-Apr-2026
Resolution:
Discourse message

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

거부

잠정 수락 상태로 3년이 지났지만 수락에 필요한 조건이 충족되지 않았으므로, 이 PEP는 거부되었습니다.

잠정 수락

이 PEP는 잠정 수락되었으며, PEP가 최종(Final) 상태가 되려면 다음 조건을 충족해야 합니다:

  1. 프로젝트 소유자가 추적 데이터를 설정할 수 있도록 필요한 UI 요소를 모두 포함한, PyPI(Warehouse)에서의 PEP 구현.
  2. PyPI 외의 저장소 최소 한 곳에서의 PEP 구현. 색인이 최소 두 개 없으면 색인 병합을 제대로 테스트할 수 없기 때문입니다.
  3. 의도한 의미론을 지원하고, 기대한 보안상 이점이 실제로 달성됨을 입증하는 데 사용할 수 있는 pip에서의 PEP 구현. 이 구현은 처음에는 “기본적으로 꺼져” 있어야 하며, 따라서 사용자가 직접 선택해야 테스트할 수 있습니다. 이상적으로는 “무소식이 희소식”이라고 가정하기보다, 새 기능을 성공적으로 사용해 본 사용자(프로젝트 소유자와 프로젝트 사용자 모두)로부터 명시적인 긍정적 보고를 수집해야 합니다.

초록

사용자가 기대한 패키지 대신 악성 패키지가 설치되는 의존성 혼동 공격은 점점 더 흔해지는 공급망 위협입니다. 최근 PyTorch 사건을 포함하여 Python 의존성을 노리는 이러한 공격 대부분은 여러 패키지 저장소를 사용할 때 발생합니다. 이때 한 저장소(예: 사용자 정의 색인)에서 올 것으로 기대한 의존성이 다른 저장소(예: PyPI)에서 설치됩니다.

이 문제를 해결하기 위해, 이 PEP는 Simple Repository API를 확장하여 저장소 연산자가 자신의 저장소에 있는 프로젝트가 다른 저장소의 프로젝트를 “추적”한다는 것을 나타낼 수 있도록 하고, 프로젝트가 자신의 네임스페이스를 여러 저장소에 걸쳐 확장할 수 있도록 할 것을 제안합니다.

이러한 기능을 통해 설치 프로그램은 특정 저장소 조합에서 제공되는 프로젝트가 예상된 것이어서 허용해야 하는 경우와, 그렇지 않아서 사용자를 보호하기 위해 오류와 함께 설치를 중단해야 하는 경우를 판단할 수 있습니다.

동기

“의존성 혼동” 공격이라고 불리는 오래된 유형의 공격이 있습니다. 대략 말하면 개별 사용자가 패키지 A를 받을 것으로 기대했지만 실제로는 B를 받게 되는 공격입니다. Python에서는 거의 항상 여러 저장소(기본값인 PyPI를 포함할 수도 있음)를 구성했기 때문에 이런 일이 발생합니다. 사용자는 패키지 A가 저장소 X에서 올 것으로 기대했지만, 누군가가 같은 이름으로 패키지 B를 저장소 Y에 게시할 수 있는 경우입니다.

의존성 혼동 공격은 오래전부터 가능했지만, 최근 이러한 공격이 실제로 성공한 공개 사례가 알려지면서 주목받게 되었습니다.

구체적인 예로 최근 PyTorch 프로젝트 사례가 있습니다. PyTorch 프로젝트에는 https://download.pytorch.org/에 있는 자체 저장소에서만 설치하도록 의도된 torchtriton이라는 내부 패키지가 있었습니다. 그런데 이 저장소는 PyPI와 함께 사용하도록 설계되어 있었고, PyPI에서 torchtriton이라는 이름이 선점되어 있지 않았기 때문에 공격자가 그 이름으로 악성 버전을 게시할 수 있었습니다.

현재 이러한 공격을 완화하는 방법은 여러 가지가 있지만, 모두 기본적으로 보호해 주는 것이 아니라 최종 사용자가 스스로를 보호하기 위해 일부러 수고를 들여야 합니다. 이는 대다수 사용자가 결국 이러한 유형의 공격을 알고 있더라도 계속 취약한 상태로 남을 가능성이 높다는 뜻입니다.

결국 이러한 공격의 근본 원인은 모든 Python 패키지 이름이 속하는 전역적으로 유일한 네임스페이스가 없다는 데 있습니다. 그 대신 각 저장소는 저마다 별개의 네임스페이스이며, 설치 프로그램은 spam과 같은 “추상적인” 이름을 설치하라는 요청을 받으면 이를 pypi.org:spam이나 example.com:spam과 같은 “구체적인” 이름으로 암묵적으로 바꾸어야 합니다. 현재 Python 설치 도구의 표준 동작은 이러한 여러 네임스페이스를 모든 네임스페이스의 파일을 포함하는 하나의 네임스페이스로 암묵적으로 평탄화하는 것입니다.

네임스페이스를 합치는 것이 기대된 동작이라는 이 가정 때문에, 서로 다른 저장소에 있는 같은 이름의 패키지를 서로 다른 주체가 작성한 경우(torchtriton 사례처럼) 의존성 혼동 공격이 가능해집니다.

이 문제가 특히 까다로운 이유는 “정답”이 없기 때문입니다. 두 저장소를 하나의 네임스페이스로 병합하려는 경우와 두 저장소를 별개의 네임스페이스로 취급하려는 경우 모두에 타당한 사용 사례가 있습니다. 이는 설치 프로그램에 항상 병합하거나 절대 병합하지 않는 일률적인 규칙이 아니라, 여러 저장소의 네임스페이스를 언제 병합해야 하고 언제 병합하지 말아야 하는지 판단할 수 있는 어떤 메커니즘이 필요하다는 것을 의미합니다.

결국 어떤 저장소에서 무엇이 설치되는지에 대한 기대가 실제로 중요한 사람은 최종 사용자이므로, 이 기능을 최종 사용자에게 직접 떠넘길 수도 있습니다. 그러나 저장소 명세를 확장하여 저장소가 안전한 경우를 나타낼 수 있게 하면, 설치 프로그램이 기본적으로 안전하게 동작하는 능력을 유지하면서도, 개별 프로젝트와 저장소의 프로젝트가 본래 여러 개의 별개 네임스페이스에 걸쳐 있는 경우에도 “기본적으로 동작”하게 할 수 있습니다.

이 PEP 자체만으로는 의존성 혼동 공격을 해결하지 못하지만, 설치 프로그램이 다른 면에서는 유효하고 안전한 사용 사례에 지나친 부수적 피해를 주지 않으면서 이러한 공격을 막을 수 있도록 충분한 정보를 제공합니다.

근거

이 PEP가 가능하게 하려는, 여러 저장소에 걸쳐 이름을 병합하는 사용 사례는 크게 두 가지입니다.

첫 번째 사용 사례는 한 저장소가 자체 이름을 정의하지 않고 다른 저장소에 정의된 이름을 확장하는 경우입니다. 이는 흔히 프로젝트를 한 저장소에서 다른 저장소로 미러링하는 경우(Bandersnatch 참조)나 저장소가 특정 플랫폼을 위한 보충 아티팩트를 제공하는 경우(Piwheels 참조)에 발생합니다.

이 경우 확장되는 저장소나 프로젝트 모두 자신이 확장되고 있다는 사실이나 누가 확장하는지를 전혀 알지 못할 수 있으므로, 이 방식은 “확장하는” 저장소 자체에 존재하지 않는 어떠한 정보에도 의존할 수 없습니다.

두 번째 사용 사례는 프로젝트가 하나의 “주” 저장소에 게시하되, 추가 플랫폼, GPU, CPU 등을 위한 바이너리를 제공하는 추가 저장소를 두고자 하는 경우입니다. 현재 휠 태그는 이러한 유형의 바이너리 호환성을 충분히 표현하지 못하므로, 이에 의존하려는 프로젝트는 여러 저장소를 구성하고 사용자가 자신의 플랫폼, GPU, CPU 등에 맞는 올바른 바이너리를 받을 수 있도록 이를 직접 설정하게 할 수밖에 없습니다.

이 사용 사례는 첫 번째와 비슷하지만, 이를 별개의 사용 사례로 구분하는 중요한 차이점은 누가 정보를 제공하며 그 신뢰 수준이 어떠한가입니다.

사용자가 특정 저장소를 설정하는 경우(또는 기본값에 의존하는 경우)에는 사용자가 어떤 저장소를 의미하는지에 대해 모호함이 없습니다. 저장소는 URL로 식별되며, 도메인 체계를 통해 URL은 전역적으로 고유한 식별자가 됩니다. 이처럼 모호함이 없다는 것은 설치 프로그램이 저장소 연산자를 신뢰할 수 있다고 가정할 수 있으며, 그들이 제공하는 메타데이터를 검증할 필요 없이 신뢰할 수 있다는 것을 의미합니다.

반대로, 설치 프로그램이 여러 저장소에서 어떤 이름을 발견했다면 설치 프로그램이 그중 어느 것을 신뢰해야 하는지 모호합니다. 이러한 모호함은 설치 프로그램이 어느 저장소의 프로젝트 소유자도 신뢰할 수 있다고 가정할 수 없으며, 그들이 실제로 같은 프로젝트이고 어느 한쪽이 의존성 혼동 공격이 아니라는 것을 검증해야 한다는 것을 의미합니다.

설치 프로그램이 여러 저장소 사이의 메타데이터를 검증할 방법이 없다면, 프로젝트는 이 사용 사례를 안전하게 지원하기 위해 직접 저장소 연산자가 될 수밖에 없을 것입니다. 그것이 특별히 잘못된 선택은 아닐 것입니다. 그러나 저장소가 프로젝트 소유자에게 이 관계를 안전하게 표현할 방법을 제공하지 않으면, 저장소는 그 대신 프로젝트 소유자가 저장소 연산자의 메타데이터를 사용하도록 허용하려는 유인을 갖게 될 위험이 있으며, 이는 원래의 보안 취약성을 다시 불러올 것입니다.

명세

이 명세는 단순 저장소 API 버전 1.2의 변경 사항을 정의하며, 저장소 “Tracks”와 “Alternate Locations”라는 두 가지 새로운 메타데이터 항목을 추가합니다.

저장소 “Tracks” 메타데이터

한 저장소가 다른 저장소에 호스팅된 프로젝트를 “확장”하려는 프로젝트를 호스팅할 수 있도록, 이 PEP는 확장하는 저장소가 확장 대상 프로젝트와 저장소의 URL을 추가함으로써 특정 프로젝트가 다른 저장소(들)의 프로젝트를 “추적(tracks)”한다고 선언할 수 있게 합니다.

이는 프로젝트별 URL($root/$project/)에서 JSON에서는 meta.tracks 키로, HTML에서는 pypi:tracks라는 이름의 meta 요소로 노출됩니다.

이 메타데이터를 사용할 때 반드시 유지되어야(MUST) 하는 몇 가지 핵심 속성이 있습니다:

  • 그것은 해당 저장소를 사용하는 개별 게시자가 아니라 저장소 연산자 자신의 통제하에 있어야 합니다(MUST).
    • “저장소 연산자”에는 특정 저장소의 전체 네임스페이스를 관리하는 사람도 포함될 수 있습니다. 이는 한 주체가 소프트웨어를 운영하지만 다른 주체가 해당 저장소의 전체 네임스페이스를 소유하거나 관리하는 호스팅 저장소 서비스와 같은 상황에서 해당될 수 있습니다.
  • 모든 URL은 확장하는 저장소의 프로젝트와 같은 “프로젝트”를 나타내야(MUST) 합니다.
    • 이것이 반드시 같은 파일을 제공해야 한다는 뜻은 아닙니다. 서로 다른 플랫폼에서 빌드된 바이너리나 로컬 패치가 적용된 사본 등을 포함하는 것도 유효합니다. 정확히 무엇이 “같은” 프로젝트를 구성하는지는 결국 사용자가 저장소와 그 연산자에게 갖는 기대에 달려 있으므로, 이 부분은 의도적으로 모호하게 남겨 두었습니다.
  • 이 메타데이터는 해당 네임스페이스를 똑같이 추적하는 다른 저장소가 아니라, 네임스페이스를 “소유”한 저장소를 가리켜야(MUST) 합니다.
  • 이 메타데이터는 (정규화 후) 정확히 같은 이름을 가진 프로젝트를 가리켜야(MUST) 합니다.
  • 이 메타데이터는 확장 대상 저장소의 기본 URL이 아니라 해당 프로젝트의 실제 URL을 가리켜야(MUST) 합니다.

저장소의 모든 이름이 같은 저장소를 추적하거나, 모든 이름이 어떤 저장소든 추적해야 할 필요는 없습니다(NOT). 일부 이름은 저장소를 추적하고 일부 이름은 추적하지 않는 혼합 용도 저장소도 명시적으로 허용됩니다.

JSON

{
  "meta": {
    "api-version": "1.2",
    "tracks": ["https://pypi.org/simple/holygrail/", "https://test.pypi.org/simple/holygrail/"]
  },
  "name": "holygrail",
  "files": [
    {
      "filename": "holygrail-1.0.tar.gz",
      "url": "https://example.com/files/holygrail-1.0.tar.gz",
      "hashes": {"sha256": "...", "blake2b": "..."},
      "requires-python": ">=3.7",
      "yanked": "Had a vulnerability"
    },
    {
      "filename": "holygrail-1.0-py3-none-any.whl",
      "url": "https://example.com/files/holygrail-1.0-py3-none-any.whl",
      "hashes": {"sha256": "...", "blake2b": "..."},
      "requires-python": ">=3.7",
      "dist-info-metadata": true
    }
  ]
}

HTML

<!DOCTYPE html>
<html>
  <head>
    <meta name="pypi:repository-version" content="1.2">
    <meta name="pypi:tracks" content="https://pypi.org/simple/holygrail/">
    <meta name="pypi:tracks" content="https://test.pypi.org/simple/holygrail/">
  </head>
  <body>
    <a href="https://example.com/files/holygrail-1.0.tar.gz#sha256=...">
    <a href="https://example.com/files/holygrail-1.0-py3-none-any.whl#sha256=...">
  </body>
</html>

“Alternate Locations” 메타데이터

이 PEP는 프로젝트가 여러 저장소에 걸쳐 자신의 네임스페이스를 확장할 수 있도록, 프로젝트 소유자가 해당 프로젝트의 “대체 위치” 목록을 선언할 수 있게 합니다. 이 목록은 JSON에서는 alternate-locations 키로, HTML에서는 pypi-alternate-locations라는 이름의 meta 요소로 노출되며, 이 meta 요소는 여러 번 사용할 수 있습니다.

이 메타데이터를 사용할 때 반드시 지켜야 하는 몇 가지 핵심 속성이 있습니다:

  • 이 메타데이터를 신뢰하려면, 해당 프로젝트가 발견되는 모든 위치 사이에서 대체 위치가 무엇인지에 대해 반드시 합의가 이루어져 있어야 합니다.
  • 대체 위치를 사용할 때 클라이언트는 응답을 가져온 url이 목록에 포함되어 있다고 반드시 암묵적으로 가정해야 합니다. 즉, https://pypi.org/simple/foo/에서 가져온 응답에 ["https://example.com/simple/foo/"] 값을 가진 alternate-locations 메타데이터가 있다면, 그 값이 ["https://example.com/simple/foo/", "https://pypi.org/simple/foo/"]인 것처럼 반드시 취급해야 합니다.
  • 배열 내 요소의 순서에는 특별한 의미가 없습니다.

설치 프로그램이 대체 위치 메타데이터를 사용하는 프로젝트를 만나면, 명시된 모든 저장소가 같은 네임스페이스를 여러 저장소에 걸쳐 확장하고 있다고 간주해야 합니다(SHOULD).

Note

이 대체 위치 메타데이터는 아티팩트 수준의 메타데이터가 아니라 프로젝트 수준의 메타데이터입니다. 즉, 코어 메타데이터 명세의 일부로 포함되지 않으며, 그 대신 각 저장소가 (이를 지원하기로 선택한다면) 설정 옵션을 제공해야 하는 것입니다.

JSON

{
  "meta": {
    "api-version": "1.2"
  },
  "name": "holygrail",
  "alternate-locations": ["https://pypi.org/simple/holygrail/", "https://test.pypi.org/simple/holygrail/"],
  "files": [
    {
      "filename": "holygrail-1.0.tar.gz",
      "url": "https://example.com/files/holygrail-1.0.tar.gz",
      "hashes": {"sha256": "...", "blake2b": "..."},
      "requires-python": ">=3.7",
      "yanked": "Had a vulnerability"
    },
    {
      "filename": "holygrail-1.0-py3-none-any.whl",
      "url": "https://example.com/files/holygrail-1.0-py3-none-any.whl",
      "hashes": {"sha256": "...", "blake2b": "..."},
      "requires-python": ">=3.7",
      "dist-info-metadata": true
    }
  ]
}

HTML

<!DOCTYPE html>
<html>
  <head>
    <meta name="pypi:repository-version" content="1.2">
    <meta name="pypi:alternate-locations" content="https://pypi.org/simple/holygrail/">
    <meta name="pypi:alternate-locations" content="https://test.pypi.org/simple/holygrail/">
  </head>
  <body>
    <a href="https://example.com/files/holygrail-1.0.tar.gz#sha256=...">
    <a href="https://example.com/files/holygrail-1.0-py3-none-any.whl#sha256=...">
  </body>
</html>

권장 사항

이 절은 규범적이지 않습니다. 이 절은 기본적으로 사용자를 보호하는 것과 기존 워크플로의 손상을 최소화하는 것 사이에서 이 PEP가 가장 좋은 절충안이라고 생각하는, 이 메타데이터의 해석 방법에 관한 권고를 설치 프로그램에 제공합니다. 이러한 권고는 구속력이 없으며, 설치 프로그램은 이를 자유롭게 무시하거나 각자의 특정 상황에 맞게 선택적으로 적용할 수 있습니다.

파일 탐색 알고리즘

Note

이 알고리즘은 현재 pip이 파일을 찾는 방식을 바탕으로 작성되었습니다. 다른 설치 프로그램은 각자의 탐색 절차에 맞게 이를 조정할 수 있습니다.

현재 “표준” 파일 탐색 알고리즘은 대략 다음과 같습니다:

  1. 설정된 모든 저장소에 걸쳐 모든 파일의 목록을 생성합니다.
  2. 잠금 파일이나 요구 사항 파일의 알려진 해시와 일치하지 않는 파일을 모두 걸러냅니다.
  3. 현재 플랫폼, Python 버전 등과 일치하지 않는 파일을 모두 걸러냅니다.
  4. 그 파일 목록을 리졸버에 전달합니다. 리졸버는 파일이 어느 저장소에서 왔는지와 관계없이 그 파일들 중에서 “최선의” 일치 항목을 결정하려고 시도합니다.

설치 프로그램은 새로운 메타데이터를 고려하도록 파일 탐색 알고리즘을 변경하여, 그 대신 다음과 같이 할 것을 권장합니다.

  1. 설정된 모든 저장소에 걸쳐 모든 파일의 목록을 생성합니다.
  2. 잠금 파일이나 요구 사항 파일의 알려진 해시와 일치하지 않는 파일을 모두 걸러냅니다.
  3. 최종 사용자가 설치 프로그램에 특정 저장소에서 프로젝트를 가져오라고 명시적으로 지시했다면, 다른 모든 저장소를 걸러내고 5로 건너뛰십시오.
  4. 탐색된 파일이 여러 저장소에 걸쳐 있는지 확인합니다. 그렇다면 “Tracks” 또는 “Alternate Locations” 메타데이터가 파일이 탐색된 모든 저장소를 함께 안전하게 병합하는 것을 허용하는지 판단합니다. 해당 메타데이터가 이를 허용하지 않는다면 오류를 발생시키고, 그렇지 않으면 계속 진행합니다.
    • 참고: 이는 원격 저장소에만 적용됩니다. 로컬 파일 시스템에 존재하는 저장소는 어떤 원격 저장소와도 병합되는 것이 항상 암묵적으로 허용되어야 합니다.
  5. 현재 플랫폼, Python 버전 등과 일치하지 않는 파일을 모두 걸러냅니다.
  6. 그 파일 목록을 리졸버에 전달합니다. 리졸버는 파일이 어느 저장소에서 왔는지와 관계없이 그 파일들 중에서 “최선의” 일치 항목을 결정하려고 시도합니다.

다소 미묘하지만, 이 권장 사항의 핵심은 다음과 같습니다:

  • 특정 아티팩트의 “유효한” 해시를 포함하는 잠금 파일이나 요구 사항 파일을 사용하는 사용자는 그 해시의 특성상 보호받는 것으로 간주합니다. 나머지 권장 사항은 해시를 생성할 때 적용되기 때문입니다. 따라서 알 수 없는 해시는 처음부터 걸러냅니다.
  • 사용자가 설치 프로그램에 특정 저장소 집합에서 프로젝트를 가져오고 싶다고 명시적으로 지시했다면, 이에 의문을 제기할 이유가 없으며 사용자가 해당 네임스페이스를 병합해도 안전하다는 것을 확인했다고 가정합니다.
  • 해당 프로젝트가 단일 저장소에서만 제공된다면 의존성 혼동이 일어날 가능성이 없으므로, 허용하는 것 외에 다른 조치를 할 이유가 없습니다.
  • 플랫폼, Python 버전 등을 기준으로 걸러내기 전에 이 PEP에서 정의한 메타데이터를 먼저 확인합니다. 특정 플랫폼이나 Python 버전 등에서만 나타나는 오류는 바람직하지 않기 때문입니다.
  • 네임스페이스를 병합해도 안전하다고 알려 주는 것이 전혀 없다면, 안전하다고 암묵적으로 가정하지 않고 대신 오류를 발생시킵니다.
  • 그렇지 않으면 네임스페이스를 병합하고 계속 진행합니다.

이 알고리즘은 설치 프로그램이 서로 다른 두 네임스페이스를 하나로 평탄화할 수 있다고 절대 가정하지 않도록 보장하므로, 사실상 어떤 종류의 의존성 혼동 공격도 일어날 가능성을 없애면서도, 서로 다른 네임스페이스가 실제로는 안전하게 병합할 수 있는 하나의 논리적 네임스페이스인 경우 사람들이 이를 명시적으로 선언할 수 있도록 스택 전반에 걸쳐 안전한 방식으로 권한을 부여합니다.

위 알고리즘은 대체로 개념적인 모델입니다. 실제로는 개인 정보를 더 잘 보호하고 더 빠르게 동작하도록, 또는 단순히 특정 설치 프로그램에 더 잘 맞도록 조정되어 알고리즘이 약간 달라질 수도 있습니다.

최종 사용자를 위한 명시적 구성

이 PEP는 최종 사용자가 특정 패키지를 정확히 어떤 저장소에서 설치할지 설정할 수 있도록 설치 프로그램이 제공하는 구체적인 메커니즘을 규정하거나 권장하지 않습니다. 그러나 설치 프로그램이 최종 사용자가 그러한 설정을 할 수 있는 어떤 메커니즘을 제공할 것을 권장합니다. 그러한 메커니즘이 없으면 torchtriton의 경우처럼, 사용자가 네임스페이스 충돌을 외부에서 해결하지 않는 한(한 저장소에서 해당 이름을 내리게 하거나, 병합을 처리하는 개인 저장소를 세우는 등) 완전히 동작하지 않는 DoS 상황에 놓일 수 있기 때문입니다.

또한 이 구성 기능이 있으면 기본 동작이 안전해질 때까지 이어질 가능성이 높은 긴 전환 기간 동안 최종 사용자가 미리 스스로를 보호할 수 있습니다.

이 변경 사항을 알리는 방법

Note

이 예시는 pip에 한정된 것이며 pip이 이 PEP를 구현하기로 선택할 방식의 세부 사항을 가정합니다. 이 예시는 이 변경 사항을 어떻게 알릴 수 있는지 보여 주기 위해 포함한 것이며, pip이나 다른 설치 프로그램이 이를 구현하는 방식을 제약하려는 의도는 없습니다. 결국 이 내용이 실제 공지의 바탕이 될 수도 있으며, 그렇게 된다면 정확성과 명확성을 위해 수정해야 합니다.

이 절은 블로그 글, 이메일, Discourse 게시물 등으로 활용할 수 있는, 이 변경 사항을 알리는 하나의 완결된 “게시물”처럼 읽으십시오.

“의존성 혼동(dependency confusion)” 공격이라고 불리는 오래된 유형의 공격이 있습니다. 대략적으로 말하면, 어떤 사람이 패키지 A를 받을 것으로 기대했지만 실제로는 B를 받게 되는 공격입니다. Python에서 이 공격은 거의 항상 최종 사용자가 여러 저장소를 구성했기 때문에 발생합니다. 사용자는 패키지 A가 저장소 X에서 올 것으로 기대하지만, 누군가가 저장소 Y에 패키지 A와 같은 이름으로 패키지 B를 게시할 수 있는 경우입니다.

현재도 이러한 공격을 완화하는 방법은 여러 가지가 있습니다. 그러나 모두 본질적으로 안전한 방식이 아니라, 최종 사용자가 스스로를 보호하기 위해 일부러 명시적인 조치를 해야 하는 방식입니다.

pip 사용자의 보안을 강화하고 이러한 유형의 공격으로부터 보호하기 위해, pip가 설치할 패키지를 찾는 방식을 변경할 예정입니다.

무엇이 바뀝니까?

같은 프로젝트를 여러 원격 저장소에서 받을 수 있다는 것을 발견하면, pip는 어느 저장소에서 설치하는 것이 올바른지 추측하지 않고 기본적으로 오류를 발생시키며 진행을 거부합니다.

원래부터 여러 저장소에 게시하는 프로젝트는 자신의 저장소들을 안전하게 “연결”할 수 있게 되며, 이렇게 하면 해당 저장소들을 함께 사용해도 pip가 오류를 내지 않습니다.

pip의 최종 사용자는 특정 프로젝트에 유효한 저장소를 하나 이상 명시적으로 지정할 수 있게 됩니다. 이렇게 지정하면 pip는 해당 프로젝트에 대해 지정된 저장소만 고려하므로 오류가 전혀 발생하지 않습니다.

자세한 내용은 TBD를 참고하십시오.

누가 영향을 받습니까?

여러 원격 저장소(예: 로컬 파일 시스템에 없는 저장소)에서 설치하는 사용자는 다음과 같은 경우 설치에 성공하지 못하고 pip 오류가 발생하는 영향을 받을 수 있습니다:

  • 같은 “이름”을 여러 원격 저장소에서 제공하는 프로젝트를 설치합니다.
  • 여러 원격 저장소에서 제공되는 해당 프로젝트 이름이 저장소들을 서로 연결하기 위해 정의된 메커니즘 중 어느 것도 사용하지 않았습니다.
  • pip를 실행하는 사용자가 특정 프로젝트에 유효한 저장소를 명시적으로 제어하기 위해 정의된 메커니즘을 사용하지 않았습니다.

여러 원격 저장소를 사용하지 않는 사용자는 전혀 영향을 받지 않습니다. 여기에는 단일 원격 저장소와 로컬 파일 시스템 “wheel house”만 사용하는 사용자도 포함됩니다.

무엇을 해야 합니까?

pip 사용자라면?

단일 원격 저장소만 사용하고 있다면 아무것도 하지 않아도 됩니다.

여러 원격 저장소를 사용하고 있다면, pip 호출에 --use-feature=TBD를 추가하여 새 동작을 선택적으로 활성화하고, 의존성 중 여러 원격 저장소에서 제공되는 것이 있는지 확인할 수 있습니다. 그런 의존성이 있다면, 그 이유와 자신에게 가장 적합한 해결 방법이 무엇인지 파악하기 위해 해당 의존성을 점검해야 합니다.

이 동작이 기본값이 되고 나면, pip 호출에 --use-deprecated=TBD를 추가하여 일시적으로 이 동작을 사용하지 않을 수 있습니다.

공개 저장소에 호스팅되지 않는 프로젝트를 사용하면서도 공개 저장소를 대체 수단으로 두고 있다면, 누군가 공개 저장소에 그 이름을 등록하여 pip에서 오류가 발생하는 일을 막을 수 있도록, 저장소 파일로 pip를 구성하여 해당 의존성을 어디에서 가져와야 하는지 명시하는 것을 고려하십시오.

프로젝트 소유자라면?

프로젝트를 단일 저장소에만 게시한다면 아무것도 하지 않아도 됩니다.

동시에 함께 사용하도록 의도된 여러 저장소에 프로젝트를 게시한다면, 최종 사용자에게 문제가 생기지 않도록 모든 저장소가 대체 저장소 메타데이터를 제공하도록 구성하십시오.

프로젝트를 단일 저장소에 게시하지만 그 프로젝트가 흔히 다른 저장소와 함께 사용된다면, 서드파티가 사용자의 pip install 호출을 실패하게 만들 수 없도록 해당 저장소들에 미리 이름을 등록하는 것을 고려하십시오. 프로젝트 이름이 너무 일반적이거나 저장소에 방어적 이름 선점을 금지하는 정책이 있다면 이 방법은 사용할 수 없을 수도 있습니다.

저장소 연산자라면?

최종 사용자가 저장소를 어떻게 사용하도록 의도하는지, 그리고 그들이 저장소를 어떻게 사용하기를 원하는지 결정해야 합니다.

비공개 프로젝트를 호스팅하는 비공개 저장소의 경우, 사용자가 의존하는 공개 프로젝트를 자체 저장소로 미러링하되 공개 프로젝트가 비공개 프로젝트와 병합되지 않도록 주의하고, 사용자에게 --index-url 옵션을 사용하여 해당 저장소만 사용하도록 안내할 것을 권장합니다.

공개 프로젝트를 호스팅하는 공개 저장소의 경우, 대체 저장소 메커니즘을 구현하고, 프로젝트 소유자가 둘 이상의 저장소에서 프로젝트를 제공할 때 그 프로젝트를 제공하는 저장소 목록을 구성할 수 있도록 해야 합니다.

다른 저장소를 “추적”하면서 특정 플랫폼용으로 빌드된 wheel 같은 보충 아티팩트를 제공하는 공개 저장소의 경우, 저장소에 “tracks” 메타데이터를 구현해야 합니다. 하지만 이 정보는 저장소에 프로젝트를 게시하는 최종 사용자가 설정할 수 있어서는 안 됩니다. 자세한 내용은 TBD를 참고하십시오.

기각된 아이디어

참고: 이 중 일부는 다소 pip에 특화된 내용이지만, pip에서 동작하지 않는 해결책은 그다지 유용한 해결책이 아닙니다.

파일 목록이 같으면 미러를 암묵적으로 허용하기

모든 저장소가 정확히 같은 파일 목록을 반환한다면, 그 저장소들을 같은 네임스페이스로 간주하여 암묵적으로 병합해도 안전합니다. 이는 어쩌면 사용자나 저장소 연산자 측의 아무런 작업 없이도 미러가 자동으로 허용된다는 것을 의미할 수 있습니다.

안타깝게도 이 방식에는 이를 바람직하지 않게 만드는 두 가지 결함이 있습니다:

  • 이 방식은 서로의 정확한 복사본인 미러의 경우만 해결할 뿐, 다른 저장소를 “추적”하는 저장소의 경우는 해결하지 못하며, 결국 후자가 더 일반적인 해결책이 됩니다.
  • 정확한 미러의 경우에도, 서로를 미러링하는 여러 저장소는 분산 시스템이므로 항상 서로 완전히 일관된 상태를 유지하지는 못하며, 사실상 최종적 일관성 시스템입니다. 즉, 이 암묵적 휴리스틱에 의존하여 동작하는 저장소는 원본 저장소와 미러 저장소 사이의 불일치로 인해 간헐적으로 실패하게 됩니다.

저장소의 순서를 정하는 메커니즘 제공하기

저장소에 순서를 부여하는 메커니즘을 제공하고, 해당 프로젝트의 파일을 제공하는 첫 번째 저장소를 찾으면 탐색 알고리즘을 즉시 중단하는 방식도 실행 가능한 또 다른 해결책이며, 순서가 올바르게 지정된다면 안전합니다.

하지만 이 방식은 여러 가지 이유로 기각되었습니다:

  • 저희는 15년 넘게 저장소를 지정하는 순서에는 의미가 없으며 사실상 순서가 정의되어 있지 않다고 사용자에게 알려 왔습니다. 이제 와서 그 입장을 번복하고 순서가 중요하다고 말하기 시작하기는 어려울 것입니다.
  • 사용자는 한 곳 안에서는 저장소를 지정하는 순서를 쉽게 바꿀 수 있지만, 여러 곳(환경 변수, 설정 파일, requirements 파일, CLI 인자)에서 저장소를 불러올 때는 그 순서가 pip에 하드코딩되어 있습니다. 그 순서가 결정적이고 문서화된 순서이기는 하겠지만, 그것이 사용자가 원하는 저장소 정의 순서라고 가정할 이유는 없습니다. 결국 사용자는 암묵적인 순서가 올바른 순서가 되도록 pip 설정 방식을 억지로 비틀어야 합니다.
  • 저장소가 정의된 순서를 암묵적으로 사용하는 대신 순서를 명시적으로 선언하는 방법을 제공하면 위 문제를 완화할 수 있습니다. 그러나 그렇게 하면 사용자가 명시적으로 설정하지 않는 한 보호가 제공되지 않습니다.
  • 순서 방식은 프로젝트별로 결정할 방법 없이 한 저장소가 다른 저장소보다 항상 우선한다고 가정합니다.
  • 순서에 의존하는 방식은 미묘합니다. 저장소의 순서를 보더라도 어떤 이름이 어떤 저장소에서 오게 될지 미리 알거나 보장할 방법이 없습니다. 어떤 이름을 어떤 저장소가 제공하는지는 그 시점에만 알 수 있습니다.
  • 순서에 의존하는 방식은 취약합니다. 서로 무관한 두 저장소 사이에 우연한 이름 충돌이 일어나지 않으리라고 가정할 이유는 없습니다. 우선순위가 낮은 저장소의 라이브러리를 사용하고 있는데 우선순위가 높은 저장소에 우연히 같은 이름이 생기기 시작하면 어떻게 되겠습니까?
  • 순서 방식이 잘못 동작하는 경우, 사용자에게 아무런 피드백도 주지 않고 조용히 잘못 동작합니다. 이는 의도된 설계입니다. 순서 방식은 무엇이 옳고 그른지 실제로 알지 못하고 단지 순서가 올바른 결과를 내기를 기대할 뿐이며, 그렇게 된다면 사용자는 아무것도 망가지지 않은 채 보호받기 때문입니다. 그러나 잘못 동작하는 경우 사용자는 pip가 조용히 엉뚱한 것을 설치하는, 매우 혼란스러운 동작을 겪게 됩니다.

이 아이디어의 변형으로, 실제 문제를 일으키는 것은 결국 누구나 이름을 등록할 수 있는 PyPI의 특성이므로 “기본” 저장소를 제외한 모든 저장소를 같은 우선순위로 취급하고 기본 저장소를 더 낮은 우선순위로 취급하면 문제가 해결된다는 주장이 있습니다.

이 방식이 상황을 개선한다는 점에서는 맞는 말이지만, 일반적인 순서 방식이 가진 문제를 (전부는 아니더라도) 상당수 그대로 가지고 있습니다.

또한 이 방식은 PyPI, 또는 “기본”으로 설정된 저장소가 누구나 이름을 등록할 수 있는 유일한 저장소라고 가정합니다. 그러나 사용자가 PyPI와 함께 사용하도록 되어 있는 Piwheels 같은 프로젝트가 존재하며, 이러한 프로젝트는 PyPI에 등록된 이름을 그대로 따라가므로 사실상 누구나 이름을 등록할 수 있는 것과 같습니다.

저장소 프록시에 의존하기

한 가지 가능한 해결책은 설치 프로그램이 이를 해결하게 하는 대신, 여러 저장소를 지능적으로 안전하게 병합할 수 있는 저장소 프록시에 의존하는 것입니다. 이 방식은 해당 문제 영역에 특화된 설정과 기능을 갖출 수 있으므로, 복잡한 요구 사항을 가진 사람들에게 더 나은 경험을 제공할 수 있습니다.

그러나 이 방안은 다음과 같은 이유로 기각되었습니다.

  • 이 방식은 사용자가 저장소 프록시 사용을 직접 선택해야 합니다. 그렇지 않으려면 설치 프로그램에서 둘 이상의 저장소를 사용하는 기능까지 제거하여, 여러 저장소가 필요한 사용자가 저장소 프록시를 사용할 수밖에 없게 해야 합니다.
    • 둘 이상의 저장소를 설정하는 기능을 제거하는 방안은 최종 사용자에게 지나치게 큰 혼란을 줄 것이므로 기각되었습니다.
  • 사용자는 상황에 따라 여러 저장소를 병합한 결과가 서로 달라야 할 수도 있고, 서로 배타적인 여러 저장소를 병합해야 할 수도 있습니다. 이는 사용자가 고유한 옵션 조합마다 저장소 프록시를 실제로 여러 개 구성해야 한다는 뜻입니다.
  • 이 방식은 사용자가 인프라를 유지 관리해야 하거나, 호출할 때마다 저장소를 자동으로 띄우는 기능을 설치 프로그램에 추가해야 합니다.
  • 이 방식은 이러한 문제에 대한 해결책이 필요하다는 요구 사항 자체를 실제로 바꾸지 않으며, 구현 책임을 설치 프로그램에서 어떤 저장소 프록시로 옮길 뿐입니다. 어느 경우든 서로 다른 이 네임스페이스들을 어떻게 병합할지 결정하는 무언가가 여전히 필요합니다.
  • 결국 대부분의 사용자는 여러 저장소를 안전하게 사용하려고 저장소 프록시까지 직접 구축하고 싶어 하지 않습니다.

해시 검사에만 의존하기

또 다른 가능한 해결책은 해시 검사에 의존하는 것입니다. 해시 검사를 활성화하면 사용자가 예상하지 않은 아티팩트를 받을 수 없으므로, 네임스페이스가 잘못 병합되었는지 여부는 중요하지 않습니다.

이것은 분명 하나의 해결책이지만, 안타깝게도 이 방식 역시 실용적이지 않게 만드는 문제를 안고 있습니다.

  • 사용자가 직접 선택해야만 사용할 수 있으므로, 기본적으로는 여전히 사용자가 보호받지 못합니다.
  • 이 방식에서는 사용자가 해시를 관리하느라 상당한 수고를 들여야 하는데, 대부분의 사용자는 이를 감수하려 하지 않을 것입니다.
  • 사용자가 의존성의 출처로 requirements.txt 파일을 사용하지 않는 경우에는 보호를 받기가 어렵고 번거롭습니다(빌드 시점 의존성과 명령줄에서 지정하는 의존성이 여기에 해당합니다).
  • 이 방식은 문제를 어느 정도만 해결할 뿐이며, 어떤 면에서는 문제의 책임을 설치 프로그램이 사용할 해시를 생성하는 시스템이 무엇이든 그쪽으로 옮길 뿐입니다. 그 시스템이 사람이 직접 해시를 검증하는 방식이 아니라면(실제로 그럴 가능성은 낮습니다), 이러한 네임스페이스를 어떻게 병합할 것인가 하는 문제를 해시 유지 관리를 구현하는 도구 쪽으로 옮겨 놓은 것에 불과합니다.

모든 프로젝트가 “기본” 저장소에 존재하도록 요구하기

또 다른 아이디어는 --extra-index-url의 범위를 좁혀서, 기본 저장소를 보완하는 저장소를 가리키는 용도로만 지원하는 것입니다. 이는 사실상 기본 저장소가 네임스페이스를 정의하고, 모든 추가 저장소는 추가 패키지로 그 네임스페이스를 확장할 뿐이라고 선언하는 것입니다.

이를 구현한다면 대략, 추가 저장소가 동작하려면 해당 프로젝트가 기본 저장소에 반드시 등록되어 있어야 한다(MUST)고 요구하는 방식이 될 것입니다.

이런 식으로 범위를 좁히는 데 성공한다면 어느 정도는 동작하지만, 최종적으로는 다음과 같은 이유로 기각되었습니다.

  • 사용자는 이렇게 축소된 범위를 이해하거나 받아들이지 않을 가능성이 높으며, 따라서 이제는 지원되지 않는 방식으로 계속 사용하려 할 가능성이 높습니다.
    • 범위가 좁아지면 제외된 워크플로를 사용하던 사용자에게는 저장소 프록시를 구축하는 것 외에 더 이상 대안이 없다는 점 때문에 문제가 더 복잡해집니다. 저장소 프록시를 구축하려면 이전에는 필요 없던 인프라와 노력이 듭니다.
  • 이 방식은 “추가” 저장소에 있는 이름이 기본 저장소에 있는 이름과 같다는 이유만으로 둘이 같은 프로젝트라고 가정합니다. 완전히 새로운 생태계에서 처음부터 시작하는 것이라면 처음부터 이 가정을 세우고 정착시킬 수 있었을지도 모르지만, 생태계가 그러한 변화에 적응하도록 만드는 것은 엄청나게 어려울 것입니다.
    • 이것은 이 접근 방식의 근본적인 문제입니다. 의존성 혼동을 일으키는 근본 원인은 서로 다른 네임스페이스를 하나로 평탄화한다는 데 있습니다. 이 접근 방식은 본질적으로 그것이 괜찮다고 선언할 뿐이며, 모든 사람이 자신의 이름을 등록하도록 요구함으로써 문제를 완화하려 합니다.
  • 위의 가정 때문에, 추가 저장소에 있는 이름이 우연히 기본 저장소의 이름과 충돌하는 경우 해당 사용자에게는 정상적으로 동작하는 것처럼 보이겠지만, 실제로는 아무도 모르게 의존성 혼동 상태에 놓이게 됩니다.
    • 더 심각한 점은 이것이 동작하게 해 주는 이름의 소유자가 자신이 그 사용자에게 어떤 역할을 하고 있는지 전혀 알지 못한다는 것입니다. 그 소유자는 자신의 프로젝트를 삭제하거나 다른 사람에게 넘겨줄 수도 있으며, 그 결과 의도치 않게 악의적인 사용자가 그 이름을 탈취하도록 허용하게 될 수도 있습니다.
  • 사용자는 방어적인 이름 선점(name squat)으로 자신의 기본 저장소에 이름을 등록하여 동작하는 상태로 되돌리려 할 가능성이 높습니다. 이렇게 할 수 있는지는 기본 저장소의 구체적인 정책, 누군가가 이미 그 이름을 가지고 있는지, 이름이 너무 일반적인지 등에 따라 달라집니다. 가장 좋은 경우라 하더라도 내부에서 쓰는 이름을 확보하는 것 외에는 아무런 목적도 없는 불필요한 자리 표시용 프로젝트가 생겨나게 됩니다.

전역적으로 고유한 이름으로 전환하기

이 문제가 존재하는 주된 이유는 전역적으로 고유한 이름이 없기 때문입니다. 대신 여러 네임스페이스 아래에 존재하는 지역적으로 고유한 이름들이 있고, 우리는 이를 하나의 평탄한 네임스페이스로 병합하려 하고 있습니다. 대신 전역적으로 고유한 이름을 가질 방법을 찾아낼 수 있다면 문제 전체를 비껴갈 수 있을 것입니다.

이 아이디어는 다음과 같은 이유로 기각되었습니다.

  • 전역적으로 고유하면서도 안전하고, 동시에 사람에게 의미 있는 이름을 생성하는 것은 어떤 형태의 중앙 집중식 데이터베이스에 편승하지 않고서는 거의 불가능한 일입니다. 제가 아는 한, 이를 해낸 시스템들은 모두 결국 도메인 시스템에 편승하여 도메인이 포함된 URL 등으로 패키지를 가리키게 되었습니다.
  • 전역적으로 고유한 이름을 얻는 메커니즘을 고안해 내더라도, 모든 것을 완전히 허물고 처음부터 다시 시작하지 않는 한 수십 년 된 우리 시스템에 이를 개조하여 적용할 수 있는 가능성은 사실상 전무합니다. 아마 우리가 할 수 있는 최선은 전역적으로 고유하지 않은 모든 이름을 암묵적으로 PyPI 도메인 이름 아래의 이름으로 선언하고, PyPI가 아닌 패키지를 가진 모든 사람에게 패키지 이름을 바꾸도록 강제하는 것입니다.
  • 이는 현재 시스템의 핵심 가정과 근본적인 부분을 너무나 많이 뒤엎게 되어, 그것들을 나열하려 해도 어디서부터 시작해야 할지 알기 어려울 정도입니다.

설치 프로그램이 명시적 설정을 제공하도록 권장하기만 하기

제기된 아이디어 중 하나는 본질적으로 명시적 구성만 구현하고 그 밖의 다른 것은 아무것도 변경하지 않는 것입니다. 사실 명시적 구성 옵션은 매핑 정책에 대한 구체적인 제안에서 영감을 받은 것이며, 그 제안은 대략 다음과 같은 모양의 파일을 만들었습니다.

{
  "repositories": {
    "PyTorch": ["https://download.pytorch.org/whl/nightly"],
    "PyPI": ["https://pypi.org/simple"]
  },
  "mapping": [
    {
      "paths": ["torch*"],
      "repositories": ["PyTorch"],
      "terminating": true
    },
    {
      "paths": ["*"],
      "repositories": ["PyPI"]
    }
  ]
}

명시적 설정을 두라는 권장 사항은 그것을 어떻게 구현할지에 대한 결정을 각 설치 프로그램에 넘기므로, 각 설치 프로그램은 자신의 사용자에게 가장 적합한 방식을 선택할 수 있습니다.

최종적으로 어떤 형태의 명시적 구성만 구현하는 방안은 기각되었습니다. 명시적 구성은 본질적으로 옵트인 방식이므로, 기존 도구로 문제를 해결할 능력이 가장 부족한 일반 사용자를 보호하지 못하기 때문입니다. 명시적 구성과 함께 추가적인 보호 장치를 더하면 모든 사용자를 기본적으로 보호할 수 있습니다.

또한 명시적 설정에만 의존하면 PyPI, Piwheels, PyTorch 등의 미러 같은 경우에도 모든 최종 사용자가 같은 문제를 계속 반복해서 해결해야 합니다. 안전을 확보하려면 사용자는 매번 직접 결정을 내리거나, 원리도 모른 채 그대로 따라 할 예제를 찾아야 합니다. 추가 기능을 함께 도입하면 가능한 곳에서는 이러한 보호 장치를 중앙에서 관리할 수 있습니다. 동시에 숙련된 최종 사용자는 여전히 자신의 운명을 완전히 스스로 통제할 수 있습니다.

npm 방식의 스코프

npm이 구현한 방식과 비슷한 스코프가 결국 이 문제를 해결할 수 있다는 제안이 있었습니다. 하지만 스코프는 이 문제에 대해 결국 아무것도 바꾸지 못합니다. 제가 아는 한 npm의 스코프는 전역적으로 고유하지 않으며, 스코프가 없는 이름과 마찬가지로 특정 레지스트리에 묶여 있습니다. 하지만 스코프가 가능하게 하는 것은 관련 프로젝트를 묶는 명확한 메커니즘과, npm.org의 사용자나 조직이 스코프 전체를 차지할 수 있는 능력입니다. 이를 통해 네임스페이스의 작은 슬라이스 하나가 온전히 자신에게 속한다고 확신할 수 있고, 스코프 전체를 특정 비공개 레지스트리에 할당하는 규칙을 쉽게 작성할 수 있으므로 명시적 설정을 훨씬 쉽게 다룰 수 있습니다.

안타깝게도 이는 결국 명시적 설정만 사용하자는 아이디어를 조금 더 쉽게 만든 것에 불과합니다. npm에서는 사람들이 자체 레지스트리를 쓰는 경우가 그리 흔하지 않아서 이 방식이 그럭저럭 통하지만, Python에서는 바로 그렇게 하도록 권장합니다.

“명시적 설정”의 정의 및 표준화

이 PEP는 특정 프로젝트가 어느 저장소에서 오는지를 명시적으로 설정하는 메커니즘을 설치 프로그램이 갖추도록 권장하지만, 그 메커니즘이 무엇인지는 정의하지 않습니다. 이는 각 설치 프로그램의 UX와 밀접하게 연결되어 있고, 각 설치 프로그램이 자신의 사용 사례에 맞다고 판단하는 방식으로 그 설정을 노출할 수 있도록 하고자 하므로, 의도적으로 정의하지 않은 채로 둡니다.

또한 그 메커니즘을 정의하자는 아이디어가 나왔을 때, 다른 설치 프로그램 중 어느 것도 그 메커니즘이 자신들을 위해 정의되는 데 특별히 관심을 보이지 않았으며, 이는 그들이 그것을 자신들의 UX의 일부로 다루는 데 만족한다는 것을 시사합니다.

마지막으로, 그 메커니즘을 정의하기로 한다면 이 PEP의 저장소 API 변경 사항에 포함하기보다는 별도의 PEP로 다루어야 합니다. 결국 표준화의 길을 가기로 결정한다면 향후 별도의 PEP가 될 수 있습니다.

감사의 글

자신의 제안으로 이 PEP로 이어진 논의를 시작해 주신 Trishank Kuppusamy에게 감사드립니다.

이 PEP의 아이디어에 대해 초기 피드백과 논의를 제공해 주신 Paul Moore, Pradyun Gedam, Steve Dower, Trishank Kuppusamy에게 감사드립니다.

이 PEP를 교정하고 구조와 품질을 개선해 주신 Jelle Zijlstra, C.A.M. Gerlach, Hugo van Kemenade, Stefano Rivera에게도 이 PEP의 교열과 구조 및 품질 개선에 도움을 주신 데 대해 감사드립니다.