Rust 개발자가 경험한 Zig
요약
이 글은 Rust 개발자가 Zig와 C23 등 여러 언어를 비교하며, 특히 레거시 시스템 이식 및 현대적인 프로그래밍 패러다임에 대한 깊이 있는 고찰을 담고 있습니다. Zig의 장점과 한계를 분석하고, 모나드 기반 함수형 프로그래밍 접근 방식과 메모리 관리 측면에서 Rust와의 차이점을 비교합니다.
핵심 포인트
- Zig는 C/C++ 상호운용성과 크로스 컴파일에 강점이 있다.
- Rust는 실무 관점의 도구 성숙도와 안정성이 더 높다.
- 함수형 프로그래밍 시, Zig는 수동 메모리 관리 비용이 발생할 수 있다.
- 최적의 언어 선택은 프로젝트의 맥락과 목표에 따라 달라진다.
여러 목적으로 C와 Go, Zig 같은 후계 후보를 비교하는 프로젝트를 진행 중임. 그중 하나는 32비트 K&R C로 작성된 1980년대 컴퓨터 대수 시스템을 장기 보존용으로 이식할 언어를 고르는 일임. Zig는 디버깅용 컴파일러로 훌륭하지만, 보존용 대상 언어로 삼기에는 아직 안정성이 부족함. https://github.com/Syzygies/Compare
현지 언어도 메뉴도 모르는 식당에서 대표 요리의 세 가지 가격대 중 하나를 고른다면 가운데를 택하겠음. 윈난의 궈차오미셴을 고를 때도 그랬고, 동행한 작가 Fuchsia Dunlop도 나중에 동의했음.
K&R C의 후계자로 Go와 C23을 고를 때도 비슷함. 둘 다 정통 혈통이지만 이름까지 물려받은 쪽은 고질적인 결점도 함께 물려받았음.
평소 C를 비판하긴 하지만, 당연히 C23을 고르겠음. K&R C와의 호환성을 깨는 변경만 반영하면 수정량을 최소화할 수 있지만, 다른 언어라면 전면 재작성이 필요함. 문법이 조금 어색하더라도 이제 함수 인자의 경계를 표시할 방법도 있음.
가운데 선택이 틀릴 수도 있음. 터무니없이 비싼 세 번째 상품으로 두 번째를 합리적으로 보이게 하는 마케팅 기법도 있으니, 정답은 맥락에 따라 다름임. 선택에 시간을 얼마나 써야 하는지조차 맥락에 달려 있음.
가장 잘 아는 언어로 먼저 작성한 뒤, 원하는 동작을 자동 검증할 수 있게 되면 LLM으로 더 적합한 언어로 옮기는 전략도 일부 상황에서는 통함.
대상 독자는 누구이며, 누가 코드를 읽고 사용하게 되나요?
절 제목인 ‘가변성과 불변 모나드가 핵심 차이’는 맞지 않음. 예제의 목적은 함수와 데이터를 받아, 일반 값이면 함수를 적용하고 컨테이너면 각 원소에 적용하는 것임. Zig에서도 불변 자료구조로 구현 가능함. data.flat_map(f) 대신 할당자 a를 넘기는 data.flat_map(a, f)로 호출하면 됨. Zig 예제가 데이터를 변경하는 것은 프로그래머의 선택이지 언어의 강제가 아님. 애초에 여기에 모나드가 무슨 상관인가요?
앞부분에서는 함수형 구현이 가능하다고 인정하되, Zig의 메모리 관리 방식 때문에 실용적이지 않게 느껴진다고 설명함. 할당자를 직접 다루면서 순수 함수형으로 새 구조를 계속 만들면 메모리 비용이나 이를 줄이기 위한 수동 관리 비용이 커진다는 취지임.
불가능하다고 단정하기보다는 Zig가 의도한 사용 방식을 이해하고 익숙한 Rust와 비교하려는 성실한 시도로 읽었음. 다른 언어의 가치관으로 평가하기보다, Zig 자체의 목표와 달성 수준을 살펴본 점이 좋았음.
Rust 코드 표면에 변경 연산이 드러나지 않아도, 더 빠르고 저렴하다면 내부 구현은 데이터를 직접 변경할 수 있음. FlatMap의 InPlaceIterable 구현이 그런 예임.
물 위에서는 우아하게 나아가지만 물밑에서는 바쁘게 움직이는 거위와 비슷함. 표면은 유지보수하기 좋은 불변 연산이지만 내부는 Zig처럼 변경 연산을 활발하게 수행함.
Zig에서는 어떻게 할지 작성하는 데 시간을 많이 쓰지만, Rust에서는 무엇을 할지만 쓰고 나머지를 기계에 맡길 수 있음. 최고의 프로그래머에게 Zig의 명시성이 유리한 예외도 있지만, 그런 상황이나 개발자가 충분히 많지는 않다고 봄.
flat_map은 모나드에서 유래함. 다른 API 대신 flat_map을 전반적으로 사용하는 것이 모나드 중심의 프로그래밍 스타일임.
도구와 컴파일 타임 기능에 두 가지를 덧붙이고 싶음. Zig와 Rust 모두 개발 도구가 좋다는 평가는 타당하며, 특히 Zig의 C/C++ 상호운용성과 크로스 컴파일은 훌륭함. 다만 실무 관점의 도구 성숙도는 Rust가 훨씬 앞섬. Zig가 훨씬 젊은 언어라는 점을 감안해도 고려할 부분임.
컴파일 타임 기능에서 Zig만 칭찬하고 Rust를 낮게 평가하는 것은 부당하다고 봄. Rust는 코드가 언제 실행되든 결과가 같아야 한다는 더 높은 목표를 추구함. 유용하지만 구현 난도가 훨씬 높아 Zig의 comptime과 근본적으로 같은 기준에서 비교하기 어려움.
Zig의 comptime 코드는 실행 시점에 따라 결과가 달라질 수 있다는 뜻인가요? 정확히 어떤 의미인가요?
실제로는 Zig의 comptime이 더 쉽고 효과적으로 느껴짐. Rust의 상수 평가도 재미있게 써봤지만, 당시에는 상수 문맥에서 함수형 기능 상당수가 지원되지 않아 성가신 명령형 우회 코드를 많이 작성해야 했음. 지금은 좀 나아졌을 수도 있음.
한 크레이트에서는 부동소수점 변환들을 상수 평가로 생성할 수 없어, 빌드 스크립트로 조회 테이블 파일들을 만들고 include_bytes!로 포함해야 했음.
프로그래밍을 깊이 이해하지 못해서일 수도 있지만, Zig에 대한 열광을 잘 모르겠음. Rust 전문가는 아니어도 조금 써봤고, GC 없이 컴파일 타임에 메모리 관리 문제를 대부분 해결해 잘 작동한다고 느낌. 주로 듣는 단점은 컴파일이 느리다는 것인데, 크레이트가 250개를 넘으면 최종 --release 링크 시간이 체감되지만 증분 컴파일도 개선되고 있음.
반면 글에 나온 Zig 문법은 JavaScript, Python, Go를 섞은 듯하면서 여전히 직접 메모리를 관리해야 함. 함수형 방식도 부족하고 메모리 누수, 이중 해제, 메모리 손상까지 남는다면 문법만 나아진 C 아닌가요? 그 대안이라면 최종 링크에 몇 분 더 쓰는 편을 택하겠음.
오랫동안 저수준 프로그래밍을 해왔고 널리 쓰이는 언어 개발에도 참여하는 입장에서 Zig가 흥미로움. 특히 부분 평가인 comptime 하나로 매크로·템플릿·제네릭·constexpr를 대체하는 일관된 설계가 매력적임. Lisp의 매크로처럼 언어의 중심을 이루며, 다른 기능에 추가하는 대신 대체한다는 점에서 전례 없는 혁신적 설계라고 봄.
Rust가 메모리 관리를 ‘대부분’ 해결했다는 표현에는 단서가 큼. 저수준 언어를 쓰는 핵심 이유인 고효율 특수 자료구조는 대개 unsafe가 필요함. 또 C++처럼 프로그램이 수년에 걸쳐 진화하면서 개별 객체의 malloc/free에 의존하는 범용 방식으로 기울고 점점 느려질 수 있음. TCMalloc 같은 거대한 런타임도 포인터를 이동시키지 못하므로 한계가 있음.
이동형 GC는 포인터 증가로 할당하고 메모리를 일괄 회수하지만, 하드웨어와의 사이에 FFI 계층을 두지 않는 저수준 언어에는 적용하기 어려움. 이에 맞서려면 비슷한 원리의 아레나 할당이 필요함. Zig는 이를 거의 사용자 친화적으로 만들고, 프로그램의 변화에도 견딜 만큼 조합 가능하게 만들려는 첫 언어임. 실제 성과는 시간이 알려줄 것임.
1970년대 후반 Modula-2나 Mesa의 타입 안전성에 comptime과 C에 가까운 문법을 더해 재포장하니 새로운 개발자들이 몰려드는 셈임. UNIX와 C를 만든 AT&T가 C의 대안으로 연구한 언어는, C와 별 차이 없는 언어가 아니라 Cyclone이었다는 점도 참고할 만함.
주로 TS, Go, Python을 쓰는 입장에서도 Zig가 그런 언어처럼 보인다는 데 동의함. 내가 Go와 Zig를 좋아하는 이유는 비교적 단순하고 C 같은 느낌이기 때문임. POSIX/Wayland 프로젝트를 더 해보고 싶은 입장에서는 C 상호운용성도 큰 장점임.
Zig는 Rust보다 더 저수준을 지향함. 홈페이지에서 강조하듯 숨겨진 메모리 할당이나 제어 흐름이 없고, 할당 방식을 더 세밀하게 제어할 수 있어 성능 제약이 엄격한 분야에 유용할 수 있음.
Kelley는 디지털 오디오 워크스테이션을 개발하면서 엄격한 실시간 요구사항을 다루기에는 기존 언어가 불편하고, C보다는 현대적인 언어가 필요해 Zig를 만들었음.
추측이지만 이 분야에서는 할당·해제 시점과 메모리 배치 제어가 Rust보다 유리할 수 있음. Rust가 수명을 세밀하게 스코프에 연결하는 방식은 대부분 유익하겠지만, 할당자 작업 때문에 CPU가 언제 멈칫할지 추론하기는 더 어려울 수 있음.
컴파일이 단일 스레드 작업이었다면 Rust는 성립하기 어려웠을 것이라고 봄. 19902000년대가 아니라 지금 등장한 이유도 상당 부분 여기에 있을 것임.10배 빨라질 수 있는 시대임. Rust 초기에는 멀티코어 장비가 비쌌지만 소비자용 4코어가 흔해진 뒤 본격적으로 확산됐고, 이제 내 낡은 노트북도 16코어임.
멀티코어 보급과 Rust의 성공이 겹친 것은 우연이 아니라고 생각함. 병렬화 효율이 70%만 되어도 최적의 순차 실행보다 5
Rust는 이런 시스템에서 실행할 올바른 병렬 코드를 작성하기에도 유리하니 이중으로 혜택을 받음.
IDE 지원이 거의 없다는 점은 충분히 예상할 만했음.
2020년대를 돌아보면 할당자에 대한 집착이 눈에 띌 것 같음. Handmade 계열의 C 후계 언어인 Zig, Odin, C3, Jai 모두 이런 성향이 보임.
단순한 예제에서는 영리한 할당자 기법으로 큰 성능 향상을 얻을 수 있음. Jai와 Odin은 프레임별 아레나를 통째로 버려 개별 할당 추적 비용을 없애는 코드를 적극 권하는 듯함.
하지만 현실의 소프트웨어는 그렇게 단순하지 않은 경우가 많음. 무가치한 기능이라는 뜻은 아니지만, 숙련된 개발자의 수많은 도구 중 하나이지 언어의 간판 기능으로 삼을 정도는 아니라고 봄.
AAA 게임은 할당자를 광범위하게 사용하며, 거의 모든 소프트웨어보다 복잡하고 성능 요구도 높음. 어떤 단순한 예제를 말하는지 모르겠음.
할당자는 2020년대 훨씬 전부터 널리 쓰였지만 최근 관심이 되살아난 것은 맞음. 더 넓게는 데이터 중심 설계에 집중하는 흐름의 일부라고 봄. 주 메모리 접근이 프로그램에서 가장 느린 작업 중 하나라는 점을 생각하면 합리적임.
Zig는 특정 방식을 강제하거나 특별히 선호하지 않음. 임의 순서로 해제할 힙 할당, 요청이나 프레임 종료 시 버릴 아레나, 고정된 버퍼를 이용하는 할당 모두 각각 적합한 할당자가 있음.
핵심은 표준 라이브러리가 특정 할당 전략을 전제하지 않는다는 것임. 현실의 소프트웨어에 만능 할당 전략이 없다는 뜻이라면 오히려 좋은 설계임.
메모리 할당은 어차피 발생하며, 컴파일러나 런타임에 맡기거나 코드에서 제어할 수 있음. 자동화된 방식은 대체로 훌륭하고 출발점으로 적절하지만, 성능이 우선이라면 선택지가 필요함.
프레임별 아레나는 ‘반복되는 작업 구간에서 상당한 메모리를 할당하는’ 일반적인 패턴의 한 예임. 웹·데이터베이스 서버의 요청 처리나 일반적인 명령줄 도구에서도 흔함.
사용자 정의 할당자의 유용성이 문제의 복잡도와 연결되는지는 잘 모르겠음. 임시 작업용 아레나는 핵심 실행 경로에서 수명이 시작하고 끝나는 동적 할당이 필요할 때, 내가 알기로 성능과 단순성을 사실상 공짜로 얻는 방식임.
Jai와 Odin이 집중하는 게임의 프레임별 작업에도 맞지만, 일반적으로는 복잡한 문제일수록 이런 상황이 더 자주 생길 것 같음.
프레임뿐 아니라 HTTP 요청별·발행/구독 메시지별로도 일반화할 수 있음. 매번 virtual.Arena를 새로 만들고 context.temp_allocator로 설정해 요청이나 메시지 처리 전체에서 사용한 뒤 통째로 버리면 됨.
글에서 Helix를 사용하고 Zig는 파일이 커지는 경향이 있다고 했는데, 그런 파일을 어떻게 탐색하는지 궁금함. 나는 큰 파일의 전체 구조를 볼 때 코드 접기를 자주 써서, 그 기능이 없는 것이 Helix 사용을 망설이는 이유임.
아레나 할당을 배워보길 권함. run_query(alloc) 안에서 arena = init_arena(alloc);로 생성하고 defer arena.deinit();로 정리하는 패턴을 사용할 수 있음.
아레나 할당의 첫 단계는 현실적인 벤치마크로 도입할 가치가 있는지 측정하는 것임. 요즘 표준 할당자는 매우 훌륭하고, mimalloc이나 jemalloc으로 교체하는 것만으로도 작업량과 오류 가능성을 훨씬 줄일 수 있음.
직관과 달리 CLI 도구는 작업의 마찰을 크게 줄여줌. 특히 아주 빠르게 작업을 진행할 때 효과적임.
두 언어를 충분히 잘 쓰지는 못하지만, Zig가 직관적이고 현대적이며 빠르고 C의 후계자가 될 잠재력이 있다는 평가에도 함수형 기능을 버리고 명령형으로 돌아가는 것은 후퇴처럼 느껴짐.
Rust는 명령형 언어이면서도 비용 없는 추상화인 반복자, 지연 평가, 대수적 자료형, 패턴 매칭, 모나드 타입, 트레이트, 클로저 등을 적극 활용함. 글의 라이브러리도 Queryable 계열 조합자를 통한 오류 처리, Data의 map·flat_map·reduce, 순수한 불변 변환, 반복자 조합, 지역적 추상화를 위한 클로저, 작은 내장 DSL 역할의 선언적 매크로, 합 타입과 곱 타입에 크게 의존함. 이 모든 것을 잃는 선택이 선뜻 납득되지는 않음.
깊이 아는 분야는 아니지만 명령형 중심 언어가 사라지거나 함수형 중심 언어가 C·C++만큼 대중화될지는 의문임. 명령형 언어가 투박하더라도 인간이 더 쉽게 이해하고, 대부분 충분히 쓸 만하므로 추진력이 크게 꺾일 것 같지는 않음.
Modula-2와 Object Pascal을 뒤로하면서 잃어버린 것이 바로 현대적이면서 매우 빠른 언어였음. 그 결과 새로운 세대는 스크립트 언어의 컴파일 언어 대안이 C와 C++뿐이었던 것처럼 생각하게 됨.
Rust는 함수형 언어를 C++ 모양으로 두드려 만든 언어에 가깝다고 봄. ML의 영향도 크게 받았음. 그렇게 설계한 이유는 이해하며, 덕분에 ‘C++를 더 깔끔하고 낫게 재구상한 언어’라는 분명한 자리에 더 잘 맞게 됨.
AI 자동 생성 콘텐츠
본 콘텐츠는 GeekNews의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기