Dan Blanchard가 Claude를 사용해 chardet v7을 처음부터 재작성한 과정, 기존 코드 접근 기록, 아키텍처 차이, 유사도 분석, 그리고 왜 파생 저작물이 아니라고 보는지에 대한 투명한 설명.
2026년 3월 29일·2026년 3월 31일 업데이트·30분·Dan Blanchard
나는 chardet를 12년 넘게 유지보수해 왔고, 버전 2.3 이후의 모든 릴리스를 거쳤다. 내가 Claude와 함께 이것을 처음부터 다시 작성하고 새 버전을 MIT 라이선스로 배포하자,1 사람들은 새 코드가 정말로 LGPL 원본과 독립적인지 의문을 제기했다.
그 논의의 대부분은 재작성 과정에서 무슨 일이 있었는지에 대한 가정에 기반하고 있었다. 이 글은 그런 가정을 원시 대화 기록의 증거로 대체한다.
요약: 나는 지난 10년 동안 표준 라이브러리 포함을 가로막아 온 LGPL 라이선스를 제거하고, 오래된 아키텍처의 성능 및 정확도 문제를 해결하기 위해 Claude와 함께 chardet를 처음부터 다시 작성했다. Claude는 학습 데이터에 chardet를 포함하고 있으며, 내 지시에도 불구하고 그 하위 에이전트들이 세 개의 세션에서 예전 소스 코드를 읽었다. 직접 노출된 모든 경우는 API 표면이거나 주로 내가 작성한 파일이었으며, 이 글에 대화 기록 증거와 함께 문서화되어 있다. 세 가지 독립적인 유사도 측정은 옛 코드와 새 코드 사이에 거의 0에 가까운 중복만을 발견했고, 아키텍처는 근본적으로 다르다. chardet 7은 파생 저작물이 아니다.
Mark Pilgrim은 원래 2006년에 Mozilla의 C++ 구현에서 chardet를 포팅했다. 그는 이후 인터넷에서 자신을 지운 지 5년 전이었다. 그가 프로젝트 저장소를 삭제했음에도 다른 사람들이 그가 멈춘 지점에서 작업을 이어갔고, 나는 2012년에 Ian Cordasco가 만든 charade라는 Python 3 포크를 당시 Python 2 전용 메인라인에 다시 병합한 뒤 유지보수를 맡았다. 그 이후로 나는 일곱 번의 메이저 릴리스에 걸쳐 수백 개의 커밋을 했고, 38개 언어와 수십 개 인코딩을 추가하고, 휴리스틱 프로버를 학습된 통계 모델로 교체하고, 감지 버그를 수정하고, detect_all() 및 인코딩 시대 필터링 같은 API 기능을 도입하고, 새로운 Python 릴리스마다 대응해 왔다.
Chardet는 월간 1억 3천만 건 이상의 다운로드를 기록하고 있어, Python 라이브러리 가운데 가장 널리 사용되는 것 중 하나다. 사용 범위가 매우 넓었기 때문에 2015년에는 Python 표준 라이브러리에 포함되는 방안이 검토되었다. Guido van Rossum은 표준 라이브러리 포함을 고려하면서 chardet의 의존성과 API 안정성에 대해 나에게 직접 물었지만, Mozilla의 원본 코드에서 이어받은 LGPL 라이선스가 걸림돌이라는 사실이 드러나면서 그 노력은 흐지부지되었다.2 나는 그때부터 계속 이 문제를 해결하고 싶었다.
오래된 아키텍처도 노후화가 뚜렷했다. chardet 5.2.0은 우리의 확장된 테스트 세트에서 초당 55개 파일 처리 기준 정확도 65%를 기록했다. 내가 배포한 6.0.0은 코퍼스 기반 학습 모델을 38개 새 언어에 추가하여 정확도를 88%까지 끌어올렸지만, 속도는 초당 11개 파일로 급락했다. 이유는 프로버 아키텍처의 확장성이 나빴기 때문이다. 새 언어를 추가할 때마다 입력의 모든 바이트에 대해 병렬로 실행되는 프로버 수가 늘어났다. 정확도를 높이려면 더 느려질 수밖에 없는 벽에 부딪힌 것이다.
초기 요구사항 목록으로 내가 휴대폰에 적어 둔 내용은 다음과 같다(Mark Pilgrim에게 남긴 내 댓글에서 그대로 옮김):
- 공개 API 호환성
- chardet를 대체할 계획이므로 이름은 여전히 chardet여야 함
- 어떤 GPL 또는 LGPL 코드에도 기반하지 않을 것
- 테스트 데이터에서 높은 chardet 감지 정확도
- 언어 감지는 필수 요구사항은 아니지만, 쉽거나 다른 설계의 부산물이라면 구현할 것
- 빠르고 메모리 효율적일 것: 여러 코어를 효율적으로 사용해야 함3
- 런타임 의존성 없음
- PyPy와 CPython에서 동작해야 함
- 깔끔하고 현대적인 설계
- 학습된 통계 모델을 사용할 경우 Hugging Face의 load dataset API를 통해 이용 가능한 데이터를 사용할 것
- 어떤 학습 코드든 개발 과정에서 자주 재학습할 수 있도록 데이터를 로컬에 캐시할 것
- 자주 벤치마크할 것
- 큰 dict 리터럴을 대량으로 사용하지 않을 것. 그런 것들은 CPython 3.12에서 import하는 데 매우 오래 걸리기 때문
초기 설계 문서와 전체 계획 커밋 기록은 공개되어 있다. 결과는 41배 성능 향상, 테스트 스위트에서 더 높은 정확도, 100개 대신 약 22개의 Python 파일, 그리고 MIT 라이선스였다.
Mark에게 남긴 그 댓글이 큰 흐름을 설명한다. 이 글은 거기서 더 나아가, Claude가 예전 코드베이스를 건드린 모든 순간에 대한 대화 기록 증거를 제시한다.
재작성 세션을 다루기 전에, 먼저 분명히 짚고 넘어가야 할 것이 있다. Claude의 학습 데이터에는 chardet 소스 코드가 포함되어 있다. 이것은 Claude가 세션 중 직접 접근한 것과는 별개의 문제이지만, 중요하다.
다음은 소스 코드를 제공하지 않은 상태에서, 기억만으로 chardet 내부 구성 요소 중 하나인 코딩 상태 머신을 설명한 Claude의 말이다.
상태 머신은 첫 번째 방어선이다. 이것은 어떤 바이트 시퀀스가 특정 인코딩에 대해 구조적으로 유효한지를 판단한다.
각 인코딩은 가능한 모든 바이트 값(0x00-0xFF)을 바이트 클래스로 매핑하는 class table과, 주어진 (current_state, byte_class)에서 다음 상태를 생성하는 state table을 가진 모델을 정의한다. 상태는
START(초기 상태이자 “다음 문자를 받을 준비 완료”),ME_ONE(바이트 1개 추가 필요),ME_TWO(바이트 2개 추가 필요),ME_THREE(GB18030 4바이트 시퀀스를 위해 3개 추가 필요),ITS_ME(완전한 유효 문자가 디코딩됨),ERROR(이 인코딩에서 유효하지 않은 바이트)이다.예를 들어 입력 바이트
0xC4 0xE3를 UTF-8 상태 머신에 통과시키면,0xC4는 2바이트 선행 바이트(110xxxxx)로 분류되어 START에서 ME_ONE으로 전이한다(“연속 바이트 1개를 기대 중”). 이어서0xE3는 3바이트 선행 바이트(1110xxxx)로 분류되지만, 머신은 연속 바이트(10xxxxxx)를 기대하는 ME_ONE 상태에 있으므로 ERROR로 전이한다. 따라서 UTF-8은 탈락한다. 한편 GB2312 상태 머신은0xC4를 유효한 GB 범위 바이트(class 4)로 분류해 START에서 ME_ONE으로 전이시키고,0xE3도 class 4로 분류하여 ME_ONE에서 ITS_ME로 전이시킨다. 즉 유효한 2바이트 문자(你)다. 그리고 Shift_JIS 상태 머신은0xC4를 유효한 단일 바이트 가타카나 문자(반각 범위 0xA1-0xDF)로 처리해 즉시 ITS_ME를 반환한 뒤,0xE3를 유효한 SJIS 선행 바이트로 처리해 ME_ONE으로 이동한다. 따라서 SJIS도 구조적으로는 살아남지만, 일본어 빈도 모델에 대한 분포 분석 점수는 매우 낮고, GB2312는 중국어 모델에 대한 분포 분석 점수가 높게 나온다.
이건 단순히 “Claude가 chardet의 존재를 안다”는 수준이 아니다. Claude는 두 개 테이블 기반 설계, 인코딩별 바이트 분류, 구조적으로 유효한 인코딩들 사이의 동률을 깨기 위한 분포 분석까지 재현했다.
Claude는 거의 모든 것을 알고 있었다. 프로버 계층 구조, 상태 머신, BOM 감지, 빈도 분석까지.
하지만 완벽하게는 아니었다. 나는 Claude에게 웹 검색이나 외부 명령 없이, 가능한 한 자세하게 chardet의 아키텍처를 설명해 보라고 했다. 원한다면 전체 재현 내용을 읽어볼 수 있다.
내가 그것을 chardet 5.2.0과 대조해 보니, 오류가 있었다. 프로버 클래스 계층을 잘못 설명했고, 몇몇 프로버를 완전히 빠뜨렸으며(MacRomanProber, CP949Prober, JOHABProber), UTF-8 confidence 공식도 틀렸다.
또 이름 없는 것들에 대해 그럴듯한 이름을 지어내기도 했다. 상태 머신에는 START, ERROR, ITS_ME라는 세 개의 이름 붙은 상수만 있지만, Claude는 코드에서 단지 정수로만 존재하는 중간 상태들에 ME_ONE, ME_TWO, ME_THREE라는 이름을 만들어 냈다. 그 상태들이 의미가 있다는 점은 이해했지만, 소스에 없는 라벨을 붙인 것이다.
즉, 아키텍처에 대한 이해는 매우 깊었지만, 구현 세부사항에는 빈틈이 있었다. 소스 코드를 신뢰할 수 있을 정도로 그대로 복원한 것은 아니지만, 피상적인 친숙함을 훨씬 넘어서는 수준이었다.
나는 Claude를 사용하면 실제로 끝낼 수 있을 만큼 충분히 빠를 것이라고 알았기 때문에 사용했다. 나는 전업 일을 하면서 남는 시간에 chardet를 유지보수하고 있고, 메이저 릴리스 사이의 간격은 역사적으로 몇 년 단위였다. 혼자서 바닥부터 다시 쓰려면 낙관적으로 봐도 몇 달은 걸렸을 것이다. 나는 약 6개월 동안 대부분의 코딩에 Claude를 사용해 왔고, Claude와 함께하자 재작성은 5일이 걸렸다.
또한 겉보기에 대한 문제에도 도움이 되었다. 나는 10년 넘게 예전 구현을 머릿속에 담고 있었고, 아무리 다르게 만들어도 내가 직접 친 코드가 정말 독립적인지 사람들은 의문을 가질 것이라는 점을 알고 있었다. Claude가 내 명세로부터 코드를 생성하게 하면, 구현 선택이 예전 코드베이스에 대한 내 손의 기억에서 나오지는 않는다. 물론 이것만으로 법적으로 깨끗해지는 것은 아니다. Claude 역시 학습 데이터에 예전 코드를 가지고 있기 때문이다. 하지만 적어도 그 코드는 내가 10년 동안 프로버 계층 구조와 상태 머신 테이블을 들여다본 기억에 의해 빚어진 것은 아니었다.
나는 이 과정을 클린룸 재구현에 가깝게 만들려고 했지만, 실제 클린룸은 아니고 근사치였다.
전통적인 클린룸 방식에서는 엄격한 정보 차단막 뒤에서 완전히 분리된 두 팀이 일한다. 한 팀은 소스를 읽고 명세를 작성하고, 다른 팀은 원본을 전혀 보지 않은 채 그 명세만으로 구현한다.
이 경우 나는 옛 코드를 알고 있는 명세 작성자였고, Claude는 빈 저장소에서 시작하는 구현자였다. 역할은 분리되어 있었지만, 명세 작성자와 구현자 모두 적어도 어느 정도는 원본 코드에 대한 잠재적 지식을 갖고 있었다.
내가 Claude에게 준 지시는 분명했다. LGPL 또는 GPL 소스 코드를 사용하지 말 것, 기존 chardet 코드를 보지 말 것, 빈 저장소에서 시작할 것. LLM 암기 연구에 따르면 이런 지시는 학습 데이터 재현 가능성을 줄이기는 하지만 제거하지는 않는다. 이는 강한 보장은 아니었지만, 노골적인 복사를 막아주기를 바랐다. 그리고 뒤에서 보겠지만, 실제로 그렇게 되었다.
나는 재작성 저장소에서 Claude Code에 제한적인 권한을 설정했다. 사전 승인된 것은 git 명령뿐이었다.
{
"permissions": {
"allow": ["Bash(git:*)"]
}
}
이제 증거로 넘어가 보자. 내 지시에도 불구하고, 재작성 중 예전 chardet 코드가 Claude의 활성 컨텍스트에 들어온 모든 순간들이다.
활성 컨텍스트는 학습 데이터와 다르다. 학습 데이터는 모델 학습 과정에서 수백만 개의 다른 파일과 섞이며, 모델은 거기서 정확한 코드를 안정적으로 재구성할 수 없다. 위의 재현 오류들이 바로 그 점을 보여 준다. 반면 활성 컨텍스트는 모델이 코드를 생성하는 바로 그 프롬프트 창 안에 원문 소스 텍스트가 그대로 들어와 있는 상태다. 따라서 축어적 재현 가능성이 훨씬 커진다. 그래서 접근 기록이 중요하다. 더 위험한 노출을 추적하기 때문이다. 다행히 가장 큰 사례는 아래에서 보듯 전체 감지 엔진이 이미 다 작성되고 커밋된 뒤에 일어났다.
다음은 재작성 중 Claude가 예전 chardet 소스 코드에 접근한 모든 경우다. 무엇에 접근했는지, 누가 시작했는지, 대화 기록 인용, 그리고 새 코드베이스에 무엇이 반영되었는지를 정리했다. 더 많은 로그를 보고 싶다면 chardet 재작성 타임라인에서 모든 세션을 살펴볼 수 있다.
Claude의 브레인스토밍 하위 에이전트는 /Users/danblanchard/repos/chardet/에 있는 예전 chardet 저장소를 탐색하려고 모든 파일을 glob으로 찾으려 했다.
Claude 하위 에이전트 시도:
Glob("**/*", path="/Users/danblanchard/repos/chardet")
나는 권한 요청을 거부했고 Claude의 작업 전체를 중단했다.
시스템: “The user doesn’t want to proceed with this tool use. The tool use was rejected.”
시스템: “[Request interrupted by user for tool use]”
그 하위 에이전트는 다른 방법을 시도해 저장소 디렉터리 목록을 보려 했다.
Claude 하위 에이전트 시도:
ls -la /Users/danblanchard/repos/chardet/시스템: “Permission for this tool use was denied.”
나는 첫날 예전 코드를 보려는 모든 시도를 막았고, 빈 저장소를 내 로컬 chardet 체크아웃과 상위 디렉터리를 공유하지 않는 다른 폴더로 옮긴 뒤에는 Claude가 그런 방식으로 찾으려는 시도를 멈추는 것을 확인했다.
나는 Claude에게 charsets.py를 가져오라고 했다. 이 파일은 6.0.0 릴리스를 위해 내가 작성한 것으로, 재작성판의 인코딩 시대 분류가 chardet 6.0.0과 일치하게 만들기 위한 것이었다.
Dan: “I want the EncodingEra classification of each encoding to match the existing chardet 6.0.0 ones”
Claude가 사용한 것: 어떤 인코딩이 어떤 시대(MODERN_WEB, LEGACY_REGIONAL 등)에 속하는지. 이것은 내가 6.0.0에서 내린 분류 결정이지, Mozilla 시절 코드에서 온 데이터가 아니다.
이 역시 전적으로 내 지시에 따른 것이었다. 나는 학습 데이터 파이프라인을 개선하기 위해 예전 chardet 저장소의 두 파일을 보라고 Claude에게 명시적으로 말했다.
create_language_model.py에 대해(31개 커밋, 전부 내가 작성):
Dan: “The existing chardet create_language_model.py handles caching, normalization, and substitutions for legacy encodings very well and I wrote all of that code, so I am comfortable with you looking at it for inspiration”
metadata/languages.py에 대해(17개 커밋 중 14개가 내 것, 나머지 3개는 매핑 데이터가 아니라 오타와 타입 주석 수정):
Dan: “The chardet languages.py data file I compiled for the original chardet has an excellent mapping of encodings to languages would be helpful for this task. Do not copy the code from that, as we are avoiding copying any code directly from chardet since it is LGPL licensed, but the data that file encodes is relevant”
Claude가 이 파일들에서 사용한 것: 내가 조사하고 정리한 인코딩-언어 매핑, 레거시 인코딩을 위한 Unicode 문자 치환 테이블, 그리고 Windows-1258/Unicode 명세에 따라 정해지는 베트남어 분해 맵.
치환 데이터는 따로 짚고 넘어갈 만하다. 특정 Unicode 문자 쌍은 예전 코드와 새 코드에서 동일한데, 이는 레거시 인코딩에서의 문자 등가성에 대한 사실 정보이기 때문이다. 예를 들어 U+201C 왼쪽 스마트 따옴표를 지원하지 않는 인코딩에서 이를 대체해야 한다면, 정확한 대체는 ASCII 큰따옴표 " 하나뿐이다.
코드의 구조 는 완전히 다르다. 모듈 수준 상수 dict와 단일 함수 내부의 if/elif 분기로 나뉘지만, 문자 쌍 자체는 같은 사실을 반영하기 때문에 동일하다.
나는 Claude에게 비교 스크립트를 수정해서 예전 chardet 버전들과도 벤치마크할 수 있게 해 달라고 요청했다.
Dan: “Update @scripts/compare_detectors.py so that it can also compare against specific versions of chardet, and then run the comparison versus chardet 6.0.0 and chardet 5.2.0”
Claude는 함수 시그니처를 확인하기 위해 5.2.0과 6.0.0 태그의 chardet/__init__.py를 읽었다. 확인한 내용은 다음과 같다.
Claude: “chardet 5.x uses
detect(byte_str, should_rename_legacy=False)with noencoding_era. Now let me also check chardet 6.0.0’s API.”
추출된 것은 파라미터 이름과 타입뿐이었다. 예를 들면 5.x에 encoding_era가 존재하는지, should_rename_legacy의 기본값이 무엇인지 같은 것들이다.
universaldetector.py 읽기 (세션 c9716c23, 2월 27일)#이것이 가장 큰 노출 사건이다. 전체 감지 엔진은 이미 이틀 전에 작성되어 커밋된 상태였고,4 이 세션의 목적은 chardet 6.0.0의 should_rename_legacy, ignore_threshold, lang_filter, chunk_size 파라미터에 대한 하위 호환 API 래퍼를 만드는 것이었다. 나는 이를 위해 Claude에게 예전 소스를 읽으라고 지시하지 않았다. 하위 에이전트들이 파라미터의 동작 방식을 확인해야 한다고 스스로 판단한 것이다.
한 하위 에이전트는 ReadTheDocs에서 chardet 문서를 가져왔다.
Claude 하위 에이전트 프롬프트: “Extract what each of these parameters does: should_rename_legacy, ignore_threshold, lang_filter/LanguageFilter, chunk_size. What behavior do they control?”
또 다른 하위 에이전트는 구현을 조사하도록 파견되었다.
Claude 하위 에이전트 프롬프트: “Research chardet 6.x compat params”
하위 에이전트들은 GitHub MCP 도구를 통해 6.0.0 태그의 universaldetector.py와 __init__.py를 전체적으로 읽었다. 추출한 것은 다음과 같다.
MINIMUM_THRESHOLD = 0.20 (UniversalDetector의 클래스 속성)LEGACY_MAP 딕셔너리 항목들(인코딩 이름 리매핑)should_rename_legacy의 센티널 로직과 기본 동작Claude: “The research shows it’s actually
MINIMUM_THRESHOLD(notMINIMUM_CONFIDENCE) in chardet 6.0.0. It’s a class attribute onUniversalDetectorwith value0.20. It IS referenced externally:detect_all()accesses it asdetector.MINIMUM_THRESHOLD. chardet 6.0.0 also exposesLEGACY_MAPas a class attribute onUniversalDetector.”
솔직한 단서: universaldetector.py 전체 내용(약 567줄)은 Claude가 코드를 생성하는 동안 컨텍스트 창 안에 들어와 있었다. 이 파일에는 Mozilla 기원의 아키텍처 패턴이 담겨 있다. 프로버 디스패치 시스템, 입력 상태 머신, BOM 감지 테이블, 프로버 confidence 비교 로직 등이 그것이다. 내가 피하려고 했던 것이 정확히 이런 노출이며, 나는 이를 축소해서 말하지 않겠다. 다만 내가 지적할 수 있는 것은 chardet 7의 아키텍처가 그 파일 내용과 근본적으로 다르다는 점, 그리고 이 세션에서 나온 API 호환 작업 역시 이후 몇 주 동안 수정이 필요한 버그를 포함하고 있었다는 점이다(#337, #341).
저작자 메모: 6.0.0 시점의 universaldetector.py는 git blame 기준으로 주로 내가 작성한 코드다(567줄 중 510줄). Mark Pilgrim은 32줄, 나머지 25줄은 다른 기여자들 것이다.
“Claude가 본 모든 것”이라는 제목의 글인 만큼 빠짐없이 적어 두겠다. 다음은 대화 기록에 문서화된 비소스코드 상호작용들이다.
chardet.detect()를 실행하고 출력만 관찰했다. API 수준 테스트였고, 소스를 읽지 않았다.chardet.readthedocs.io를 가져왔다.이것들은 모두 일반적인 개발 관행이다. 다만 여기서는 완전성이 중요하기 때문에 포함했다.
위의 모든 사례에도 불구하고, 재작성 전체 과정에서 한 번도 접근되지 않은 것들은 다음과 같다.
latin1prober.py, sbcharsetprober.py, charsetprober.py, charsetgroupprober.py, mbcharsetprober.py 등 나머지 전부)codingstatemachine.py, mbcssm.py, escsm.py)chardistribution.py, jpcntx.py, 그리고 모든 *freq.py 파일)charset_name 문자열을 grep한 경우 제외)escprober.py)재작성 중 핵심 감지 코드 자체는 Claude의 활성 컨텍스트에 들어온 적이 없다. Claude는 학습 데이터로부터 이 파일들의 아키텍처 는 알고 있었지만(위에서 문서화한 대로), 실제 소스 코드는 코드를 생성하는 동안 모델 앞에 놓인 적이 없었다.
이 과정은 흠 없이 깨끗하지 않았다. Claude의 하위 에이전트들이 스스로 예전 코드에 손을 뻗지 못하도록 더 엄격한 가드레일을 설치했어야 했다. 그렇게 하지 않은 이유는 재작성에 집중하느라 그런 일이 일어날 거라 예상하지 못했기 때문이다.
어떤 사실 데이터는 예전 것과 새 것이 동일하다. 문자 치환 쌍(인코딩 표준이 정함), charsets.py의 인코딩 시대 분류, 그리고 languages.py의 인코딩-언어 매핑이 그렇다. 이 세 가지 원본 파일은 모두 내 코드다. create_language_model.py는 31개 커밋 전부 내 것이고, charsets.py는 9개 커밋 전부 내 것이며, languages.py는 17개 중 14개 커밋이 내 것이다. 나머지 3개는 오타와 타입 주석 수정이다. 나는 이 파일들의 코드에 대한 저작권을 보유하고 있으며, 원한다면 어떤 방식으로든 라이선스를 부여할 수 있다.
다음은 아키텍처를 나란히 놓은 것이다(전체 흐름도: 5.2.0, 6.0.0, 7.0.0):
| Component | chardet 5.2.0 | chardet 6.0.0 | chardet 7.0.0 |
|---|---|---|---|
| Architecture | 프로버 계층 구조(CharSetProber 하위 클래스) | 동일, EncodingEra 필터링 추가 | 12단계 순차 파이프라인 |
| UTF-8 detection | CodingStateMachine + 확률적 confidence | 동일 | 직접 바이트 범위 검증 + 비율 기반 confidence |
| CJK detection | 상태 머신 + 빈도 분포 테이블 | 동일 | 바이트 유효성 필터링 + 구조적 쌍 분석 + bigram 코사인 유사도 |
| Single-byte scoring | 프로버별 64x64 선행행렬 + SequenceLikelihood | 코퍼스 학습 모델(동일 점수 구조) + 휴리스틱 동점 깨기 | 256x256 IDF 가중 bigram 모델에 대한 코사인 유사도 |
| Disambiguation | 하드코딩된 ISO_WIN_MAP + 전용 Hebrew 3-프로버 시스템 | 동일, Mac/Windows/ISO 혼동용 휴리스틱 규칙 7개 추가 | 사전 계산된 구분 바이트 맵 + Unicode 범주 투표 |
| Binary detection | 없음(임계값 초과 프로버가 없으면 암묵적으로 처리) | null 바이트 비율 임계값 | 제어 문자 임계값 + 매직 넘버 식별(약 80개 형식) |
| Language detection | 암묵적(프로버 선택의 부작용) | 동일 | 명시적 3단계 시스템(단일 인코딩 조회, bigram 점수화, decode+score) |
| Package source files | 42 + 생성된 언어 모델 7개 | 39 + 생성된 언어 모델 45개 | 23(모델은 바이너리 .bin) |
| Lines of Python | 약 8,700 + 생성된 약 32,600 | 약 10,600 + 생성된 약 282,800 | 약 4,000 |
위 표는 이 버전들이 얼마나 다른지를 과소평가한다. 옛 코드의 프로버는 상태를 가진 객체였다. 각 프로버는 자신의 CodingStateMachine을 유지하고, feed() 호출로 바이트를 흘려보내며, 청크 전반에 걸친 실행 중 confidence 점수를 추적했다. 그룹 프로버는 모든 하위 프로버를 병렬로 실행하고 가장 높은 confidence를 선택했다. 7.0.0은 이를 상태 없는 파이프라인 함수들로 대체했고, 각 함수는 결정적인 결과를 반환하거나 다음 단계로 넘길 후보 집합을 걸러낸다.
점수화 역시 근본적으로 바뀌었다. 옛 단일 바이트 프로버는 바이트를 문자 빈도 순위로 매핑한 뒤, bigram을 4개의 가능성 버킷(NEGATIVE, UNLIKELY, LIKELY, POSITIVE)에 대해 점수화했다. 7.0.0은 원시 바이트 쌍을 256x256 조밀 lookup table에 대해 IDF 가중치와 코사인 유사도로 점수화한다. 옛 멀티바이트 프로버는 상태 머신으로 바이트를 하나씩 따라가며 유효/무효 전이를 추적했다. 7.0.0은 Python의 decode()를 strict 모드로 호출해 구조를 검증한 다음, 바이트 쌍 통계로 점수화한다.
모호성 해소 메커니즘도 다르다. 5.x에는 ISO_WIN_MAP(하드코딩된 dict)과 3-프로버 Hebrew 시스템이 있었다. 6.x는 여기에 휴리스틱 규칙 7개를 추가했다. 7.0.0은 이 모든 것을 사전 계산된 구분 바이트 맵과 Unicode 범주 투표로 대체했다. 이는 특수 사례 코드 없이 어떤 혼동 쌍도 처리할 수 있는 데이터 기반 시스템이다.
나는 chardet의 19개 버전에 걸쳐 git blame을 실행하며, Mark Pilgrim에게 귀속되는 줄 수를 추적했다. 옛 버전들에는 아무 플래그 없는 일반 git blame을 사용했는데, 이는 오히려 Pilgrim의 기여를 과소집계 할 가능성이 있다. 파일 간 이동된 코드를 따라가지 않기 때문이다.
버전 1.0에서는 7,188줄 중 7,188줄, 즉 100%였다. Ian Cordasco의 charade Python 3 포크, 이를 다시 메인라인에 병합한 Python 2/3 호환화, 그리고 수년간의 유지보수를 거치며 Pilgrim의 줄 수는 점진적으로 줄었다. charade 1.0.0에서는 2,999줄, chardet 3.0.0에서는 1,282줄, 6.0.0에서는 1,207줄이었다. 하지만 그 줄들은 20년 동안 모든 릴리스에 남아 있었다.
chardet 7.0.0에는 Mark Pilgrim에게 귀속되는 줄이 0줄이다. 심지어 git blame -C -C -C처럼 Git이 제공하는 가장 공격적인 복사 감지 옵션을 사용해도 아무것도 찾지 못했다. 3,931줄 전부가 나에게 귀속된다.
JPlag v6.3.0은 포맷과 변수명을 제거한 뒤 토큰 수준에서 코드를 비교하는, 학계에서 널리 쓰이는 표절 탐지 도구다. 나는 모든 연속 버전 쌍과 하나의 양끝 비교(1.0 vs 7.0.0)에 대해 이를 실행했다.
| Version Pair | Avg Similarity | Max Similarity |
|---|---|---|
| 1.0 vs 1.0.1 | 97.63% | 98.45% |
| 1.0.1 vs 1.1 | 93.84% | 94.08% |
| 1.1 vs unreleased-2.0.1 | 93.87% | 94.08% |
| unreleased-2.0.1 vs 2.1.1 | 93.87% | 94.08% |
| 2.1.1 vs charade-1.0.0 | 81.82% | 83.02% |
| charade-1.0.0 vs charade-1.0.1 | 96.40% | 96.40% |
| charade-1.0.1 vs charade-1.0.2 | 95.54% | 95.83% |
| charade-1.0.2 vs charade-1.0.3 | 96.95% | 96.95% |
| charade-1.0.3 vs 2.2.0 | 94.97% | 96.69% |
| 2.2.0 vs 2.2.1 | 96.72% | 96.76% |
| 2.2.1 vs 2.3.0 | 94.82% | 95.75% |
| 2.3.0 vs 3.0.0 | 36.51% | 44.49% |
| 3.0.0 vs 4.0.0 | 82.99% | 94.09% |
| 4.0.0 vs 5.0.0 | 87.41% | 91.99% |
| 5.0.0 vs 5.1.0 | 90.97% | 93.83% |
| 5.1.0 vs 5.2.0 | 99.08% | 99.11% |
| 5.2.0 vs 6.0.0 | 3.30% | 80.05% |
| 6.0.0 vs 7.0.0 | 0.04% | 1.30% |
| 1.0 vs 7.0.0 | 0.50% | 0.65% |
패턴은 분명하다. 연속된 옛 버전들은 81~99% 유사도에 모여 있다. 심지어 3.0.0 vs 2.3.0도 36%까지밖에 떨어지지 않았는데, 이 릴리스는 이름 규약을 표준화하고(enum 대문자화, Python식 속성명), 프로버 반환값에 언어를 추가하고, Turkish 감지 지원을 더한 정도였다.
6.0.0 vs 5.2.0은 흥미롭다. 내가 추가한 학습 인프라 때문에 평균은 3.3%까지 떨어졌지만, 최대값은 80%에 머물렀다. 감지 코드는 변함없이 이어졌기 때문이다. 여전히 파생물이고, 여전히 LGPL이다.
6.0.0 vs 7.0.0: 평균 0.04%, 최대 1.30%. JPlag는 변수명과 포맷을 제거하므로 이름만 바꾼 복사도 잡아낸다. 그런데 총 47토큰에 해당하는 정확히 세 개의 일치만 발견했다. 다음이 그 전부다.
일치 1 (18토큰): 7.0.0의 pipeline/orchestrator.py에 있는 from X import (Y, Z) 구문 블록이, 6.0.0의 sbcsgroupprober.py에 있는 다른 from X import (Y, Z) 블록과 매칭되었다. 가져오는 대상은 완전히 다르다. 단지 import 문법일 뿐이다.
# 7.0.0/pipeline/orchestrator.py lines 3-36
from __future__ import annotations
from chardet._utils import DEFAULT_MAX_BYTES
from chardet.enums import EncodingEra
from chardet.models import (
BigramProfile,
has_model_variants,
infer_language,
score_best_language,
)
from chardet.pipeline import (
DETERMINISTIC_CONFIDENCE,
HIGH_BYTES,
DetectionResult,
PipelineContext,
)
# 6.0.0/sbcsgroupprober.py lines 235-249
from .langpolishmodel import (
CP852_POLISH_MODEL,
ISO_8859_2_POLISH_MODEL,
ISO_8859_16_POLISH_MODEL,
MACLATIN2_POLISH_MODEL,
WINDOWS_1250_POLISH_MODEL,
)
from .langportuguesemodel import (
CP037_PORTUGUESE_MODEL,
CP500_PORTUGUESE_MODEL,
CP850_PORTUGUESE_MODEL,
CP858_PORTUGUESE_MODEL,
CP860_PORTUGUESE_MODEL,
ISO_8859_1_PORTUGUESE_MODEL,
)
일치 2 (15토큰): argparse CLI 설정. 두 파일 모두 명령줄 도구를 구현하므로 argparse.ArgumentParser()와 add_argument()를 호출한다.
# 7.0.0/cli.py lines 25-35
def main(argv: list[str] | None = None) -> None:
parser = argparse.ArgumentParser(description="Detect character encoding of files.")
parser.add_argument("files", nargs="*", help="Files to detect encoding of")
parser.add_argument(
"--minimal", action="store_true", help="Output only the encoding name"
)
# 6.0.0/cli/chardetect.py lines 64-83
def main(argv: Optional[list[str]] = None) -> None:
parser = argparse.ArgumentParser(
description=(
"Takes one or more file paths and reports their detected encodings"
)
)
parser.add_argument(
"input",
help="File whose encoding we would like to determine. (default: stdin)",
type=argparse.FileType("rb"),
nargs="*",
default=[sys.stdin.buffer],
)
일치 3 (14토큰): 7.0.0의 registry.py에 있는 두 글자 언어 코드 튜플이, 6.0.0의 utf1632prober.py에서 불리언 속성 초기화 연속 구문과 매칭되었다. JPlag는 짧은 값들이 쉼표로 나열된 토큰 패턴을 비슷하다고 본 것이다. 실제 코드는 공통점이 없다.
# 7.0.0/registry.py lines 12-33
_WESTERN = (
"en", "fr", "de", "es", "pt", "it", "nl",
"da", "sv", "no", "fi", "is", "id", "ms",
)
# 6.0.0/utf1632prober.py lines 66-72
self.invalid_utf16be = False
self.invalid_utf16le = False
self.invalid_utf32be = False
self.invalid_utf32le = False
self.first_half_surrogate_pair_detected_16be = False
self.first_half_surrogate_pair_detected_16le = False
self.quad = [0, 0, 0, 0]
그게 전부다. 이것이 chardet 6.0.0의 293,000줄과 chardet 7.0.0의 4,000줄 사이의 전체 중복이다.
양끝 비교도 같은 이야기를 한다. 1.0 vs 7.0.0: 평균 0.50%, 최대 0.65%, 그리고 15토큰짜리 일치 하나뿐이다.
Copydetect v0.5.0는 Stanford의 MOSS가 사용하는 것과 같은 지문 알고리즘인 winnowing을 사용한다. JPlag가 프로젝트 수준 유사도를 보여 준다면, Copydetect는 이를 파일별로 분해해 어떤 파일이 무엇과 가장 비슷한지 보여 준다.
| Version Pair | Avg | Max | Top 5 Most Similar Files |
|---|---|---|---|
| 1.0 vs 1.0.1 | 100.00% | 100.00% | charsetprober.py 100%; langhebrewmodel.py 100%; chardistribution.py 100%; euctwprober.py 100%; euckrfreq.py 100% |
| 1.0.1 vs 1.1 | 96.29% | 100.00% | sjisprober.py 100%; langhebrewmodel.py 100%; mbcsgroupprober.py 100%; euckrfreq.py 100%; eucjpprober.py 100% |
| 1.1 vs unreleased-2.0.1 | 99.04% | 100.00% | escprober.py 100%; mbcharsetprober.py 100%; langgreekmodel.py 100%; euctwprober.py 100%; hebrewprober.py 100% |
| unreleased-2.0.1 vs 2.1.1 | 96.30% | 100.00% | charsetgroupprober.py 100%; sjisprober.py 100%; langgreekmodel.py 100%; mbcssm.py 100%; gb2312prober.py 100% |
| 2.1.1 vs charade-1.0.0 | 88.63% | 100.00% | euckrfreq.py 100%; big5freq.py 100%; jisfreq.py 100%; gb2312freq.py 100%; euctwfreq.py 100% |
| charade-1.0.0 vs charade-1.0.1 | 100.00% | 100.00% | sbcsgroupprober.py 100%; mbcsgroupprober.py 100%; langgreekmodel.py 100%; jisfreq.py 100%; mbcharsetprober.py 100% |
| charade-1.0.1 vs charade-1.0.2 | 97.14% | 100.00% | sbcharsetprober.py 100%; charsetprober.py 100%; jpcntx.py 100%; big5freq.py 100%; langthaimodel.py 100% |
| charade-1.0.2 vs charade-1.0.3 | 100.00% | 100.00% | compat.py 100%; langgreekmodel.py 100%; euctwprober.py 100%; langthaimodel.py 100%; utf8prober.py 100% |
| charade-1.0.3 vs 2.2.0 | 98.62% | 100.00% | langbulgarianmodel.py 100%; langhebrewmodel.py 100%; mbcharsetprober.py 100%; langthaimodel.py 100%; eucjpprober.py 100% |
| 2.2.0 vs 2.2.1 | 99.81% | 100.00% | langbulgarianmodel.py 100%; jisfreq.py 100%; langhebrewmodel.py 100%; chardistribution.py 100%; langgreekmodel.py 100% |
| 2.2.1 vs 2.3.0 | 98.30% | 100.00% | charsetprober.py 100%; euctwfreq.py 100%; langthaimodel.py 100%; codingstatemachine.py 100%; euckrprober.py 100% |
| 2.3.0 vs 3.0.0 | 71.78% | 99.94% | langthaimodel.py 99.94%; langhebrewmodel.py 99.94%; langgreekmodel.py 99.90%; langbulgarianmodel.py 99.90%; langhungarianmodel.py 99.90% |
| 3.0.0 vs 4.0.0 | 82.70% | 100.00% | mbcssm.py 100%; big5freq.py 100%; hebrewprober.py 100%; euckrprober.py 100%; euctwprober.py 100% |
| 4.0.0 vs 5.0.0 | 84.44% | 100.00% | gb2312freq.py 100%; euctwfreq.py 100%; big5freq.py 100%; euckrfreq.py 100%; jisfreq.py 100% |
| 5.0.0 vs 5.1.0 | 85.15% | 100.00% | langbulgarianmodel.py 100%; langgreekmodel.py 100%; langhungarianmodel.py 100%; gb2312freq.py 100%; euctwfreq.py 100% |
| 5.1.0 vs 5.2.0 | 93.75% | 100.00% | mbcssm.py 100%; langhebrewmodel.py 100%; euctwprober.py 100%; escprober.py 100%; euckrprober.py 100% |
| 5.2.0 vs 6.0.0 | 67.35% | 100.00% | utf8prober.py 100%; hebrewprober.py 100%; jisfreq.py 100%; sjisprober.py 100%; euctwprober.py 100% |
| 6.0.0 vs 7.0.0 | 3.30% | 59.21% | __main__.py 59%; languages.py 25%; universaldetector.py 10%; utf1632prober.py 10%; __init__.py 9% |
| 1.0 vs 7.0.0 | 0.00% | 0.00% | 25토큰 임계값을 넘는 일치 없음 |
상위 5개 열이 핵심을 말해 준다. 옛 버전들에서 가장 비슷한 파일은 핵심 감지 코드다. 프로버 파일, 언어 모델, 상태 머신, 빈도 테이블들이다. 바로 인코딩 감지를 실제로 수행하는 파일들로, 릴리스마다 이어져 온 것들이다.
반면 7.0.0 행에서는 완전히 다른 그림이 나온다. 최상위 일치는 __main__.py 59%인데, 이것은 어떤 Python 패키지에도 있는 세 줄짜리 CLI 진입점이다(from X import main; if __name__: main()).
languages.py vs __init__.py의 25% 일치는 공통된 @dataclass 패턴과 인코딩 이름 문자열 때문이다. utf1632prober.py vs detector.py의 10% 일치는 self.x = False 초기화와 @property 보일러플레이트다. 두 파일 모두 reset(), done, result를 가진 감지기 클래스를 구현하기 때문이다.
universaldetector.py vs detector.py의 10% 일치는 조금 더 흥미롭다. 일부는 의도적으로 유지한 API 표면(class UniversalDetector, def reset, return self.result) 때문이다. 하지만 두 가지 실질적 중복도 있다. UTF-16/32 감지를 위한 null 바이트 계산, 그리고 인코딩 동등성 맵("ascii": "Windows-1252", "euc-kr": "CP949" 등)이다.
null 카운팅은 Python에서 UTF-16을 감지하는 자연스러운 방식이다. 그리고 동등성 맵은 어떤 인코딩 라이브러리라도 필요로 하는 사실 관계 표다. Unicode 명세로부터 독립적으로 도달할 수 있다.
세 가지 독립적인 척도는 모두 같은 방향을 가리킨다. chardet 7은 이전 버전의 파생물이 아니다. git blame -C -C -C에서 이전 기여자 줄 수는 0이고, 토큰 수준 유사도는 최대 1.5% 미만이며, 일치 항목도 일반적인 보일러플레이트뿐이다. 지문 기반 유사도는 원래 1.0 릴리스와 비교하면 0까지 떨어진다.
소프트웨어의 비문자적 저작권 침해를 판단하는 표준 법리 시험은 1992년 Computer Associates v. Altai에서 나온 추상화-여과-비교 테스트다. 법원은 비교에 앞서 보호할 수 없는 요소들, 즉 아이디어, 표준적 관용구, 문제에 의해 결정되는 요소를 걸러낸다. AFC에서 핵심 질문은 저작권으로 보호되는 SSO(구조, 순서, 조직)가 새 저작물에 살아남았는가다. 독일 변호사 Chan-jo Jun은 FOSS Backstage Berlin 발표에서 바로 이 점을 지적했다. 텍스트 유사성을 넘어서 저작권 있는 “기능 아키텍처”가 같은지를 보아야 한다는 것이다. 그게 맞는 질문이다.
그렇다면 저작권 있는 아키텍처는 살아남았는가? 위의 아키텍처 비교가 이를 자세히 다루지만, 짧게 말하면 이렇다. 옛 코드는 수십 개의 상태 보유 프로버 객체를 병렬로 실행하며, 각 객체는 손으로 작성한 상태 머신에 바이트를 흘려보내고 독자적인 누적 confidence 점수를 추적한다. chardet 7은 이 전부를, 결정적인 결과를 반환하거나 다음 단계의 후보 집합을 걸러내는 상태 없는 함수들의 순차 파이프라인으로 대체했다. 옛 점수화는 4개 버킷의 서열 범주(NEGATIVE/UNLIKELY/LIKELY/POSITIVE)를 사용한다. chardet 7은 IDF 가중치를 둔 연속적 코사인 유사도를 사용한다. 옛 멀티바이트 검증은 명시적 상태 전이 테이블을 통해 바이트를 하나씩 걸어간다. chardet 7은 Python의 str.decode()를 strict 모드로 호출하여 CPython의 codec 기계에 맡긴다. 이것은 단지 같은 것을 다른 이름으로 부르는 수준이 아니다. 다만 이런 기법들이 왜 보호 대상이 아닌지 보려면, 사실 나란한 비교조차 필요 없다.
나는 chardet의 아키텍처를 Mozilla 코드베이스와 독립적으로 개발된 인코딩 감지기들과 비교했다. Firefox용 Henri Sivonen의 chardetng는 Rust로 처음부터 작성되었고, Sivonen은 Mozilla에서 일했음에도 Mozilla의 옛 감지기 코드를 명시적으로 재사용하지 않았다. Google의 Compact Encoding Detector, 그리고 IBM의 ICU CharsetDetector도 마찬가지다. ICU는 chardet와 같은 Mozilla 연구 논문에서 영감을 받았지만 독자 구현이다. 이들 모두는 같은 핵심 기법에 도달했다. 먼저 BOM을 확인하고, 바이트 유효성으로 후보를 걸러내고, 살아남은 후보를 통계적 빈도 모델로 점수화하며, confidence가 커질수록 후보를 점진적으로 제거한다. chardetng는 빈도 점수화 전에 강한 제거 규칙을 사용한다. CED는 점진적으로 강화되는 가지치기 임계값과 함께 순위화된 확률 분포를 유지한다. 심지어 알고리즘적 영감을 공유한 ICU조차 chardet와는 완전히 다른 구현을 만들었다. 이것들은 인코딩 감지 문제에 대한 수렴적 해법들이다. chardet 7의 설계 선택은 특히 옛 chardet 코드베이스와 정렬되어 있다기보다, 생태계 전체의 합의와 정렬되어 있다.
코드 수준에서 JPlag 일치는 import 문, argparse 보일러플레이트, 쉼표로 구분된 값들에 대한 오탐이다. universaldetector.py vs detector.py의 Copydetect 중복은 더 실질적이지만(null 바이트 카운팅, 인코딩 동등성 맵), 이것들 역시 해결하려는 문제에 의해 결정되는 요소들이다. Python에서 null 바이트를 세는 방법은 많지 않고, 인코딩 동등성은 Unicode가 정의하는 사실이다. 여과 과정을 거치고 나면 비교할 만한 보호 대상 요소는 남지 않는다.
내가 알기로, 공유된 요소가 표준적 관용구뿐인 경우 코드를 파생 저작물로 본 법원 판결은 없다. 훨씬 더 많은 중복이 있었던 클린룸 재구현도 인정된 바 있다(IBM BIOS 클론). Sony v. Connectix에서는 제9순회항소법원이 더 나아갔다. Connectix는 개발 중 Sony의 BIOS를 직접 복사했지만, 최종 제품에 침해 요소가 포함되지 않았기 때문에 공정 이용이라고 판단했다. 또 내가 여과에 대해 틀렸고 보호 대상이 무언가 살아남았다고 하더라도, 대법원은 Google v. Oracle (2021)에서 Google이 Java API 선언 11,500줄을 직접 복사했음에도 공정 이용을 인정했다. 제9순회항소법원은 Oracle v. Rimini Street (2024)에서 소프트웨어가 파생 저작물이 되려면 원본의 저작권 있는 자료를 “포함해야 하며”, 상호운용성만으로는 충분하지 않다고 판시했다.
이 판례들 가운데 어느 것도 AI 보조 재구현을 다룬 것은 아니고, 법원도 아직 그 문제에 대해 판단한 적이 없다. 하지만 법적 질문은 언제나 도구가 아니라 산출물에 관한 것이었다.
과정에 대한 흔한 반론도 있다. 이것은 클린룸이 아니었으니 결과물은 오염되었다는 주장이다. 하지만 클린룸 방법론은 법적 요구사항이 아니다. Antirez는 이 점을 잘 지적했다. 그것은 “소송에 대비한 최적화일 뿐”이라는 것이다. LWN.net의 논쟁도 같은 결론에 도달했고, 한 구독자는 미국법의 실제 기준을 정리했다. AFC 체계 아래에서 접근과 실질적 유사성이다. 실질적 유사성을 입증할 수 없다면, 거기에 어떻게 도달했는지는 중요하지 않다. Jun 자신도 FOSS Backstage에서 이 점을 인정했다. 결과물이 진정으로 새로운 아키텍처를 가진다면, 어떤 도구를 사용했는지와 상관없이 재라이선스가 합법일 수 있다는 것이다.
chardet 7은 같은 문제를 해결하는 다른 프로그램이다. 내가 찾을 수 있었던 모든 척도로 볼 때, 이것은 이전 코드를 파생한 것이 아니다.
이 재작성에 대해 내가 본 가장 흔한 반론 중 하나는 이렇다. “이게 허용된다면 누구나 AI를 이용해 어떤 오픈소스 프로젝트의 라이선스든 벗겨낼 수 있다. 옛 코드를 LLM에 먹이고, 기능을 설명하게 한 뒤, 그 설명으로 새 세션에서 재구현하면 copyleft 라이선스를 MIT로 세탁할 수 있다.”
이 우려는 정당하다. AI는 클린룸 스타일 재구현의 비용을 극적으로 낮췄다. 예전에는 비싼 엔지니어링 팀이 몇 달 동안 해야 했던 일을 이제는 Armin Ronacher의 표현대로 아주 쉽게 할 수 있다. Ars Technica와 The Register도 그 broader implication을 다루었다.
이런 비용 감소는 copyleft를 집행 수단으로 사용하는 데 실제로 파괴적이며, 이것이 오픈소스에 무엇을 의미하는지 솔직해질 필요가 있다.
하지만 법적 메커니즘 자체는 새롭지 않다. 클린룸 재구현은 수십 년 동안 독점 소프트웨어와 copyleft 소프트웨어를 복제하는 데 사용되어 왔다. Compaq의 IBM PC BIOS(1982), Wine, ReactOS가 그 예다. Antirez가 지적했듯, 오픈소스 운동 자체도 재구현 위에 세워졌다. GNU는 UNIX를 재구현했고, 많은 기여자들은 자신들이 대체하려는 코드를 접해 본 경험이 있었다.
명백한 반론은 이렇다. 그런 경우 구현 팀은 소스 코드를 암기하고 있지 않았다. 반면 Claude는 학습 데이터에 chardet를 가지고 있다. 이것은 실제 차이지만, 핵심 질문은 그 학습 데이터 노출이 파생 저작물을 만들어 냈는가 하는 것이고, 위의 유사도 증거는 그렇지 않다고 말한다. Bruce Perens도 GitHub에서 같은 결론에 도달했다. “현재 시점에서 법적 위험을 이유로, 다른 저작물과의 유사성이 검증상 낮은 AI 매개 오픈소스 프로그램을 거부하는 것은 권하지 않는다.” 그는 AI를 “인류 지식의 합에 가까운 무언가를 프롬프트에 가장 잘 답할 가능성이 높은 방식으로 섞는 블렌더이며, 결과는 어느 하나의 출처에서 파생된 것으로 알아볼 수 없게 된다”고 묘사했다.
더 큰 질문은 재구현의 경제성이지, 내가 한 일이 새로운 종류의 침해인가가 아니다. AI는 새로운 법적 메커니즘을 만든 것이 아니라, 기존 메커니즘을 대기업만이 아니라 개별 유지보수자도 사용할 수 있을 만큼 싸게 만들었다. 이것은 소프트웨어 개발 전반에 걸친 실제 변화이며, 오픈소스 커뮤니티의 우리 누구도 아직 그것이 무엇을 의미하는지 완전히 파악하지 못했다고 생각한다.
또 다른 흔한 비판은 chardet라는 이름을 유지하지 말고 새로운 패키지 이름으로 공개했어야 한다는 것이다. Luka Kladaric는 이를 가장 날카롭게 표현했다. “가치는 코드에 있는 것이 아니라 이름에 있다.” PSF Fellow Jonathan LaCour도 이 논쟁 전체가 이름만 달랐다면 “별것 아닌 일”이었을 것이라고 주장했다.
나는 그 말이 맞다고 생각하고 실제로도 고려했다. 하지만 chardet는 월간 1억 3천만 건 다운로드를 가진다. 버전 7은 버전 6보다 거의 50배 빠르고 정확도도 훨씬 높다. 매달 그렇게 많은 사람이 사용하는 무언가에서, 성능에 눈에 띄는 영향을 주는 변화를 만든다면 이는 측정 가능한 에너지 영향까지도 가질 수 있다. 같은 이름으로 배포하면 모든 사용자가 다음 pip install이나 의존성 업데이트 때 자동으로 그 이득을 얻는다. 마이그레이션 노력도 필요 없다.
새 패키지 이름을 썼다면 chardet-next 같은 것을 내고, 기존 chardet를 폐기 예정으로 만들고(월 1억 3천만 설치에 시끄러운 경고를 추가하고), 생태계가 옮겨 가기를 기다려야 했을 것이다. 어떤 프로젝트는 아예 옮기지 않았을 것이다. 성능과 정확도 개선은 지금보다 훨씬 적은 사용자에게만 도달했을 것이다.
나는 유지보수자다. 12년 넘게 유지보수자였다. 사용자에게 옳은 선택은 생태계를 갈라놓고 대부분의 사람을 느리고 덜 정확한 버전에 남겨 두는 포크가 아니라, 투명한 업그레이드라고 생각했고 지금도 그렇게 생각한다.
나는 재작성의 모든 대화 세션 타임라인을 구축했다. 전체 대화 기록을 둘러보고, 어떤 도구가 호출되었는지, 어떤 파일을 읽었는지, 어떤 코드가 작성되었는지 볼 수 있다. 아무것도 가려 놓지 않았다. 과정 중 내가 멍청한 말을 하거나 오타를 낸 부분을 사람들이 많이 찾아낼 거라고 확신한다. 괜찮다. 나는 나 자신을 오류 없는 사람으로 보이게 하려는 것이 아니다. 실제로 무슨 일이 있었는지 투명하게 보여 주려는 것뿐이다.
과정은 완벽하지 않았다. Claude는 내가 보게 할 의도가 없던 예전 소스 코드를 보았고, 나는 더 엄격한 가드레일을 설치했어야 했다. 하지만 결과물은 스스로 말한다. 근본적으로 다른 아키텍처, 내가 찾을 수 있었던 모든 척도에서 거의 0에 가까운 유사도, 그리고 원래 코드베이스에서 살아남은 줄 수 0.
나는 chardet가 표준 라이브러리에 들어가지 못하게 만든 라이선스 문제를 해결하려고 10년을 보냈다. 옛 아키텍처는 성능 면에서 한계에 도달해 있었다. 재작성은 그 둘을 모두 해결했다. 만약 당신이 이 과정이 결함이 있었다고 생각한다면, 나는 그 판단이 추측이 아니라 실제로 일어난 일에 근거하길 바란다.
초기 재작성은 MIT로 배포되었다. 이후 나는 0BSD로 바꾸었는데, 이는 퍼블릭 도메인에 준하는 라이선스로, 애초에 AI 생성 코드가 저작권 보호 대상인지라는 질문 자체를 피해 간다.↩︎
이 대화에서 내 쪽 기록은 사라졌다. Elon이 Twitter를 인수한 뒤 나는 계정을 삭제했다. Guido의 답글만으로도 흐름을 따라가기에 충분하고, 나중에 남긴 issue #36의 댓글도 결론을 확인해 준다. LGPL 때문에 표준 라이브러리 포함은 불가능했다.↩︎
결국 멀티코어 감지는 채택되지 않았다. 재작성판은 단일 스레드만으로도 충분히 빨랐고, 코어 간 작업 분배 오버헤드가 이득을 상쇄했다. 코드는 스레드 안전하지만, 내부적으로 병렬화하지는 않는다.↩︎
각 구성 요소별 개별 커밋: binary, BOM, ASCII, UTF-8, markup, byte validity, multi-byte structural, statistical scoring, orchestrator, bigram model training, accuracy suite, benchmark suite, escape detection, mypyc optimization.↩︎
© 2026 Dan Blanchard · Powered by Hugo&PaperMod