두 달 전, 인터넷 구석에서 하나의 저장소(repository)를 발견했다: 3,358개의 별점(stars), 744개의 포크(forks), M
요약
대규모 데이터 릴리스를 전체 다운로드 없이 HTTP range requests를 사용하여 효율적으로 탐색하고 분석하는 방법론을 소개합니다. 특정 오픈소스 저장소의 사례를 통해 데이터 편집(redaction)의 허점과 대용량 바이너리 파일을 다루는 기술적 트릭을 설명합니다.
핵심 포인트
- HTTP range requests를 활용해 대용량 파일을 부분적으로 읽는 효율적인 방법
- gzipped 상태의 샤드 파일을 압축 해제 없이 탐색하는 기술적 트릭
- Git에서 제외된 데이터가 대규모 바이너리 릴리스에 포함될 수 있는 보안/데이터 관리 이슈
두 달 전, 인터넷 구석에서 하나의 저장소(repository)를 발견했다: 3,358개의 별점(stars), 744개의 포크(forks), MIT 라이선스, 그리고 518,400개 학습 샘플로 광고된 릴리스 — 총 5.5 GB 크기로 세 개의 zip 파일로 나뉘어 있었다. 내가 활동하는 분야는 중국 점성술 소프트웨어인데, 기계가 읽을 수 있는 데이터가 거의 없는 영역이다. 따라서 이 정도 규모의 코퍼스(corpus)는 지난 몇 년간 발표된 것 중 가장 유용한 것이거나 아니면 아무것도 아닐 터였다. 나는 어느 쪽인지 알고 싶었다.
그러다 샘플 개수를 한 번 더 자세히 살펴보았다. 518,400 = 60 × 12 × 30 × 12 × 2. 육십 년, 열두 달, 삼십 일, 열두 개의 두 시간 간격, 그리고 두 성별. 이것은 누군가가 관찰한 것들의 단순 합계가 아니다. 이는 중첩 루프(nested loop)의 크기이다.
이것은 호기심을 갖기에 멋진 이유이자, 어떤 결론을 내리기에 끔찍한 이유였다. 그래서 나는 아카이브를 읽어보기로 했다 — HTTP range requests를 통해 48 MB만 가져왔고, 나머지 5.83 GB는 전혀 건드리지 않았다. 이 경험은 목적지보다 더 흥미로웠으며, 이 방법론은 나의 이상한 작은 분야를 넘어 훨씬 더 넓게 적용될 수 있는 부분이다.
자, 여기 몇 장의 사진 값으로 다중 기가바이트(multi-gigabyte) 릴리스를 읽는 방법을 알려주고, 그 데이터가 나에게 말해준 네 가지 내용을 소개하겠다.
첫째, 트릭: 5.5 GB 릴리스 전체를 다운로드하지 않고 읽기
이 특정 아카이브에는 보너스가 있었다. 이 아카이브는 이미 gzipped 상태였기 때문에, .jsonl.gz 샤드(shards)를 압축을 해제하지 않고 저장만 하는 방식인 method 0으로 저장했다. 모든 엔트리(entry)는 독립적으로 주소 지정이 가능하며, 끝부분에 있는 작은 텍스트 파일들은 압축되지 않은 상태로 놓여 있어, 단순한 curl -r 명령만으로도 읽을 수 있는 소스 코드를 얻을 수 있다.
# 1. 목차: 마지막 부분의 마지막 3 MB
curl -sL -r 1890639808-1893639807
".../ziwei-samples-v3-part3.zip.003" -o p3.tail
...
917개의 엔트리가 반환되었다. 그중 781개는 모두가 다운로드했던 바로 그 데이터였다. 나머지 136개가 바로 이야기가 반전된 지점이었다.
총 가져온 양: 47,897,905 bytes — 릴리스의 0.81%. 전체 스크립트는 부록에 있다.
발견 1: 릴리스에는 저장소(repo)가 제외했다고 명시한 파일이 포함되어 있다
공개 저장소는 lib/ziwei/db-analysis.ts를 2,124바이트 크기의 스텁(stub)으로 배포하며, 그 헤더 주석에는 왜 그렇게 했는지에 대해 매우 솔직하게 적혀 있다 (나의 번역):
분석 콘텐츠 라이브러리는 오픈 소스 범위에 포함되지 않습니다. 전체 온라인 버전에는 14개의 주요 별(star) × 13개의 궁(palace) 맥락에 대한 상세한 읽기 내용이 포함되어 있습니다 — 이는 차트 엔진과 함께 오픈 소스로 공개되지 않는 핵심 콘텐츠입니다.
그리고 맨 아래에는 다음과 같이 적혀 있다: export const STAR_DB: Record<string, unknown> = {};
하지만 릴리스 내부의 동일한 경로는 377,157 bytes이며, 161번 라인부터 STAR_DB가 채워져 있다: 524개의 문자열 리터럴(string literals), 145,580자의 수기로 작성된 중국어 산문이 들어 있다. Git에서 제외되었던 라이브러리는 어쨌든 릴리스에 포함되어 있었다 — 단지 아무도 찾아보지 않는 3부로 나뉜 분할 zip 파일 깊숙한 곳(5.87 GB)에 주차되어 있었을 뿐이다.
이것이 첫 번째이자 가장 범용적인 교훈이며, 점성술과는 아무런 상관이 없다: 편집(redaction)은 대규모 바이너리 아티팩트(binary artifacts)를 통해 유출된다. Git에서 주의 깊게 스텁(stub) 처리한 파일이라도 릴리스 타르볼(tarball), Docker 레이어, 모델 체크포인트(model checkpoint), 학습 데이터 덤프(training-data dump)에 즐겁게 함께 실려 나갈 수 있다. 아티팩트가 클수록 아무도 알아차리지 못할 가능성이 높지만, 이는 실제 위험이 작동하는 방식과는 정반대이다.
출처를 찾아낸 덕분에 이 데이터셋이 무엇인지도 확실히 알게 되었다. lib/ziwei/ 디렉토리 전체를 살펴보면 216,172자의 TypeScript 코드가 있으며, 그중 183,182자가 문자열 리터럴 (string literals) 내부에 존재한다. 생성기 (generator)는 해당 그리드 위에서 작동하는 5중 중첩 루프(five nested loops)로 구성되어 있으며, 경도(longitude)는 120으로 하드코딩되어 있다. 그리고 감사 (audit) 스크립트 자체의 샘플러 (sampler)를 제외하면, 어디에도 Math.random 호출이 없다. 출력값은 5개의 정수로 이루어진 키 (key)의 순수 함수 (pure function)이다.
이는 해당 릴리스 (release)가 메모 테이블 (memo table)임을 의미한다. 압축을 풀면 약 32.9 GB에 달하며 (샤드 (shard) 하나를 45,649,978 바이트로 측정했으며, 총 720개가 있음), 약 1.09 × 10¹⁰ 자의 생성된 산문 (prose)을 담고 있다. 이 모든 것은 183K 자의 소스 코드에서 조립된 것이며, 이 소스 코드는 전체를 다시 생성하는 npm run full 명령과 함께 동일한 zip 파일 안에 포함되어 있다.
내가 추출한 3,600개의 샘플 (코퍼스 (corpus)의 0.69%)에서 측정된 재사용률은 다음과 같다: 46,800개의 발행된 토픽 블록 (topic blocks) 중 **16,359개가 고유 (distinct)**하며, 이들 각각은 동일한 리터럴 (literals)의 순열 (permutation)이다. 이것으로 파인튜닝 (Fine-tuning)을 하는 것은 모델에게 특정 도메인 (domain)을 가르치는 것이 아니다. 단순히 엔진을 호출하면 될 것을, 템플릿 엔진 (template engine)을 가중치 (weights)로 나쁘게 압축하기 위해 GPU 비용을 지불하는 꼴이다.
| 릴리스 크기 (Release size) | 5.5 GB (3개 파트) |
| ... |
발견 2: 샘플 중 2,520개는 존재하지 않는 날짜이다
days: range(1, 30) — 모든 달에 대해 30일이며, 오직 30일만 존재한다.
이는 두 가지 결과를 초래한다. 모든 31일이 누락된다: 7개월 × 60년 × 12시간 × 2개 성별 = 데이터 행이 아예 없는 10,080개의 실제 생일. 그리고 2월은 연도와 상관없이 29일과 30일을 가지게 되어, 존재하지 않는 날짜에 키가 지정된 2,520개의 행이 남게 된다.
생성기가 불가능한 입력값에 대해 어떻게 작동하는지 확인하기 위해, 윤년이 아닌 1962년 2월을 추출해 보지 않을 수 없었다. 생성기는 실패하지 않는다. 월말을 그대로 지나쳐 계속 숫자를 세어 나간다:
| 그레고리력 입력 (Gregorian input) | 엔진이 할당한 음력 날짜 (lunar day the engine assigned) |
|---|---|
| 1962-02-27 | 1월 23일 |
| ... | |
| 따라서 1962-02-30으로 라벨링된 샘플은 3월 2일에 태어난 사람을 위한, 완전히 확신에 찬 63 KB 분량의 차트(chart)가 된다. 파이프라인(pipeline) 어디에서도 이를 알아채지 못했다. |
동일한 그리드(grid)에는 두 가지 형태적 기이함(shape quirks)이 더 존재한다. 연도 축은 range(1924, 1983)이며, 소스 주석에는 "60년은 육십갑자(sexagenary cycle) 한 주기를 포함한다"라고 설명되어 있다 — 이 구간(window)은 숫자가 딱 떨어지게 만들기 위해 선택된 것이지, 사용자의 범위를 포괄하기 위해 선택된 것이 아니다. 1983년 이후에 태어난 사람은 아무도 포함되어 있지 않으며, 이는 2026년 기준으로 이 코퍼스(corpus)가 43세 이상의 사람들에 대해서만 독점적으로 다루는 데이터임을 의미한다. 또한 경도(longitude)는 모든 행에서 120으로 고정되어 있는데, 이는 다소 뼈아픈 부분이다. 출생 시각이 가장 엔트로피(entropy)가 높은 입력값인 시스템에서, 가장 변화할 가치가 있는 단 하나의 변수가 상수로 고정되어 버렸기 때문이다.
발견 3: "518,400 / 518,400 검증됨"은 단순한 부분 문자열 확인(substring check)임이 드러남
릴리스(release)에는 자체적인 검증 및 감사 로그(validation and audit logs)가 포함되어 있으며, 겉보기에는 매우 훌륭해 보인다. 실패 제로, 경고 제로, 다음과 같은 줄들이 나타난다:
health contains 「liver/kidney/spleen-stomach」: 518400/518400 (100.00%)
health contains 「子午流注」 and 「經絡」: 518400/518400 (100.00%)
female health contains 「gynaecology/menses/pregnancy」: 259200/259200 (100.00%)
...
이 모든 항목은 String.includes 방식이다. 이는 템플릿(template)이 실행되었음을 입증할 뿐이다. 이 방식은 '존재함(present)'과 '정확함(correct)'을 구분할 수 없다. 그리고 여기서 그 구분은 매우 결정적인데, 왜냐하면 바로 동일한 로그의 다섯 줄 위에서 다음과 같은 결과가 나오기 때문이다:
daXians[0] contains siHua: 0/518400 (0.00%)
any daXian contains siHua: 0/518400 (0.00%)
daXians[0] contains stemIndex: 0/518400 (0.00%)
...
생성 에이전트(generating agent)에게 전달된 사양서(spec) — 아카이브에 포함된 README-CODEX.md — 는 해당 필드 중 하나를 "가장 틀리기 쉬운 단 한 가지 요소"라고 지칭하며, 이를 필수 요구 사항 #6으로 규정하고 생성 후 체크리스트를 통해 이를 검증하도록 명시하고 있습니다. 저는 배포된 기록을 직접 확인해 보았습니다. 각 daXians 항목에는 startAge, endAge, palaceBranch, palaceName이라는 정확히 네 개의 키(key)만 존재했습니다. 사양서가 그 자체를 중심으로 구축했던 필드는 그저 존재하지 않았습니다.
한편, 패키징 매니페스트(packaging manifest)는 **"Verified results"**라는 헤딩 아래 이 내용을 영어로 다음과 같이 나열하고 있습니다:
Verified results:
- Total samples: 518,400
- Validation failures: 0
...
상자 안에 그대로 남아 있는 이전 감사 보고서(audit report)를 보면, 샘플링된 20개 행 전체에 걸쳐 동일한 필드들이 실제 값으로 채워져 있는 것을 확인할 수 있습니다. 즉, 해당 실행 시점과 배포 시점 사이의 어딘가에서 가장 중요한 세 가지 계산된 필드(computed fields)가 누락되었고, 검증기(validator)는 그 부재를 '통과(pass)'로 판정했습니다. 이후 스코어카드(scorecard) 파일은 각 제약 조건에 대한 검증 명령이 존재한다는 명시된 근거를 바탕으로, "강제 제약 조건 구현 및 감사(hard-constraint implementation and audit)\
| 상태 | 개수 | 파일의 정의 |
|---|---|---|
| 검증됨 (verified) | 18 | 강의 녹취록에 등장함, 출처 확인됨 |
| ... | ||
| 인용된 출처의 19%가 추적되었습니다. 노트는 1인칭 시점으로 작성되었으며 매우 가차 없습니다 — "이 문장은 내가 지어낸 것이니, '북방 학파는 ...라고 주장한다'로 수정해야 함"; "'정밀한 월 도출법(precise month-derivation method)'은 내가 만든 명칭이지, 그의 용어가 아님"; "⚠️ 주요 오류: 그의 질병 판독 키(key)는 별의 요소(star element)가 아니라 궁(palace)의 분지에 기반함 — 이 매핑 전체가 시스템에서 벗어남." |
또한 top_priority_fixes 배열도 존재합니다. 하나의 P1(우선순위 1) 항목은 위에서 인용한 운명-궁(fortune-palace) 문장으로, 아마도 그의 말이 아니며 재작성이 필요하다고 표시되어 있습니다. 이 문장은 518,400개의 샘플 중 518,400개 전체에 나타나는 문장입니다. 수정 목록이 정작 그것을 차단(gate)했어야 할 아티팩트(artifact) 내부에 포함되어 배포된 것입니다. (생성된 산문 또한 스승의 이름을 오기하여 해당 문장과 수십 개의 다른 문장들에 출처를 부여하고 있습니다.)
그리고 이 글을 단순히 비판하기 위해서가 아니라, 기록할 가치가 있게 만드는 부분은 바로 여기입니다. 해당 주석(annotations) 파일은 이 분야의 거의 어떤 리포지토리(repo)보다도 출처(provenance)에 대한 철저한 조사를 담고 있으며, 유지 관리자(maintainer)가 배포 전 자신의 텍스트에 대해 직접 작성한 것입니다. 그 옆에는 수집된 자료를 4단계로 분류하고, 라이선스 없이 전문(full text)을 저장하는 것을 금지하며, rights_status=unknown 자료가 학습 코퍼스(trainable corpora)에 들어오는 것을 금지하는 권리 정책(rights policy)이 놓여 있습니다. 소스 레지스트리(source registry)에는 다음과 같이 기록되어 있습니다: 211개의 소스, 1개는 전문 사용 승인됨, 167개는 라이선스 필요, 15개는 전면 금지됨. 누군가가 이 모든 것에 대해 깊이 고민했다는 증거입니다.
따라서 실패의 원인은 근면함(diligence)의 부족이 아닙니다. 문제는 그 근면함이 게이트(gate)에 연결되어 있지 않았다는 점입니다. 6개의 조작된 인용문이 포함된 파일 목록이 있다고 해서 배포가 차단되지 않습니다. "알 수 없는 권한은 학습 코퍼스(training corpora)에 포함될 수 없다"라는 정책이 있어도, 해당 자료에서 파생된 코퍼스가 MIT 라이선스로 배포되는 것을 막지 못합니다. 특정 문자열을 검색(grep)하는 검증기(validator)는 내용이 틀렸다는 이유로 빌드(build)를 실패시킬 수 없습니다. 그 상자 안의 모든 신뢰 자산(artifact of trust)은 검증 대상인 것과 동일한 파이프라인에 의해 생성되었습니다. 에이전트가 데이터를 생성하고, 감사를 수행하고, 성공을 보고했으며, 이 세 단계 각각은 단순히 완료되었는지 여부로만 점수가 매겨졌습니다.
이것이 2026년형 실패 모드이며, 저는 이것이 드문 사례라고 생각하지 않습니다. 단순한 슬롭(slop, 저질 데이터)이 아닙니다. 통과하는 테스트 스위트(test suite)를 갖춘 슬롭입니다.
제가 현재 수행하는 세 가지 점검 사항
- 샘플 수(sample count)를 인수분해하십시오. 만약 N이 작은 정수 인수로 분해된다면, 당신은 무언가에 대한 관찰이 아니라 키 공간(key space)을 덮는 그리드(grid)를 보고 있는 것입니다. 관찰 단위(unit of observation)가 무엇인지 물으십시오. 만약 답이 "가능한 입력값"이라면, 학습할 신호(signal)가 없으며, 주장을 확인할 결과, 판결, 또는 인간 참여(human in the loop)도 존재하지 않습니다.
- 데이터를 보기 전에 생성기(generator)를 찾으십시오. 만약 생성기가 포함되어 있다면 — 아카이브의 뒷부분에서 매우 자주 발견되는 일입니다 — 생성기가 곧 데이터셋이며, 크기는 5자릿수(five orders of magnitude)만큼 더 작습니다. 그런 다음 생성기의 무작위성(randomness)을 확인하십시오. 무작위성이 없다는 것은 해당 배포물이 순수 함수(pure function)의 메모 테이블(memo table)임을 의미하며, 이 경우 그냥 함수를 호출하면 됩니다.
- 검증(validation) 결과가 아니라 검증기(validator)를 읽으십시오. 검증기가 실제로 무엇을 단언(assert)하는지 grep으로 확인하십시오. 부분 문자열 존재 여부, 필드 존재 여부, 파일 개수 확인은 형태 점검(shape checks)에 불과합니다. 이러한 점검은 올바른 출력물과 반대로 뒤집힌 출력물 모두에 대해 똑같이 열정적으로 통과를 선언합니다. 술어(predicate)를 확인하기 전까지 녹색 로그(green log)는 아무것도 알려주지 않습니다. 제 검증기는 계산된 값들 사이의 관계를 단언하며, 그것이 어느 정도의 가치가 있든 간에 저 역시 여전히 완전히 신뢰하지는 않습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기