ADR-08: External Repository ID 기반 데이터 무결성
| 날짜 | 작성자 | 리포지토리 |
|---|---|---|
| 2024-12-22 | @KubrickCode | all |
Context
문제
현재 리포지토리 식별 방식: UNIQUE (host, owner, name) 제약조건 사용. 여러 시나리오에서 실패:
시나리오: 삭제 후 재생성
1. 리포 A: alice/my-repo (external_repo_id: 100) → 분석 완료
2. 리포 A 삭제
3. 리포 B 생성: alice/my-repo (external_repo_id: 200)
4. Scheduler가 alice/my-repo 재분석 요청
5. Clone 성공 (리포 B)
6. 분석 결과가 리포 A의 row에 저장
→ 데이터 오염!추가 시나리오
| 시나리오 | 문제 |
|---|---|
| Rename (alice/old → alice/new) | 새 이름으로 요청 시 히스토리 단절 |
| Transfer (alice/repo → bob/repo) | owner 변경 시 히스토리 단절 |
| 삭제 후 재생성 | 다른 리포 데이터가 기존 히스토리에 오염 |
목표
- 데이터 무결성: 잘못된 리포지토리의 분석 결과가 저장되는 것 방지
- 히스토리 연속성: rename/transfer 시 분석 히스토리 유지
- API 효율성: VCS API 호출 최소화 (rate limit 고려)
Decision
이중 검증 메커니즘 채택: external_repo_id로 식별 + git fetch SHA로 무결성 검증
핵심 원칙
재분석 시 API 호출 없이
git fetch <last_commit_sha>로 무결성 검증
메커니즘 조합
| 메커니즘 | 용도 | API 필요 |
|---|---|---|
external_repo_id | Rename/Transfer 시 히스토리 연결 | Yes (신규 분석) |
git fetch <sha> 검증 | 같은 리포인지 확인 | No |
git fetch SHA 검증
bash
# 마지막으로 분석한 commit이 현재 리포에 존재하는지 확인
git fetch --depth 1 origin <last_commit_sha>
# 결과
# - 성공: 같은 리포 (해당 commit 존재)
# - 실패: 다른 리포 (삭제 후 재생성) 또는 force push에러 메시지 (존재하지 않는 경우):
fatal: remote error: upload-pack: not our ref <sha>Options Considered
Option A: 항상 VCS API 호출 (기각)
설명: 모든 분석 시 API 호출로 리포지토리 ID 확인 및 검증.
장점:
- 단순한 구현
- 항상 정확
단점:
- Rate limit 소진 (GitHub 5000/hr)
- 지연 시간 증가
- 빈번한 재분석에 확장성 부족
Option B: git fetch SHA만 사용 (기각)
설명: external_repo_id 없이 git fetch 검증만 사용.
장점:
- API 호출 0회
- 단순
단점:
- Rename/transfer 감지 불가 (히스토리 단절)
- Force push와 삭제 후 재생성 구분 불가
Option C: 이중 메커니즘 (선택)
설명: external_repo_id 저장과 git fetch SHA 검증 조합.
장점:
- 필요 시에만 API 호출 (신규 분석, 검증 실패)
- external_repo_id로 rename/transfer 감지
- Force push vs 삭제 후 재생성 구분
- 확장 가능 (대부분의 재분석은 API 호출 불필요)
단점:
- 구현 복잡도 증가
- 스키마 변경 필요
Implementation
케이스 분류
| 케이스 | 조건 | 결과 |
|---|---|---|
| A | DB에 없음, external_repo_id도 없음 | 새 codebase 생성 |
| B | DB에 있음, git fetch 성공 | 기존 codebase 재분석 |
| D | DB에 없음, external_repo_id는 존재 | owner/name 업데이트 |
| E | DB에 있음, git fetch 실패, ID 다름 | stale 마킹 + 새 생성 |
| F | DB에 있음, git fetch 실패, ID 같음 | Force push, 재분석 |
흐름
[분석 요청: owner/repo]
│
├─ 1. Clone
│
├─ 2. DB 조회 (owner, repo)
│ │
│ ├─ 없음 ──────────────────────────────┐
│ │ │
│ └─ 있음 │
│ │ │
│ ├─ 3. git fetch <last_sha> │
│ │ │ │
│ │ ├─ 성공 │
│ │ │ → 분석 진행 │
│ │ │ │
│ │ └─ 실패 │
│ │ │ │
│ │ ▼ │
│ └───────────┴───────────────────┤
│ │
│ ┌───────────────────┘
│ │
│ ▼
│ 4. VCS API 호출
│ → external_repo_id
│ │
│ ▼
│ 5. DB 조회 (external_repo_id)
│ │
│ ┌────────┴────────┐
│ │ │
│ 있음 없음
│ │ │
│ ▼ ▼
│ owner/name 새 codebase
│ 업데이트 생성
│ │ │
│ └────────┬────────┘
│ │
│ ▼
└──────────────→ 6. 분석 & 저장스키마 변경
sql
-- 컬럼 추가
ALTER TABLE codebases ADD COLUMN external_repo_id VARCHAR(64);
ALTER TABLE codebases ADD COLUMN is_stale BOOLEAN DEFAULT false;
-- owner/name partial unique 인덱스 (stale 제외)
CREATE UNIQUE INDEX idx_codebases_owner_name
ON codebases(host, owner, name)
WHERE is_stale = false;
-- external_repo_id unique 인덱스
CREATE UNIQUE INDEX idx_codebases_external_repo_id
ON codebases(host, external_repo_id);VARCHAR(64) 선택 이유:
| 플랫폼 | 타입 | 예시 |
|---|---|---|
| GitHub | BIGINT | 123456789 |
| GitLab | INTEGER | 12345678 |
| Bitbucket | UUID | {550e8400-e29b-41d4-a716-446655440000} |
모든 타입을 문자열로 통일하여 저장.
Race Condition 처리
Clone-Rename Race:
T1: Worker가 alice/old-repo clone
T2: 사용자가 alice/old-repo → alice/new-repo로 rename
T3: Worker가 clone 완료 (old-repo 코드)
T4: Worker가 API 호출 → external_repo_id: 100
T5: DB에서 id=100 조회 → alice/new-repo로 변경됨
T6: Worker가 old-repo 코드를 new-repo에 저장
→ 데이터 오염!해결책: Clone 시점의 owner/name과 API 조회 결과 비교
go
if existingCodebase.Owner != req.Owner || existingCodebase.Name != req.Name {
return ErrRaceConditionDetected // 재시도 유도
}동시 분석 요청:
(host, external_repo_id)unique 제약조건으로 중복 생성 방지- Application layer에서 케이스별 분리 처리 (UPSERT 사용 금지)
Stale 정책
| 항목 | 값 |
|---|---|
| 보존 기간 | 30일 |
| UI 표시 | "리포지토리가 더 이상 존재하지 않습니다" |
| 자동 삭제 | 30일 후 |
Consequences
Positive
데이터 무결성:
- 삭제 후 재생성 시나리오 올바르게 처리
- Force push와 식별 변경 구분
- Rename/transfer 시 히스토리 보존
효율성:
- 대부분의 재분석은 API 호출 불필요
- Rate limit 부담 최소화
- 수백만 리포지토리로 확장 가능
경쟁 우위:
- Codecov/Coveralls와 달리 rename 시 자동 히스토리 연결
- 수동 재설정 불필요
Negative
복잡도:
- 6가지 케이스 분류 구현 필요
- 스키마 마이그레이션 필요
- Race condition 처리 필요
마이그레이션:
- 기존 codebase에 external_repo_id 백필 필요
- 단계적 배포 필요 (nullable → 백필 → NOT NULL)
플랫폼 의존성:
- Bitbucket Cloud git fetch SHA 지원 불확실
- GitLab self-hosted는
uploadpack.allowReachableSHA1InWant설정 필요
플랫폼 지원
| 플랫폼 | git fetch SHA | 테스트 결과 |
|---|---|---|
| GitHub | 지원 | 직접 테스트 완료 |
| GitLab | 지원 | 직접 테스트 완료 |
| Bitbucket Server | 지원 (v5.5+) | 문서 확인 |
| Bitbucket Cloud | 불확실 | 추후 테스트 필요 |
API 호출 빈도
| 케이스 | API 호출 | 빈도 |
|---|---|---|
| 신규 분석 | 1회 | 낮음 |
| 재분석 (정상) | 0회 | 높음 |
| Scheduler (정상) | 0회 | 높음 |
| 삭제 후 재생성 | 1회 | 매우 낮음 |
| Force push | 1회 | 매우 낮음 |
| Rename/Transfer | 1회 | 매우 낮음 |
대부분의 케이스에서 API 호출 불필요 → Rate limit 부담 최소화
References
- ADR-07: Repository 패턴 - 데이터 접근 추상화
- ADR-05: Worker-Scheduler 분리 - 프로세스 아키텍처
- GitHub API Rate Limits
