대안 구현 문제(Alternative Implementation Problem)
(pointersgonewild.com)- 언어 런타임·JIT 같은 대안 구현은 성능이 좋아도 정본 구현의 변화와 사용자 기대를 계속 따라가야 해 채택이 제한될 수 있음
- PyPy, LuaJIT, TruffleRuby는 빠른 실행 성능을 보여줬지만 호환성 격차와 새 기능 추적 부담이 실제 배포의 장벽이 됨
- YJIT는 별도 Ruby 구현이 아니라 CRuby 내부에 들어가는 길을 택해 처음부터 CRuby 기능과 100% 호환되도록 만들었고, Shopify·Discourse·GitHub 등에 배포됨
- Crystal처럼 기존 언어와 매우 비슷하지만 호환되지는 않는 선택은 사용자가 “거의 Ruby지만 Ruby는 아닌” 차이를 계속 마주하게 만들 수 있음
- JSON 파서나 JavaScript처럼 공개 표준이 구현체와 분리된 영역에서는 대안 구현의 부담이 줄어들지만, 정본 구현이 사실상 표준인 생태계에서는 다른 전략이 필요함
반복되는 대안 구현의 함정
- 소프트웨어 세계에서는 기존 시스템의 더 나은 대안 구현으로 출발한 프로젝트가 정본 구현의 그늘에 갇히는 일이 반복됨
- 대안 구현은 기능, 성능, 생태계, 사용자 기대에서 표준처럼 받아들여지는 정본 구현(canonical implementation) 과 비교됨
- 정본 구현이 계속 변하면 대안 구현은 자기 방향을 정하기보다 그 변화를 따라가는 데 많은 에너지를 써야 함
- 전통적으로 인터프리터였던 언어에 JIT 구현을 붙이는 경우, 새 기능은 인터프리터에 더 빨리 들어가기 쉬워 JIT 쪽의 추적 부담이 커짐
PyPy, LuaJIT, TruffleRuby에서 보인 패턴
- PyPy는 Python용 고급 JIT 컴파일러로 CPython 대비 큰 속도 향상을 낼 수 있었지만, 실제 사용은 매우 적었음
- Python은 정기적으로 새 CPython 버전과 기능이 나오는 움직이는 목표물임
- PyPy는 이를 따라가는 데 어려움을 겪어 항상 Python 몇 버전 뒤처지는 상태가 됨
- Python 소프트웨어를 PyPy와 호환되게 만들려면 사용할 수 있는 Python 기능이 제한되고, 대부분의 Python 프로그래머는 이를 신경 쓰고 싶어 하지 않음
- LuaJIT는 기본 인터프리터 방식 Lua 구현보다 큰 성능 향상을 제공하며 높은 평가와 어느 정도의 실제 채택을 얻었음
- LuaJIT 제작자 Mike Pall은 많은 사람에게 뛰어난 프로그래머로 평가됨
- Lua 언어가 계속 새 기능을 추가하면서 LuaJIT도 여러 버전 뒤처져 있음
- 일부 Lua 사용자는 이 때문에 LuaJIT 사용을 꺼림
- Lua는 미니멀리즘으로 알려진 언어지만, 새 기능 추가를 늦추거나 Mike Pall과 조율하는 노력은 이뤄지지 않았음
- TruffleRuby는 Ruby JIT 중 가장 인상적인 성능 수치를 내세웠지만 배포는 제한적이었음
- 실용적 이유 중 하나는 TruffleRuby의 워밍업 시간이 CRuby보다 훨씬 길다는 점임
- CRuby가 기능을 계속 추가하면서 TruffleRuby 기여자들은 이를 따라가기 위해 노력해야 했음
- Ruby 사용자는 CRuby를 정본 구현으로 보고, 완전히 호환되지 않는 구현은 고려할 가치가 낮다고 봄
YJIT가 택한 다른 길
- YJIT는 또 하나의 Ruby JIT로 시작했지만, 별도 구현이 아니라 CRuby 자체 내부에 구축됨
- 이 선택은 여러 설계상 트레이드오프를 만들었지만, YJIT가 시작부터 모든 CRuby 기능과 100% 호환될 수 있게 했음
- 현재 YJIT는 Ruby의 “공식” JIT이며 Shopify, Discourse, GitHub 등에 배포되어 있음
- github.com이나 Shopify 스토어를 방문한 사용자는 YJIT와 상호작용한 셈임
- 지금까지 Ruby JIT 컴파일러 중 YJIT가 가장 큰 성공을 거뒀고, 그 성공에는 호환성이 핵심 역할을 했음
“이길 수 없으면 합류하라”만으로는 부족함
- 대안 구현으로 자신을 포지셔닝하면 정본 구현의 그림자 안에서 계속 따라잡기 게임을 하게 될 가능성이 큼
- 정본 프로젝트가 계속 진화하면 대안 구현은 자기 프로젝트의 방향에 대해 제한된 의사결정권만 가진 채 뒤따라가야 함
- 정본 구현에 합류하는 편이 더 나은 결과를 낼 수 있지만, 그것만으로 모든 경우가 해결되지는 않음
- Ruby 생태계의 Crystal은 Ruby와 비슷한 문법을 가진 정적 컴파일 언어이며 타입 추론을 사용함
- Crystal은 의도적으로 Ruby 호환을 목표로 하지 않고 Ruby에서 갈라져 나감
- Rubyist에게는 “거의 Ruby지만 완전히 Ruby는 아닌” 언어로 보이며, 실제로는 많은 미묘한 차이와 비호환성이 있음
- 이런 유사성은 사용자의 기대를 흔들고 혼란을 만듦
- Crystal은 처음부터 Ruby와 비슷하다고 마케팅하지 않았다면 더 나은 결과를 얻었을 수 있음
경쟁을 피하고 자기 방향을 갖는 선택
- Peter Thiel의 “competition is for losers”는 불필요하게 경쟁해야 하는 위치에 스스로 들어가지 말라는 맥락으로 사용됨
- 새 프로그래밍 언어를 만들려면 Python의 부분집합이나 기존 언어와 표면적으로 매우 가까운 무언가를 만들려 하지 않는 편이 낫다는 조언으로 이어짐
- 자기만의 것을 만들면 다른 구현의 성능, 기능 집합, 라이브러리 생태계를 맞춰야 한다는 기대에 묶이지 않고 자기 속도와 방향으로 시스템을 발전시킬 수 있음
- 이 조언은 언어나 시스템에 정본 구현이 존재하는 상황에 적용됨
- 공개 표준이 있는 영역은 예외가 될 수 있음
- JSON 파서는 비교적 작고 빠르게 변하지 않는 명확한 명세가 있어 자체 구현이 가능한 대상임
- JavaScript는 여러 브라우저 기반 구현이 존재하며, 이는 JS 명세를 관리하는 외부 표준 기구가 있기 때문에 가능함
- JS 표준 작업자들은 JIT 컴파일 구현이 성능에 중요하다는 점을 이해하고, 언어 진화를 그에 맞춰 이끎
- 이들은 가능한 한 빠르게 많은 새 기능을 추가하는 게임을 하고 있지 않음
댓글과 토론
Hacker News 의견들
-
OP가 놓친 중요한 점이 하나 더 있음. 대체 구현을 만들면 보통 기준 구현과 아키텍처가 다르고, 기준 구현에서는 쉬운 일이 내 구현에서는 매우 어려울 수 있음
예를 들어 금융 보고서용 독점 소프트웨어가 이상한 이진 형식으로 문서를 저장한다고 하자. 무료 대안을 만들면서 문서 전체를 메모리에 읽고, 저장할 때는 전체 파일을 다시 쓰는 구조를 택했는데, 원본은 RAM이 적던 시절에 만들어져 사용자가 작업 중인 섹션만 읽고 쓰며 제자리 수정까지 가능했다고 가정할 수 있음
이후 원본이 문서에 첨부파일을 넣는 기능을 추가하면, 투자자 통화 녹음이나 수백 페이지 스캔 PDF 같은 큰 파일도 섹션 단위 로딩 덕분에 잘 동작함. 반면 내 구현은 문서 전체를 역직렬화하므로 사용자의 RAM보다 문서가 커지는 순간 문제가 되고, 원본에서는 개발자 한 명이 일주일이면 끝낼 변경 때문에 소프트웨어 전체를 재설계해야 할 수 있음- 모든 핵심 소프트웨어 구성요소에 두 개의 독립 구현이 필요한 업계에서 일하며 여러 재구현에 참여했는데, 원본 구현이 분명 X를 하고 있어서 우리 아키텍처에서는 잘 맞지 않는 기능을 본 적은 있음. 그래도 그 기능 때문에 프로그램을 재설계해야 했던 적은 없고, 한쪽에서는 사소한 기능이 다른 쪽에서는 일이 좀 더 드는 정도였음
반대로 원본에서는 구현이 더 힘들었지만 두 번째 구현에서는 사소해진 기능도 있었음. 큰 재설계는 한 번뿐이었고, 그것도 지수적 폭증을 낳는 가정을 가진 원본 구현에서였음
공개 사례로는 Linux에서 Windows 애플리케이션을 실행하는 문제가 있음. Linux 커널은 NT와 완전히 다른 구현이고 호환을 목표로 하지도 않지만, Windows 앱 실행에 커널 전체 재설계가 필요한 것은 아님. 비교적 일반적인 커널 기능 몇 가지와 사용자 공간 호환 계층이면 됨. Wine은 작성과 유지에 많은 노력이 들지만 Windows 자체 구현보다는 훨씬 적고, Windows 호환을 지향하지 않았던 플랫폼 위에서 돌아감. 다만 글에서 말한 것처럼 Windows를 계속 뒤쫓아야 하고, 기준 구현의 버그에 의존하는 코드까지 생기므로 버그까지 호환하려면 어떤 버그를 구현해야 하는지 먼저 찾아내야 함 - 저자는 인터프리터 언어와 컴파일 언어에서 새 기능 구현 난이도가 다르다는 부분에서 이 점을 이미 다룬 듯함
- 맞음. 이것이 Python을 빠르게 만들기의 고전적인 문제임. CPython은 최적화 없이 코드가 시키는 대로 따라가는 단순 인터프리터로 시작했고, 모든 것이 딕셔너리이며 실질적인 동시성이 거의 없음
그래서 어떤 코드든 실행 중에 다른 것을 고칠 수 있음. 실제로 그렇게 많이 쓰지는 않지만, 빼려고 하면 사람들이 난리침. Python을 실제로 컴파일하는 구현은 어떤 스레드가 갑자기 다른 스레드 밑의 무언가를 바꾸는 상황까지 처리해야 함 - 혁신가의 딜레마 관점에서는 참조 구현이 경쟁사의 것일 수도 있음. 시장을 더 잘 알게 된 지금 더 나은 아키텍처를 고르면, 경쟁사보다 새 기능을 더 싸고 빠르게 추가하는 위치에 설 수 있음
작은 회사가 큰 회사의 부담을 키우는 방법 중 하나이고, 기술 부채를 덜 방치한 회사도 그럴 수 있음. 경영진에게 기술 부채를 명확히 보여주기 드문 순간이기도 함. “Acme보다 이 기능 구현에 시간이 더 오래 걸린다”라고 말할 수 있기 때문임 - 이런 아키텍처 차이가 의도적인 경우도 있음. 예를 들어 많은 기본 Unix 유틸리티의 GNU 버전은 저작권 침해 의혹을 피하려고 원래 Unix와 BSD 버전과 완전히 다른 절충을 택함
- 모든 핵심 소프트웨어 구성요소에 두 개의 독립 구현이 필요한 업계에서 일하며 여러 재구현에 참여했는데, 원본 구현이 분명 X를 하고 있어서 우리 아키텍처에서는 잘 맞지 않는 기능을 본 적은 있음. 그래도 그 기능 때문에 프로그램을 재설계해야 했던 적은 없고, 한쪽에서는 사소한 기능이 다른 쪽에서는 일이 좀 더 드는 정도였음
-
“Python의 부분집합을 만들려고 하지 말라”는 주장에는 동의함. “Python이지만 X가 더 좋다”로 마케팅되는 프로젝트는 기준 구현과 경쟁하기가 늘 어렵고, 특히 X가 속도라면 더 그렇음. 동적 타입 언어를 쓰는 사람은 결국 실행 속도를 크게 신경 쓰지 않는 경우가 많음
하지만 대체 구현이 항상 실패하는 것은 아님. MicroPython은 Python 3.4 수준 이상을 거의 지원하지 않는데도 꽤 성공한 듯함. 마이크로컨트롤러에서 돌도록 설계되어 CPython과 경쟁하는 것이 아니라 다른 마이크로컨트롤러 프로그래밍 환경과 경쟁하기 때문임
다만 MicroPython 유지보수자들은 더 최신 Python 기능 요청을 많이 받을 것 같음. 한때 애플리케이션 내장용으로 가볍고 임베딩에 집중한 Python 대체 구현을 생각한 적이 있는데, 이 경우도 CPython이 아니라 Lua와 경쟁하는 구상이었음. 그런데 1순위 기능 요청은 “NumPy를 지원하나요?”였음- 흥미롭게도 MicroPython은 이제 웹 용도로도 관심과 요청을 받고 있음. PyScript의 훌륭한 노력 덕분임. 하지만 전체 RAM이 1MB인 마이크로컨트롤러와 달리 웹에서는 기존 코드가 돌아가고 완전한 CPython 경험과 호환성이 있기를 기대하므로 훨씬 어렵다
동시에 CPython 쪽에서도 프론트엔드에서 더 잘 동작하게 하려는 작업이 진행 중인 듯하고, 핵심 고통 지점은 패키지 크기임
- 흥미롭게도 MicroPython은 이제 웹 용도로도 관심과 요청을 받고 있음. PyScript의 훌륭한 노력 덕분임. 하지만 전체 RAM이 1MB인 마이크로컨트롤러와 달리 웹에서는 기존 코드가 돌아가고 완전한 CPython 경험과 호환성이 있기를 기대하므로 훨씬 어렵다
-
스타트업을 만들며 비슷한 것을 배웠음. 다시 한다면 우리 분야의 기본 입장권 기능을 적극적으로 피했을 것임
대신 엔터프라이즈식 요구사항을 우리 아키텍처가 지원할 수 있다는 확신을 줄 최소한만 만들고, “아, 이게 어디까지 갈지 보인다”는 반응을 얻을 수 있는 차별화 요소에 전부 집중했어야 함. “그냥 X의 복제품이네” 같은 반응을 받는 기능 말고- 요지는 알겠지만 “table stakes”라는 용어는 잘못 쓰고 있음. 정의상 그건 판에 참여하려면 반드시 내야 하는 것들임
말한 전략은 기본 필수 기능은 구현하되, 그 이후의 “일반적인” 확장 기능까지 깊게 가지 않는 쪽에 가까움. 흥미로운 기능으로 다시 연락을 받게 만들고, 동시에 필수 요소가 없어서 탈락하지 않을 정도의 기본 기능은 갖추는 방식임 - 제대로 이해했다면, 기업 고객에게 필요한 최소한, 즉 그들의 리더십이 거래를 승인하는 데 요구하는 것만 구현하고, 그 이상은 잊은 뒤 경쟁사와 구별되는 아주 새로운 것에 집중하라는 뜻인가?
- iPhone이 처음 나왔을 때 복사·붙여넣기도 없고 GSM 무선 성능도 아주 실망스러웠던 것과 비슷함
- 요지는 알겠지만 “table stakes”라는 용어는 잘못 쓰고 있음. 정의상 그건 판에 참여하려면 반드시 내야 하는 것들임
-
모든 래퍼 코드에 대해 비슷하게 느낌. 누군가 “이 API의 내부용 버전이 필요하다”고 말하는 경우가 있음
이유는 다양하지만 “공식 API를 제대로 쓸 거라고 믿을 수 없다” 같은 식임. 그럴 수는 있지만, 내부 버전은 표준도 덜하고 문서도 더 나쁨
때로는 “추가 기능이 필요하다”가 이유인데, 그렇다면 API 전체를 감싸지 말고 함수 3개만 추가하면 됨. 시간이 지나면 코드베이스의 99%가 폴리필이 될 수 있음
요지는 기본값을 쓰지 않으면 나중에 코드베이스를 물려받는 사람에게 큰 고통을 준다는 것임- 그런 래퍼에는 아래쪽 라이브러리와 직접 상호작용할 수 있는 인터페이스가 있으면 도움이 된다고 봄
예를 들어 Zig로 Python 네이티브 모듈을 작성하는 ziggy-pydust 라이브러리는 일반적인 Python.h import보다 확실히 보기 좋음. 그래도 아직 구현되지 않았지만 Python.h에는 있는 함수에 직접 접근하는.ffi도 있음
이런 선택지가 없으면 보통 그런 라이브러리를 버리고 원본을 선호하게 됨. 다만 이것도 어떤 의미에서는 래퍼, 즉 네이티브 모듈이고, 개발 속도를 위해서는 때로 ctypes를 직접 쓰는 편이 더 나을 때도 있음
- 그런 래퍼에는 아래쪽 라이브러리와 직접 상호작용할 수 있는 인터페이스가 있으면 도움이 된다고 봄
-
좋은 글이고 훌륭한 교훈이 많지만 핵심 재료 하나가 빠져 있음. 이것은 어떤 제품에 대한 경쟁 대안과도 비슷함
Amazon이 사람들이 익숙한 오프라인 서점을 갖추지 못해서 실패했다는 식으로 말하는 것과 비슷한데, 실제로는 그렇지 않았음
이런 JIT 대안들이 실패하고 계속 따라잡기만 했던 이유는 실무적으로 X 언어 개발자 대부분이 JIT를 그리 중요하게 여기지 않기 때문임. 더 정확히는 언어 기능과 상호운용성을 JIT보다 더 중요하게 봄
그래서 경쟁하기보다 “합류하는” 제품이 이김. 더 높은 안정성이나 상호운용성을 제공하지 못하기 때문임 -
오랫동안 언어와 컴파일러를 다뤄 왔고, 이 글이 매우 재미있었음. 같은 생각을 다른 방식으로 말하면, 언어는 단순한 컴파일 속도보다 훨씬 많은 것임
컴파일 속도는 매우 중요하고, 확실히 상위 10개 차원 안에는 듦. 특히 컴파일 속도를 높이면 개발자 피드백 루프의 속도가 올라가고, 핵심 팀이 다른 모든 차원도 더 빨리 개선할 수 있음
그래도 프로그래밍 언어에는 매우 중요한 차원이 30개 넘게 더 있음- 글은 실행 속도에 관한 것처럼 보임. 그래도 거기서도 CPython의 인기를 보면 실행 속도가 1순위 요소는 확실히 아님
-
이런 프로젝트가 성공하는 방법에 대한 결론은 좋지만, 많은 프로젝트가 왜 뜨지 못하는지에 대해 아직 덜 언급된 요소가 있음. 대체 구현의 호환성이 실제로는 주장보다 낮은 경우가 많고, 오래된 언어 기능에서도 그렇다는 점임
예를 들어 Ruby와 Python 앱은 의존성 어딘가에 네이티브 C 확장이 있는 경우가 매우 흔한데, 주요 대체 구현들은 이를 지원한 적이 없는 것으로 앎. 시도한 적은 있지만 명백한 기술적 이유로 잘 되지 않았고, 라이브러리들이 여러 구현을 제공하길 기대하는 대안도 역사가 순탄치 않았음
여기에 이런 언어들이 CPU보다 입출력이 더 큰 성능 요인인 CRUD 웹사이트에 자주 쓰인다는 사실까지 합치면, 더 빠른 대안의 매력은 훨씬 줄어듦 -
정말 훌륭한 글임. 기술의 사회학이 아주 흥미롭다
언어 구현 유지보수자들은 사용자에게 이로운 새 기능을 설계하고 배포할 때 최대한의 유연성을 원함. 기능을 내보내기 전에 여러 구현의 합의를 받아야 해서 발목 잡히고 싶어 하지 않음. 사례로는 JavaScript의 발전이 수년간 얼마나 빙하처럼 느렸는지, 그리고 TypeScript가 상대적으로 얼마나 빠르게 진화하는지 보면 됨
동시에 대체 구현은 언어 생태계가 튼튼하다는 신호가 될 수 있으므로 장점도 있음. 대체 구현이 정말 좋다면, 일부 틈새 요구 사용자에게만 좋더라도 생태계에 실제 가치를 더할 수 있음
그래서 언어 설계자나 유지보수자라면 대체 구현에 적극적으로 적대적이지는 않겠지만 단점은 있음. 대체로 사용자의 피드백은 새 기능을 내고 언어를 발전시키라는 방향으로 올 것임. PyPy, IronRuby, LuaJIT 등이 따라올 수 있게 속도를 늦춰 달라는 요청은 많지 않을 것임
언어 소비자가 어떤 구현 위에 만들지 고를 때 가장 큰 우선순위는 보통 안전성과 안정성임. 한때 매우 똑똑한 박사과정 학생이 만들었지만 지금은 다른 프로젝트로 떠난 대체 구현의 미묘한 동작 특성에 백만 줄 코드베이스가 의존하게 되는 상황을 원하는 사람은 없음. 그래서 사용자는 가장 많이 쓰이는 구현으로 몰리고, 그 사실이 다시 다른 사용자를 끌어들이는 강한 긍정적 피드백 루프가 생김
결과적으로 반대 방향으로 밀어붙이는 강한 힘이 없다면 대부분의 언어는 하나의 기준 구현으로 수렴함. 이것이 좋은 일이라는 주장도 가능함. 언어 구현에 투입되는 거의 모든 엔지니어링 노력이 여러 구현으로 쪼개지지 않고 모든 사용자에게 이득이 되기 때문임. 물론 단점은 구현이 국소 최적점에 갇힐 수 있다는 것임 -
LuaJIT이 언급됐지만, 항상 글의 결론대로 되지는 않는다는 사례이기도 함. 많은 사람과 프로젝트가 LuaJIT을 Lua보다 의도적으로 선택했음
- 이 말을 하려고 댓글에 왔음. LuaJIT은 TeX 커뮤니티에서 열성적으로 쓰이고,
luajittex실행 파일도luatex와 나란히 쉽게 구할 수 있음
- 이 말을 하려고 댓글에 왔음. LuaJIT은 TeX 커뮤니티에서 열성적으로 쓰이고,
-
덜 인기 있는 생각일 수 있지만, 사람들은 자신도 포함해 가끔 자존심을 점검해야 함. 기존 오픈소스 프로젝트에 기여하는 것보다 “내 것”인 병렬 프로젝트를 만드는 편이 더 쉬울 수 있지만, 누구를 위해 하는지 물어봐야 함
프로젝트 유지보수자를 위한 것인지, 프로젝트 자체인지, 사용자들인지, 자기 자존심인지. 마지막 항목에 화가 난다면 아마 관련이 있을 가능성이 큼
기존 언어에 JIT을 추가하는 것은 큰 작업이어서 기준 구현이 받아들이는 기준도 높을 것임. 그래도 목표는 그쪽이어야 한다고 봄. 포크하거나 새로 만드는 것도 큰 일을 할 자유를 주지만, 흔히 임시적인 것으로 봐야 함
목표가 내가 무엇을 할 수 있는지 보여주는 것이라면 멀리 가지 못할 가능성이 큼. 더 나은 것을 만들겠다는 목표라면 다른 사람들의 제약 안에서 일하는 법을 배우게 됨- 놀랍게도 반대 생각임. 단일 프로젝트, 기준 프로젝트에만 기여, 대체 구현은 없어야 한다고 설교하는 사람들이야말로 자존심을 점검하고, 여러 접근법의 존재와 경쟁이 독점보다 나은 이유를 배워야 함
이 글은 Python, Lua, Ruby가 그런 접근을 택해 많은 사람을 실망시킨 방식을 보여준다고 봄. 그 결과 수천 명의 개발자와 수백만 사용자가 더 느린 개발과 소프트웨어를 감수하게 됨. 불가능해서가 아니라, 행정적으로 그렇게 할 유인이 없기 때문임 - 흠, 공식 구현이 완전히 저주받은 경우도 있어서 자기만의 작은 정원을 만들어야 할 때가 있음. 그게 20에이커와 윤작 계획으로 커지더라도, 때로는 화산보다 낫다
- 놀랍게도 반대 생각임. 단일 프로젝트, 기준 프로젝트에만 기여, 대체 구현은 없어야 한다고 설교하는 사람들이야말로 자존심을 점검하고, 여러 접근법의 존재와 경쟁이 독점보다 나은 이유를 배워야 함