# GitHub의 모든 객체에는 두 개의 ID가 있다

> Clean Markdown view of GeekNews topic #25818. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=25818](https://news.hada.io/topic?id=25818)
- GeekNews Markdown: [https://news.hada.io/topic/25818.md](https://news.hada.io/topic/25818.md)
- Type: GN+
- Author: [neo](https://news.hada.io/@neo)
- Published: 2026-01-15T05:32:50+09:00
- Updated: 2026-01-15T05:32:50+09:00
- Original source: [greptile.com](https://www.greptile.com/blog/github-ids)
- Points: 2
- Comments: 1

## Topic Body

- GitHub API를 사용하던 중 **PR 코멘트 링크 생성 기능**에서 ID 불일치로 링크가 작동하지 않는 문제가 발생  
- 조사 결과 GitHub은 **GraphQL의 node ID**와 **REST API의 database ID**라는 **두 가지 ID 체계**를 병행 사용  
- node ID를 **base64 디코딩**한 결과, 하위 32비트에 database ID가 포함되어 있음이 확인되어 **간단한 비트마스크 연산으로 변환 가능**  
- 추가 분석을 통해 GitHub이 **MessagePack 기반의 새로운 ID 포맷**과 **문자열 기반의 레거시 포맷**을 혼용하고 있음이 드러남  
- 이러한 구조는 **GitHub 내부 객체 식별 체계의 이중성**을 보여주며, 개발자에게 API 통합 시 주의가 필요함  

---
### GitHub의 이중 ID 체계 발견
- Greptile의 **AI 코드 리뷰 도구** 기능 개발 중, GitHub PR 코멘트 링크가 작동하지 않는 문제 발생  
  - 저장된 코멘트 ID를 URL에 연결했으나, 클릭 시 GitHub 페이지로 이동되지 않음  
- GitHub 문서 확인 결과, **GraphQL API의 node ID**와 **REST API의 database ID**가 서로 다른 체계로 존재  
  - node ID 예시: `PRRC_kwDOL4aMSs6Tkzl8`  
  - database ID 예시: `2475899260`  
- node ID는 GitHub 전체에서 객체를 전역적으로 식별하기 위한 **base64 인코딩 문자열**, database ID는 **정수형 URL 식별자**로 사용  

### node ID와 database ID의 관계 분석
- 여러 PR 코멘트의 node ID와 database ID를 비교한 결과, **두 값이 일정한 간격으로 증가**함을 확인  
- node ID의 base64 부분을 디코딩하자 **96비트 정수**가 생성되었으며, 이 값의 하위 32비트가 database ID와 일치  
  - 예시: `PRRC_kwDOL4aMSs6Tkzl8` → 하위 32비트 = `2475899260`  
- 간단한 **비트마스크 연산**으로 database ID를 추출 가능  
  - `decoded & ((1 << 32) - 1)` 형태의 연산으로 변환 수행  

### GitHub의 레거시 ID 포맷
- 오래된 저장소(`torvalds/linux`)의 node ID를 디코딩하자 **다른 형식의 문자열**이 나타남  
  - 예시: `MDEwOlJlcG9zaXRvcnkyMzI1Mjk4` → `010:Repository2325298`  
- 이 포맷은 `[객체 타입 번호]:[객체 이름][Database ID]` 구조로, **명시적 문자열 기반 식별자**임  
- 트리 객체의 경우 `04:Tree2325298:7201bfb9...` 형태로, **리포지토리 ID와 SHA 값**을 함께 포함  
- GitHub은 **레거시 포맷과 새로운 포맷을 병행 사용**하며, 객체 유형과 생성 시점에 따라 포맷이 달라짐  

### 새로운 node ID 포맷의 구조
- GitHub의 **GraphQL 마이그레이션 가이드**는 node ID를 불투명 문자열로 취급하라고 명시하지만, 내부 구조는 존재  
- base64 디코딩 후 **MessagePack**으로 언팩하면 배열 형태의 데이터가 나타남  
  - 예시: `[0, 47954445, 2475899260]`  
- 배열의 구성  
  - 첫 번째 요소(0): 버전 식별자로 추정  
  - 두 번째 요소(47954445): 리포지토리의 database ID  
  - 세 번째 요소(2475899260): 객체의 database ID  
- 객체 유형에 따라 배열 길이가 다르며, **커밋은 SHA를 포함**, **리포지토리는 두 요소만 포함**  

### 실용적 활용과 결론
- 새로운 node ID에서 database ID를 추출하는 Python 코드 예시  
  ```python
  import base64, msgpack
  def node_id_to_database_id(node_id):
      prefix, encoded = node_id.split('_')
      packed = base64.b64decode(encoded)
      array = msgpack.unpackb(packed)
      return array[-1]
  ```
- 이 방식으로 **PR 코멘트의 database ID를 직접 추출**하여 URL 링크 문제 해결 가능  
- GitHub은 현재 **MessagePack 기반의 새로운 ID 체계와 문자열 기반의 레거시 체계**를 동시에 유지  
- 이러한 구조는 **GitHub 내부의 전환 과정과 호환성 유지 노력**을 보여주며, API를 사용하는 개발자는 ID 포맷 차이에 주의해야 함

## Comments



### Comment 49229

- Author: neo
- Created: 2026-01-15T05:32:50+09:00
- Points: 1

###### [Hacker News 의견들](https://news.ycombinator.com/item?id=46602591) 
- 최신 **GitHub 글로벌 노드 ID**는 `'X-Github-Next-Global-ID'` 헤더를 통해 강제로 사용할 수 있음  
  ID는 객체의 **타입 접두사**와 base64로 인코딩된 msgpack 페이로드로 구성됨  
  예를 들어 내 사용자 ID `"U_kgDOAAhEkg"`는 `[0, 541842]`로 디코딩되며, 이는 REST API의 `databaseId`와 일치함  
  하지만 이런 내부 구현에 의존하지 말고, GraphQL API의 `databaseId` 필드를 직접 조회하는 것이 좋음  
  관련 문서: [GraphQL 글로벌 노드 ID 마이그레이션 가이드](https://docs.github.com/en/graphql/guides/migrating-graphql-global-node-ids), [내 GitHub 사용자 정보](https://api.github.com/user/541842), [CyberChef 디코딩 예시](https://gchq.github.io/CyberChef/#recipe=Find_/_Replace(%7B'option':'Regex','string':'%5E%5B%5E_%5D%2B_'%7D,'',true,false,true,false)From_Base64('A-Za-z0-9%2B/%3D',true,false)From_MessagePack()&input=VV9rZ0RPQUFoRWtn), [GitHub ETag 구현](https://github.com/bored-engineer/github-conditional-http-transport)

- 이런 식으로 디코딩하는 건 **취약**하다고 생각함  
  GraphQL의 글로벌 노드 ID는 본래 **불투명(opaque)** 해야 함  
  GitHub의 여러 타입(PullRequest 등)은 `databaseId` 필드를 제공하므로 그걸 쓰는 게 맞음  
  대부분의 GraphQL API는 타입명과 DB ID를 base64로 인코딩하지만, 이 규칙이 항상 유지된다고 보장할 수 없음  
  참고: [PullRequest 객체 문서](https://docs.github.com/en/graphql/reference/objects#pullrequest), [GraphQL 글로벌 ID 스펙](https://graphql.org/learn/global-object-identification/)
  - GitHub의 GraphQL 타입에는 `permalink`, `url` 같은 필드와 `UniformResourceLocatable` 인터페이스가 있어서 직접 URL을 구성할 필요가 없음
  - 이런 내부 구조는 **시간이 지나면 깨질 가능성**이 높음  
    그래서 API가 permalink를 제공하는 이유가 있음. ID나 링크 패턴은 언제든 바뀔 수 있음
  - 식별자에 메타데이터를 넣고 싶다면, 사용자가 내부 구조에 의존하지 않도록 **암호화**하는 게 좋음  
    이런 방식은 pagination 토큰에서도 자주 사용됨

- `010:Repository2325298` 같은 ID는 명확한 구조를 가짐  
  `010`은 타입 enum, `Repository`는 이름, `2325298`은 DB ID임  
  즉, **길이 접두사(length prefix)** 형태임. Repository는 10자, Tree는 4자임
  - BitTorrent 프로토콜이 떠오름
  - 거의 **URN**처럼 보임

- **Opus 4.5**는 이런 GitHub ID 디코딩 트릭을 알고 있으며, 자동으로 디코딩 코드를 작성함

- 작성자가 발견한 내용은 기술적으로는 맞지만, **문서화되지 않았고 지원되지 않음**  
  GitHub는 과거에도 노드 ID 내부 구조를 조용히 바꾼 적이 있음  
  MessagePack 배열에 필드를 추가하거나, 인코딩을 바꾸거나, 암호화하거나, UUID 기반으로 바꾸면  
  이런 내부 구조에 의존한 시스템은 즉시 깨짐

- 내가 명시적으로 저장하는 GitHub 식별자는 **불변 URL 키(issue/pr 번호나 커밋 해시)** 정도임  
  댓글 ID는 JSON 블롭 안에 그냥 포함시킴  
  모든 걸 정규화하려고 할 필요는 없음. JSON은 충분히 빠름  
  코멘트 단위로 교차 쿼리를 하지 않는 이상, 성능 문제로 드러날 일은 거의 없음
  - 하지만 issue/pr URL은 **불변이 아님**  
    저장소가 이름을 바꾸거나 다른 조직으로 옮기면 URL이 바뀔 수 있음

- 예전 v3 API에는 ID가 없어서, 누군가 사용자명이나 저장소명을 바꾸면 누군지 추적하기 어려웠음  
  그래서 나는 **팀 단위 소유권 관리 시스템**을 직접 구현했음  
  Terraform provider가 별로라서, 오프보딩 시 “유일한 관리자였던 사람이 나갔다” 같은 문제가 자주 생겼기 때문임  
  모든 저장소는 팀이 소유하고, 접근 권한도 팀 단위로만 부여함
  - “사용자에게 접근 권한을 준다”가 아니라 “팀에 권한을 주고, 사용자는 그 팀의 일원이다”라는 사고방식이 훨씬 효율적임  
    이런 **팀 기반 접근 제어**는 GitHub뿐 아니라 다른 시스템에도 유용함

- **Hyrum’s Law**의 전형적인 사례임 — 사람들이 문서화되지 않은 동작에 의존하기 시작하면 결국 깨짐

- 데이터베이스 설계에서는 보통 외부에는 **불투명한 자연 키**를 제공하고, 내부에서는 증가형 정수 ID를 사용함
  - 그 이유는 두 가지임  
    1) 외부에 객체 개수를 노출하지 않기 위해  
    2) ID를 단순히 증가시켜 모든 객체를 순회하지 못하게 하기 위해  
    하지만 복합 ID를 쓰면 이런 문제가 줄어듦.  
    예를 들어 저장소 ID 안에 객체 ID가 포함되어 있으면, ID를 증가시켜도 같은 저장소 내 객체만 탐색됨  
    여기에 **엔트로피나 타임스탬프**를 섞으면 악용이 거의 불가능함
  - 하지만 자연 키는 **변경될 수 있음**  
    그래서 의미 없는 **대체 키(surrogate key)** 를 노출하는 게 더 안전함  
    예를 들어 YouTube는 내부적으로 인덱스 번호를 쓰더라도, 외부에는 의미 없는 코드 형태의 ID를 제공함

- GitHub 팀이 최근 몇 년간 Rails에 **sharded/multi-database 지원**을 대폭 확장한 이유가 이제 이해됨
