겉으로는 단순해 보이는 비알고리즘적 프로그래밍과 라이브러리 개발이 왜 예상외로 어렵고 불안정한지, 플랫폼·컴파일러·도구 생태계의 취약함과 CI·크로스 컴파일 환경에서 드러나는 마찰을 통해 짚는다.
2025년 7월 2일 Reddit
나는 이것을 아무리 예상하려 해도 계속 뒤통수를 맞게 되는 기술적 현상으로 겪고 있다.
육체노동은 어렵게 느껴진다(feels). 누군가를 보고 나에겐 지구력이 훨씬 부족하다는 걸 깨달을 수 있고, 설령 그렇지 않더라도 여전히 힘들게 느껴진다.
연구도 어렵게 느껴진다. 다른 누구도 아직 생각해 보지 않은 무언가를 고민하도록 요구받는다. 과학 바깥에서도 그런 일은 좀처럼 일어나지 않는다 — 유일무이한 농담을 한번 만들어 보라.
그런데 비알고리즘적 프로그래밍은? 정확히 지시를 따르는 기계에 내가 원하는 바를 말해 주는 일이다. 잘해야 기술 번역가에 가깝다. 박사 학위를 따려는 것도 아니다. 본질적으로 새로운 것을 만들지 않고 이것저것 배선을 잇는 일이다. _단순_해 보이고, 그래서 쉽게 느껴진다.
ㅋㅋ. 진짜 ㅋㅋ.
경험상 전혀 쉽지 않다는 건 알고 있지만, 왜 그런지 정확히 집어내기 늘 어려웠다. 이제야 좋은 답을 찾은 것 같다.
최근에 Lithium에 다시 관심을 갖고 모난 부분을 좀 다듬었다.
API 수준에서 Lithium이 하는 일은 패닉 또는 더 저수준 메커니즘으로 타입드 예외를 시뮬레이션하기 위한 throw와 catch 함수를 제공하는 것뿐이다. 고수준 구성 요소로서 썩 좋지는 않지만, 그럼에도 유용한 도구다.
프로토타입이라면 최대 50줄로 구현할 수 있다. 물론 성능 최적화를 하면 LoC(코드 줄 수)가 크게 늘어나지만, 그래도 관리 가능한 수준일 것처럼 보인다.
하지만 이 저장소에는 커밋이 200개나 있다. 깨지는 부분과 자잘한 이슈가 수두룩하게 튀어나오고, 그러다 보니 이 프로젝트를 “완료”라고 선언하고 잊어버린 채 다음 프로젝트의 토대로 삼을 수가 없다.
Lithium은 일부 저수준 rustc 메커니즘과 나이틀리 기능에 의존하므로, 변경 사항에 빠르게 대응하려면 CI가 필요하다. 매주 자동으로 CI 잡을 돌리지만, 깨짐이 내가 원하는 시점보다 조금 늦게 발견되는 게 불편해서 주기를 더 짧게 할까 고민 중이다.
깨짐이 주로 나이틀리 기능 변경 때문에 일어날 거라고 생각할 수 있지만, 그건 사실이 아니다. Lithium에는 플랫폼별 코드가 꽤 있어서 많은 타깃에서 CI를 돌린다. 그리고, 아 진짜… _엉망_으로 동작한다.
*-pc-windows-gnullvm으로 컴파일된 코드에서 스레드 로컬이 정렬이 맞지 않게 되는 버그도 있다. 머지되지 않은 수정이 있긴 하다. 또한 Wine의 i686-pc-windows-gnu에서는 panic!이 그냥 멈춰 버리기도 하는데, 이게 Rust 버그인지 Wine 버그인지조차 모르겠다.결국 Lithium이 이렇게 불안정한 주된 이유는 외부의 설계 결함과 버그 때문이다. 논리적으로는 단순하지만, 믿을 만한 토대가 부족하니 꼼수를 쓰거나 원래라면 좋은 접근을 포기해야 한다.
아니면 업스트림을 필사적으로 고치는 수밖에. rustc를 패치할 수도 있고, 언와인더나 Cranelift까지는 손을 댈 수 있다. LLVM은 선을 긋는다. 그런데 그 정도로는 충분하지 않은 듯하다.
이 취약함은 어디를 봐도 드러난다. 복잡성의 상당 부분은 CI를 꾸리는 데서 비롯된다. 이제쯤이면 사람들이 이걸 완성했을 법도 한데, 전혀 그렇지 않다:
aarch64-pc-windows-gnu 타깃을 지원하지 않는다. x86_64-pc-windows-gnu와 aarch64-pc-windows-gnullvm은 둘 다 지원한다. 이유를 모르겠다.gnullvm 타깃을 지원하려면 MinGW도 수동으로 설치해야 한다.cargo test --target <target>가 내가 모르는 사이에 doctest를 건너뛰다가, 업데이트 후에 갑자기 doctest를 실행하기 시작했고, 그 덕분에 RUSTFLAGS와 RUSTDOCFLAGS 모두에 rustc 플래그를 넣어야 하는 문제 같은 것들이 드러났다.사실, 나이틀리 전용 및 컴파일러 내부 기능들은 가장 덜 걱정된다. 그런 것들은 당연히 바로 동작하지 않아야 하는데, 놀랍게도 아주 잘 동작한다. 물론 Miri를 예외 처리해 줘야 하는 식의 코드 스멜이 있고, std나 rustc 내부에 의존하는 게 마음에 들지는 않지만, 그럼에도 다른 무엇보다 문제를 덜 일으킨다는 사실은 시사하는 바가 크다.
우리를 도와줘야 할 도구들이 오히려 우리를 파멸로 이끈다는 건 분통 터지는 일이다. 그리고 이것은 소프트웨어 개발에서 흔한 클리셰라고 생각한다.
복잡한 알고리즘 문제를 풀거나 프로그램을 최적화해야 한다면, 보통은 해낼 수 있다. 까다로울 수 있고, 뭔가를 조사해야 하거나 못생긴 unsafe 코드를 써야 할 수도 있지만, 일단 구현하면 동작한다. 영리한 코드 더미를 쓰고, _끝_이다. 요구 사항이 크게 바뀌지만 않는다면.
하지만 신뢰할 수 있는 제품은 코드 스니펫 이상의 것이다. 항상 전제가 있다. 프런트엔드 개발자가 JavaScript 런타임이 올바르게 동작한다고 가정한다든가, Rust 프로그래머가 LLVM을 신뢰한다든가, Docker 런타임이 리눅스가 멍청한 짓을 하지 않을 거라고 믿는다든가. 그리고 이 신뢰가 실패하면, 제정신을 잃기 시작한다. 당신이 할 수 있는 어떤 것도 문제를 반드시 해결해 준다는 보장이 없다. 분위기 싸움이고, “일단 내고 뭔가 깨지면 핫픽스하자”는 세계이며, 순도 100%의 광기다.
그리고 그건 _실재_한다. 사회적 이유로만, 어딘가에서 누군가 에지 케이스를 고려하지 않았고 이제 그걸 고치는 데 누구도 들이고 싶지 않은 노력이 필요하기 때문일 뿐이지만, 그래도 덜 실재하는 건 아니다. 엿같고, 일어나선 안 됐으며, 우리가 헛소리 같은 “worse is better” 이데올로기에 휘둘리지 않았다면 벌어지지 않았을 일이다. 하지만 우리는 이제 그 결과를 감수하고 살아야 한다.
컴퓨팅은 이제 불투명하고 신뢰하기 어려운 마술이다. 프로그래밍은 이 난해한 마법을 길들이는 일이다. 이렇게 프레임을 바꾸고 나서야, 비로소 어렵게 느껴진다.