토큰이 건 링크가 어디서 유실되는지, 후보 4안이 엣지케이스 10건을 각각 어떻게 처리하는지, 그리고 어느 안도 풀지 못하는 것은 무엇인지.
· 근거: docs/features/social-url-structure-audit/samples/ 실측 · src/modules/social-graph/ 현행 코드 · be-system-design.md
이 문서는 C1(링크 도착점 모델링) 결정 하나를 위한 비교표입니다. 최종 스키마의 필드 목록, 인덱스 설계, 마이그레이션 계획은 담지 않았습니다. draft 의 나머지 항목(공통 필드 최소화, Creator 재정의, 문서 표기 구분)은 C1 이 정해져야 답이 나오므로 다음 단계로 미뤘습니다.
이 절이 답하는 것 — 엣지 1행이 생기려면 통과해야 하는 관문이 무엇이고, 실측 URL 중 몇 %가 각 관문에서 탈락하는가.
토큰과 소셜을 잇는 기록은 content_token_links 컬렉션의 한 행입니다. 이 행이 만들어지려면 도착점이 반드시
contents 컬렉션의 문서여야 합니다. 모델 코드가 그렇게 강제합니다.
// src/modules/social-graph/content-token-link.model.ts:26-28 /** 토큰이 실제로 건 것. `lineage[0]` 과 항상 같은 중복 저장이지만 … */ @prop({ required: true, type: Schema.Types.String }) content_key: string; // ← contents FK
그리고 content_key 는 Content.buildKey(sourceType, platformContentId) 로 조립됩니다.
platformContentId 는 fetch 응답이 준 값입니다. 따라서 엣지 한 행이 생기려면 서로 다른 조건 두 개를
모두 통과해야 합니다. 하나라도 실패하면 행은 만들어지지 않고, 실패했다는 기록조차 남지 않습니다.
audit 샘플 13종에 기록된 URL 은 모두 7,742건입니다. 이 중 source type 만으로 Content 가 아님이 확정되는 것, Content 임이 확정되는 것, 그리고 한 파일 안에 프로필과 게시물이 섞여 있어 구조를 더 봐야 하는 것으로 나뉩니다.
| 구분 | source type | 건수 | 지금 어디로 가는가 |
|---|---|---|---|
| 비Content | x_profile | 1,118 | accounts 마스터 1행. 토큰과 이어진 기록 없음 |
| unknown | 398 | tokens.unclassified_links[] 문자열 | |
| tg_channel | 360 | venues 마스터 1행. 토큰과 이어진 기록 없음 | |
| tiktok_profile | 273 | accounts 마스터 1행 | |
| x_community | 137 | venues 마스터 1행 | |
| tiktok_search | 57 | tokens.unclassified_links[] 문자열 | |
| tg_guard_group | 3 | venues 마스터 1행 | |
| Content | x_tweet | 3,879 | contents + 엣지 ✓ |
| tiktok_video | 115 | contents + 엣지 ✓ (단 관문 2 가 남음) | |
| 혼재 | 576 | 프로필 390 · 게시물 178 · 기타 8 | |
| youtube | 514 | 채널(@handle) 104 · 영상 246 · 판별 필요 164 | |
| 161 | 게시물 106 · share 10 · 사용자 18 · 서브레딧 16 · 기타 11 | ||
| github | 151 | 레포 78 · 소유자 68 · Pages 5 |
혼재 1,402건의 내역을 구조별로 갈라 보면 비Content 가 약 620건 더 나옵니다. 이를 더하면 관문 1 에서 탈락하는 URL 은 약 2,966건, 전체의 38.3% 입니다. 이 수치는 fetch 가 100% 성공한다고 가정해도 달라지지 않습니다. 종류 자체가 Content 가 아니라서 탈락하기 때문입니다.
관문 1 을 통과한 URL 도 fetch 가 실패하면 platform_id 를 못 얻어 탈락합니다. 아래는 audit 이 구조별로 3건씩
뽑아 실제로 fetch 해 본 결과입니다. 전수 조사가 아니라 표본이므로 비율로 확대 해석하면 안 되지만, 어떤 소셜이
구조적으로 막혀 있는지는 분명하게 드러납니다.
| source type | 표본 fetch 결과 | 원인 |
|---|---|---|
| skipped_paid 20 / 20 | Apify 유료 게이트. enablePaid=false 면 전건 차단 | |
| tiktok_video | skipped_paid 6 / 6 | 같은 유료 게이트 |
| error 18 / 18 | Reddit 이 우리 IP 를 차단 | |
| youtube | ok 6 · not_found 11 | 삭제되었거나 비공개 전환된 영상 |
| x_tweet | ok 11 · not_found 1 | 유일하게 대부분 통과 |
| tiktok_search | unsupported 6 / 6 | 비객체라 애초에 fetch 대상이 아님 |
| unknown | 시도 없음 37 | 종류를 모르므로 fetcher 를 고를 수 없음 |
Instagram 을 예로 들면, 576건 중 178건만 관문 1 을 통과하고, 그 178건이 전부 유료 게이트에서 탈락합니다.
결과적으로 Instagram URL 576건 전부가 엣지를 한 행도 만들지 못합니다. Reddit 은 106건이 관문 1 을 통과하지만
IP 차단으로 관문 2 에서 전멸합니다. 남는 것은 사실상 x_tweet 계열뿐입니다.
※ 7,742 는 샘플 파일 13종의 totalUrls 합계입니다. be-system-design.md 가 인용한
"7,087 distinct URLs" 와 다른 이유는 중복 URL 포함 여부 차이로 보이며, 이 문서의 비율 계산에는 영향을 주지 않습니다.
이 절이 답하는 것 — 두 축(키 전략 · 도착점 구조)의 조합이 각각 어떤 데이터 모양이 되는가.
후보들은 서로 독립적인 두 축의 조합입니다. 첫째 축은 무엇을 정체성으로 삼는가입니다. 문자열을 조립해 만드는
자연키를 쓸 수도 있고, MongoDB 가 발급하는 _id 를 대리키로 쓸 수도 있습니다. 둘째 축은
엣지가 어디에 도착하는가입니다. 모든 URL 을 받아주는 얇은 컬렉션을 하나 두고 거기로 보낼 수도 있고,
기존 세 마스터 컬렉션 중 하나를 골라 가리키게 할 수도 있습니다.
| 도착점 = 스파인 컬렉션 | 도착점 = 마스터 3종 (다형 포인터) | |
|---|---|---|
| 문자열 자연키 | 안 1 — 키에 subtype 을 넣고, 나중에 정체가 바뀌면 resolves_to 포인터로 잇는다 | 조합하지 않음 — 다형 포인터는 키가 바뀌면 그대로 깨져서 안 0 과 같아진다 |
_id 대리키 | 안 2 — 스파인 행마다 _id. 정체가 바뀌면 컬럼만 갱신 | 안 3 — 엣지가 target_id + target_collection 을 들고 마스터를 가리킨다 |
여기에 비교 기준선으로 안 0(현행) 을 함께 놓습니다. 네 가지의 데이터 모양은 이렇습니다.
contents 전용{platform}:{subtype}:{url_key}, 정체 변경은 resolves_tosubtype 이 들어가 있어 정체가 바뀌면 키도 바뀌므로,
옛 행을 남기고 resolves_to 로 새 행을 가리키게 합니다. 읽는 쪽이 포인터를 따라가야 합니다.
_id 대리키 + 스파인_id, 중복 판정 = 복합 unique indexsubtype 이 바뀌어도 행의 정체성은 유지됩니다. 대신 "이 URL 이 기존 행인가"를
판정할 복합 unique index 가 필요하고, 그 구성 필드를 골라야 합니다.
_id 대리키 + 다형 포인터이 절이 답하는 것 — 각 케이스가 실제로 어떤 URL 이고, 실측 몇 건이며, 무엇이 깨지는가.
아래 10건은 앞선 논의에서 실제로 안들의 판정이 갈렸던 것만 골랐습니다. 모두 audit 실측 또는 코드·설계 문서에 근거가 있는 사례이며, 가정으로 만든 것은 없습니다.
| # | 케이스 | URL 예시 | 실측 | 무엇이 깨지는가 |
|---|---|---|---|---|
| E1 | 유료 게이트 platform_id 를 영영 못 얻을 수 있음 |
instagram.com/p/Bjnx75lFFQp | IG 576건 표본 20/20 skipped_paid |
응답 유래 id 를 정체성으로 쓰면 행 자체를 만들 수 없다. 결제하지 않는 한 영구히 그렇다. |
| E2 | 같은 게시물, 다른 URL 1대다 — 분리 기록 |
instagram.com/p/{code} instagram.com/{h}/reel/{code} instagram.com/p/{code}/embed |
170 · 5 · 3건 | URL 유래 식별자를 정체성으로 쓰면 같은 게시물이 세 행이 된다. 지표 타임라인이 셋으로 쪼개진다. |
| E3 | 단축 링크 사후 해석 정체가 나중에 밝혀짐 |
vm.tiktok.com/ZSABC123 | fetcher 가 tiktok_shortlink 를 "미해석 마커"로 정의 |
해석되는 순간 subtype 이 shortlink→video 로 바뀐다. 키에 subtype 이 들어 있으면 키가 바뀌어 엣지가 끊긴다. |
| E4 | 핸들 재할당 다대1 — 잘못된 병합 |
tiktok.com/@doge | TT 프로필 273건 설계 §1.4: 월 주기 회전·재할당 |
핸들을 정체성으로 쓰면 2024년 소유자와 2025년 소유자가 한 행으로 합쳐진다. 남의 데이터가 섞여 들어오므로 조용히 틀린다. |
| E5 | 키 공간 충돌 사용자와 서브레딧이 같은 이름 |
reddit.com/user/spez reddit.com/r/spez |
RD 사용자 18 · 서브레딧 16건 | 현행 규칙은 둘 다 rd:spez 다. 지금은 컬렉션이 달라 안전하지만, 한 컬렉션으로 합치는 순간 충돌한다. |
| E6 | 성격 정밀화 대상은 같고 성격만 밝혀짐 |
t.me/{name} | TG 채널 360건 표본 24건: 포털 54% · 빈껍데기 25% · 활성 4% |
URL 만으로는 판정 불가라 fetcher 가 관측 후 portal/shell 로 좁힌다. 이때 subtype 이 바뀌면 E3 과 같은 문제가 난다. |
| E7 | Content 아닌 도착점 가장 큰 유실 |
x.com/{handle} | 1,118건 (단일 최대) | 계정 마스터 1행은 생기지만 어느 토큰이 걸었는지가 어디에도 안 남는다. 토큰↔KOL 축이 통째로 없다. |
| E8 | 미분류 사후 해석 classifier 추가로 해석됨 |
x.com/search?q=… x.com/hashtag/{tag} |
unknown 398건 중 200건이 X 검색, 97건이 TikTok |
지금은 문자열로만 남는다. 나중에 classifier 를 붙이면 종류가 확정되는데, 그때 정체성이 바뀐다. |
| E9 | 미관측 대상 참조 아직 fetch 안 한 것을 가리켜야 함 |
인용 트윗의 작성자 mentions[] 의 계정 |
설계 §1.4 source_ref{via, content_key, depth} |
대리키는 행이 있어야 값이 생긴다. 참조하려면 빈 행(stub)을 먼저 만들어야 한다. |
| E10 | 공유 도메인 여러 토큰이 같은 사이트 |
foo.com | v3 §1371: "4번째 객체를 만들지 않는다" → tokens.web{} |
도메인이 토큰의 속성으로 들어가 있어, 같은 도메인을 공유하는 토큰들이 한 지점에서 만나지 않는다. |
이 절이 답하는 것 — 10개 케이스를 4안이 각각 해결·부분해결·미해결 중 무엇으로 처리하는가.
| 케이스 | 안 0 · 현행 | 안 1 · 자연키+스파인 | 안 2 · 대리키+스파인 | 안 3 · 대리키+다형 |
|---|---|---|---|---|
| E1 유료 게이트 | 미해결응답 id 가 없어 행을 못 만든다. IG 576건 전멸. | 해결URL 유래 키로 스파인 행이 즉시 생긴다. | 해결같음. 나중에 응답을 받으면 같은 행에 채운다. | 미해결contents 에 응답 없는 stub 을 넣으면 마스터가 오염되고, 안 넣으면 유실된다. |
| E2 같은 게시물 3형태 | 해당 없음응답 id 기준이라 분리는 없지만, 애초에 행이 없다. | 미해결세 행이 생기고, fetch 를 못 하면 이어붙일 근거가 없다. 병합 수단이 없다. | 부분세 행이 생기지만 응답 id 를 얻는 순간 병합할 수 있다. 병합 시 엣지·스냅샷 재연결 필요. | 미해결E1 과 같은 이유로 행 자체가 없다. |
| E3 단축 링크 해석 | 미해결해석 전에는 행이 없고, 해석 후 생긴 행은 이전 관측과 무관하다. | 부분옛 행을 남기고 resolves_to 로 잇는다. 읽는 쪽이 매번 포인터를 따라가야 한다. |
해결subtype 컬럼만 갱신한다. 엣지·스냅샷은 손대지 않는다. |
미해결해석 전 단계에서 가리킬 마스터가 없다. |
| E4 핸들 재할당 | 해결응답 id 기준이라 두 소유자가 애초에 다른 행이다. | 미해결스파인의 tt:profile:doge 가 한 행이고 resolves_to 는 최신 하나만 가리킨다. 과거 엣지가 새 소유자로 해석된다. |
부분스파인 행은 하나지만 마스터 링크를 구간으로 들면 as-of 해석이 된다. accounts.handles[] 와 같은 방식이라 선례가 있다. |
해결엣지가 accounts 를 직접 가리키고 그 키가 응답 id 다. |
| E5 u/spez ↔ r/spez | 해결컬렉션이 분리되어 있어 같은 문자열이어도 충돌하지 않는다. | 해결키에 subtype 이 들어가 rd:user:spez 와 rd:subreddit:spez 로 갈린다. |
해결복합 unique 에 subtype 이 포함된다. |
해결컬렉션 분리 유지. |
| E6 채널 성격 정밀화 | 부분venues 행은 생기지만 포털인지 빈껍데기인지가 스키마에 자리가 없다. |
해결subtype 을 "URL 로 판정 가능한 것"으로 못박아 tg:channel:foo 를 고정하고, 성격은 상세의 상태 필드로 내린다. |
해결키가 값과 무관하므로 subtype 을 그대로 갱신해도 된다. 정의를 바꿀 필요조차 없다. |
해결venues 행의 컬럼 갱신. |
| E7 프로필 링크 1,118건 | 미해결엣지가 contents 만 가리킨다. 단일 최대 유실. |
해결스파인 행이 생기고 엣지가 그것을 가리킨다. | 해결같음. | 해결엣지가 accounts 를 직접 가리킨다. |
| E8 unknown 사후 해석 | 미해결문자열 배열로만 남아 플랫폼·종류 집계에 참여하지 못한다. | 부분행은 생긴다. 해석되면 키가 바뀌므로 resolves_to 체인이 하나 더 쌓인다. |
해결행이 생기고, 해석되면 subtype 컬럼만 갱신한다. 200건의 X 검색이 그대로 살아난다. |
미해결종류를 모르므로 세 마스터 중 어디에도 넣을 수 없다. |
| E9 미관측 대상 참조 | 해결문자열 키를 그냥 적어두면 나중에 실제 행이 생겼을 때 자동으로 이어진다. | 해결같음. | 부분참조 시점에 stub 행을 만들어야 한다. 다만 "본 것은 다 행으로"라는 스파인 규칙과 방향이 같아 추가 개념은 아니다. | 부분stub 을 accounts·contents 에 직접 만들어야 해서 마스터에 미관측 행이 섞인다. |
| E10 공유 도메인 | 부분tokens.web.domain 인덱스로 공유 집계는 되지만, 링크 축에서는 보이지 않는다. |
해결web:site:foo.com 한 행을 여러 토큰이 가리킨다. |
해결같음. | 미해결사이트를 받아줄 컬렉션이 없다. 만들면 그게 곧 스파인이다. |
안 3 은 비객체를 받아줄 자리가 없습니다. E1·E8·E10 이 모두 같은 이유로 미해결입니다. 이 셋을 풀려면 네 번째 컬렉션을 만들어야 하는데, 그 컬렉션이 정확히 스파인입니다. 즉 안 3 은 요구사항을 만족시키려 하면 안 2 로 수렴합니다.
안 1 과 안 2 의 차이는 E2·E3·E4·E8 네 곳에 몰려 있고, 전부 "정체가 나중에 바뀌는" 케이스입니다. 자연키는 정체성이 값에 묶여 있어 값이 바뀌면 키가 바뀌고, 그래서 별칭 포인터로 우회해야 합니다. 대리키는 정체성이 값과 분리되어 있어 컬럼만 갱신하면 됩니다. 대신 E9 에서 stub 비용을 냅니다.
안 0 이 유일하게 이기는 곳은 E4 와 E9 입니다. 둘 다 "응답 유래 id 를 정체성으로 쓴다"는 현행 규칙(v3 §⑬)의 직접적 이점입니다. 이 이점은 안 2 에서도 마스터 계층에 그대로 남습니다.
이 절이 답하는 것 — 시간이 흐르며 새 사실을 알게 될 때 각 안의 DB 행이 실제로 어떻게 변하는가.
Instagram 게시물 URL 을 토큰이 걸었고, 관측 시점에는 유료 게이트 때문에 fetch 를 못 했습니다. 한참 뒤 유료 옵션을 켜고 다시 관측합니다.
instagram.com/p/Bjnx75lFFQp 를 건 것을 관측 · fetch 결과 skipped_paidcontents (행 없음) 엣지 (행 없음) tokens.unclassified_links (website 도 아니라 여기도 안 감)
link_targets target_key ig:post:Bjnx75lFFQp status skipped_paid resolves_to null 엣지 target_key → ig:post:Bjnx75lFFQp
link_targets _id ObjectId(A) platform ig subtype post url_key Bjnx75lFFQp platform_id null status skipped_paid 엣지 target_id → ObjectId(A)
17954… 를 돌려줌contents content_key instagram_post:17954… 엣지 t1 관측은 복원 불가 t2 이후 것만 남는다
link_targets (t1 행 유지) target_key ig:post:Bjnx75lFFQp resolves_to ig:post:17954… ← 채움 contents entity_key ig:post:17954…
link_targets (같은 _id) _id ObjectId(A) platform_id 17954… ← 채움 status ok contents target_id → ObjectId(A) 엣지 t1 에 쓴 그대로. 손대지 않음
안 1 과 안 2 는 결과가 같습니다. 차이는 비용을 언제 내는가입니다. 안 1 은 읽을 때마다 포인터를 따라가고, 안 2 는 쓸 때 한 번 컬럼을 채웁니다. 읽기 횟수가 쓰기 횟수보다 훨씬 많은 워크로드라면 안 2 가 유리합니다.
토큰이 TikTok 단축 링크를 걸었고, 나중에 다른 토큰이 같은 영상의 정규 URL 을 겁니다. 그 뒤 단축 링크의 정체가 밝혀집니다. 이 순서가 중요합니다 — 정규 URL 행이 이미 존재하는 상태에서 해석이 일어나기 때문입니다.
vm.tiktok.com/ZSABC123 · 해석 불가link_targets target_key tt:shortlink:ZSABC123 엣지 ABC → tt:shortlink:ZSABC123
link_targets _id ObjectId(A) subtype shortlink url_key ZSABC123 엣지 ABC → ObjectId(A)
tiktok.com/@espn/video/7412… · 같은 영상, 정규 URLlink_targets 행 2개 tt:shortlink:ZSABC123 tt:video:7412… 엣지 ABC → shortlink 행 XYZ → video 행
link_targets 행 2개 ObjectId(A) shortlink ZSABC123 ObjectId(B) video 7412… 엣지 ABC → A · XYZ → B
ZSABC123 은 영상 7412… 였다link_targets tt:shortlink:ZSABC123 resolves_to tt:video:7412… tt:video:7412… "이 영상을 건 토큰 수" 쿼리: video 행 직접 + shortlink 행 경유 둘을 합쳐야 2가 나온다
병합: ObjectId(A) → ObjectId(B) 엣지 ABC 의 target_id 를 B 로 재연결 A 행 삭제 또는 tombstone link_targets 행 1개 ObjectId(B) video 7412… 엣지 ABC → B · XYZ → B "이 영상을 건 토큰 수" 쿼리: B 를 세면 2. 접을 것 없음
안 1 의 위험은 "포인터를 안 접으면 에러 없이 적은 수가 나온다"는 데 있습니다. 설계 문서가
lineage[0] !== content_key 를 두고 경계한 것과 같은 형태의 실패입니다 — 쿼리가 실패하지 않고
틀린 답을 돌려주기 때문에 발견이 늦습니다.
안 2 의 비용은 병합입니다. 옛 _id 를 가리키던 엣지·지표 스냅샷·원본을 모두 재연결해야 합니다.
드물게 일어나는 일이라 배치로 처리할 수 있지만, 공짜는 아닙니다.
이 케이스는 앞의 둘과 방향이 반대입니다. 앞의 둘은 두 행이 사실은 하나였던 경우이고, 이건 한 행이 사실은 둘인 경우입니다.
tiktok.com/@doge · 계정 id 111link_targets tt:profile:doge resolves_to tt:profile:111 엣지 ABC → tt:profile:doge
link_targets ObjectId(A) profile doge links[] [{ id:111, from:t1, to:null }] 엣지 ABC → ObjectId(A) @ t1
@doge 를 계정 222 에게 재할당link_targets tt:profile:doge resolves_to tt:profile:222 ← 덮어씀 "토큰 ABC 가 건 계정은?" 쿼리: 222 라고 답한다 실제로는 111 이었다
link_targets ObjectId(A) profile doge links[] [{ id:111, from:t1, to:t2 }, { id:222, from:t2, to:null }] "토큰 ABC 가 건 계정은?" 쿼리: 엣지의 관측시각 t1 로 구간 조회 → 111. 정확하다
accounts.handles[] 가 이미 같은 방식(first_seen/last_seen)이라 새 개념이 아니다.
안 1 의 resolves_to 는 포인터가 하나뿐이라 시간을 표현할 수 없습니다. 그래서 핸들이 재할당되면
과거 기록이 새 소유자에게 붙습니다. 이걸 고치려면 결국 포인터를 구간 배열로 바꿔야 하는데, 그 순간
안 2 와 같은 모양이 됩니다.
설계 문서 §1.4 는 이미 이 문제 때문에 accounts.handles[] 를 구간으로 들고 있습니다.
"TikTok 은 핸들을 한 달 주기로 회전시키고 옛 핸들을 재할당한다" 는 실측 근거도 거기 적혀 있습니다.
스파인이 핸들 기반 키를 갖는 이상, 같은 구간 장치가 스파인에도 필요합니다.
이 절이 답하는 것 — 4안 전부에 남는 잔여 문제와, 그것을 감수할 때 치르는 비용.
| 잔여 문제 | 언제 터지는가 | 감수한다는 것의 의미 |
|---|---|---|
| 영원히 fetch 못 하는 대상의 정체 | 유료 게이트를 계속 끄는 한 상시 | Instagram 은 응답 id 를 못 얻으므로 어떤 안을 써도 URL 이상은 알 수 없습니다. 스파인은 "이 URL 을 봤다"를 보존할 뿐, 같은 게시물의 서로 다른 URL 을 이어붙이지는 못합니다. E2 의 세 행은 결제 전까지 세 행으로 남습니다. |
| 관측 이전의 사실 | 이미 삭제된 콘텐츠를 처음 볼 때 | 토큰이 링크를 건 시점과 우리가 관측한 시점 사이에 콘텐츠가 지워졌으면, 그 콘텐츠가 무엇이었는지는 복원할 수 없습니다. YouTube 표본에서 not_found 가 11/17 이었던 것이 이 경우입니다. |
| unknown 의 진짜 종류 | classifier 를 추가하기 전까지 | 398건 중 200건은 X 검색이라 classifier 만 붙이면 풀리지만, 나머지는 무엇인지 모릅니다. 안들이 하는 일은 "나중에 고칠 수 있게 자리를 비워두는 것"이지 스스로 판별하는 것이 아닙니다. |
| 병합·재연결의 원자성 | 병합 도중 프로세스가 죽을 때 | Phase 1.6 에서 트랜잭션 처리를 명시적으로 제외했습니다(be-system-design.md §6). 병합은 여러 컬렉션을 순차 수정하므로, 중간에 죽으면 엣지 일부만 재연결된 상태가 남습니다. 이걸 감지할 장치도 현재 설계에 없습니다. |
| append-only 와 사후 정정의 긴장 | 병합·재연결을 실행할 때마다 | 엣지는 관측 로그라 덮어쓰지 않는 것이 원칙입니다(content-token-link.model.ts:19). 재연결은 그 원칙을 깹니다. "그때 우리가 본 것은 단축 링크였다"는 사실을 남기려면 재연결 이력을 별도로 기록해야 하고, 그건 추가 설계입니다. |
| 도메인의 소유 주체 | 여러 토큰이 한 도메인을 공유할 때 | 스파인에 web:site:foo.com 행이 생기면 공유는 보입니다. 하지만 그 사이트가 누구 것인지는 여전히 모릅니다. 작성자도 발행 시각도 없어서 Creator 를 붙일 수 없다는 v3 의 판단(§1371)은 스파인을 도입해도 그대로 유효합니다. |
| 유실을 셀 수 없다는 문제의 잔여분 | 스파인 이전에 쌓인 데이터 | 스파인은 앞으로 들어오는 URL 을 전부 기록합니다. 지금까지 tokens.unclassified_links[] 로만 남은 것들은 원본 소셜 필드가 다른 레포에 있어서, 재수집하지 않는 한 복원되지 않습니다. |
이 절이 답하는 것 — 이 비교 뒤에도 사람이 골라야 하는 선택지.
매트릭스상 안 3 은 비객체 수용처가 없어 안 2 로 수렴하고, 안 1 은 시간을 표현하지 못해 재생 3 에서 조용히 틀립니다. 남는 실질적 선택지는 안 2 입니다. 반대로 안 2 를 택하지 않는다면, E1·E7·E8·E10 의 유실을 의도적으로 감수한다는 뜻이 됩니다. 그 경우 최소한 "무엇을 몇 건 버리는지"는 세어 둘 수 있어야 하는데, 지금 스키마로는 그것도 불가능하다는 점이 §1 의 결론입니다.
_id 를 정체성으로 쓰더라도, "이 URL 이 기존 행인가"를 판정할 인덱스는 여전히 필요합니다. 후보는 둘입니다.
(platform, subtype, url_key) — 모든 URL 이 즉시 행을 갖습니다. 대신 E2 처럼 같은 대상의 URL 이
여러 개면 행이 나뉘고, 나중에 platform_id 를 알게 되면 병합해야 합니다.(platform, subtype, platform_id) — 대상 동일성이 완벽합니다. 대신 platform_id 가
없는 URL 은 이 인덱스로 판정할 수 없어 별도 취급이 필요하고, 그 별도 취급이 결국 앞의 인덱스를 다시 요구합니다.
실무적으로는 두 인덱스를 모두 두고 — url_key 는 항상, platform_id 는 있을 때만
부분 인덱스로 — 병합 시점에 후자를 충돌 감지에 쓰는 형태가 가능합니다. 이 조합의 채택 여부가 결정 대상입니다.
§6 이 지적한 대로 병합·재연결은 append-only 원칙과 충돌합니다. "그때 본 것은 단축 링크였다"를 보존하려면 재연결 이력 컬렉션이나 tombstone 이 필요합니다. 이것을 지금 설계에 넣을지, 아니면 병합이 실제로 발생한 뒤에 넣을지가 결정 대상입니다. 후자를 택하면 첫 병합 이전의 이력은 남지 않습니다.
이 문서가 다루지 않은 것 — 최종 스키마의 필드 목록, 인덱스 설계 전체, 마이그레이션 계획, 그리고 draft 의 나머지 항목(공통 필드 최소화, Creator 재정의, 최적화용/공용/개별 필드의 문서 표기 구분). 모두 위 세 결정이 확정된 뒤에 이어집니다.