https://www.youtube.com/watch?v=5_oqWE9otaE
개발자들의 목소리: 로리스 크로우와 함께하는 Zig 언어 탐험
진행자: 안녕하세요. 최근 프로그래밍 트렌드를 살펴보면 C 언어를 대체하려는 시도가 많이 보이는데요, 이는 현명하면서도 두려운 일이라고 생각합니다. C 언어는 50년이나 된 오래된 언어이기 때문에 현명한 선택이죠. 오래됐다고 나쁜 것은 아니지만, 50년이라는 시간 동안 프로그래밍 언어 디자인에서 무엇이 효과가 있고 무엇이 효과가 없는지에 대해 생각해 볼 시간이 충분했습니다. 50년 동안 축적된 기술을 실제로 적용해야 하는데, 바로 이 부분이 두려운 부분입니다. 왜냐하면 C 언어는 어디에나 있기 때문입니다. 여러분이 직접 C 언어로 코딩하지 않더라도, 여러분이 작성한 코드는 C 언어로 만들어진 무언가 위에서 실행될 것이고, 그 무언가는 또 다른 C 언어로 만들어진 무언가 위에서 실행될 것입니다. 우리가 설치하는 모든 것의 컴파일 스택에 C 언어가 존재합니다. 따라서 Go, Rust와 같은 언어가 새로운 C 언어가 되고 싶다면 정말 힘든 싸움을 해야 할 것입니다. 이 모든 것이 이번 주 주제를 더욱 흥미롭게 만듭니다. 바로 Zig 언어입니다. Zig는 시스템 프로그래밍 분야의 왕좌를 차지하기 위해 C, C++, Rust, Go와 경쟁하는 것뿐만 아니라 LLVM과 같은 C 언어 자체가 구축되는 인프라까지 대체하려고 합니다. 궁극적으로 모든 아키텍처에서 시스템 수준 소프트웨어를 구축하는 가장 좋은 방법이 되기를 희망하는 것이죠. Zig는 광범위한 목표를 가진 프로젝트이며, 프로그래밍 언어에 관심 있는 사람이라면 이번 주 에피소드에서 많은 것을 얻어 갈 수 있을 것입니다. 크로스 플랫폼 컴파일부터 메모리 관리 기술, Comptime 메타 프로그래밍에 대한 새로운 아이디어, 장기적인 목표를 가진 오픈 소스 프로젝트의 장기 자금 조달을 위한 구조까지 다룹니다. 다룰 내용이 많으니 평소보다 조금 긴 에피소드가 될 것 같네요. 바로 시작해 보겠습니다. 저는 여러분의 호스트 크리스 젠킨스이고 오늘의 목소리는 로리스 크로우입니다.
(음악)
진행자: 오늘 로리스 크로우와 함께합니다. 잘 지내시죠, 로리스?
로리스 크로우: 안녕하세요, 크리스. 네, 잘 지냅니다.
진행자: 함께하게 되어 반갑습니다. 언어 심층 분석을 할 때마다 항상 즐겁습니다. 저는 특히 프로그래밍 언어 분야에 관심이 많은데요, 오늘 로리스는 Zig에 대해 알려주실 예정입니다. TigerBeetle의 유란 더크 그리프가 쇼에 출연하여 Zig로 새로운 데이터베이스를 작성했다고 말하기 전까지는 저도 Zig에 대해 들어본 적이 없었습니다. 그래서 Zig에 대해 알아봐야겠다고 생각했습니다. Zig에 대해 배워야만 했죠. 먼저 새로운 프로그래밍 언어는 시중에 부족한 것에 대한 반응으로 탄생한다고 생각합니다. Zig가 존재해야 했던 절실한 이유, Zig의 존재 이유는 무엇이라고 생각하시나요?
로리스 크로우: 네, 맞습니다.
로리스 크로우: 사실적으로 답변하자면, Zig가 처음 만들어진 과정을 살펴보면 알 수 있습니다. Zig의 창시자인 앤드류 켈리는 전자 음악을 만들기 위한 디지털 오디오 워크스테이션 소프트웨어를 만들고 싶어했습니다.
진행자: 음악 제작용 소프트웨어 말이죠?
로리스 크로우: 네, 맞습니다. 그는 여러 언어를 시도해 봤지만, 각 언어가 제공하는 해결책이나 절충안에 만족하지 못했습니다. 처음에는 고수준 언어로 시작했지만, 실시간 오디오 처리를 하려면 자동 메모리 관리 기능이 없는 언어, 즉 하드웨어를 정밀하게 제어할 수 없는 언어를 사용할 수 없다는 것을 금방 깨달았습니다.
진행자: 오디오는 하드 리얼타임 처리가 필요한 분야 중 하나죠. 정확한 시간에 처리해야 하니까요.
로리스 크로우: 네, 그렇습니다. 반대로 당시 기계를 완전히 제어할 수 있는 주요 언어는 C와 C++였습니다. 각 언어에는 나름의 문제점이 있었는데, 일부는 개인적인 취향에 따라 다를 수 있습니다. 예를 들어 C는 매우 저수준 언어이지만 메타 프로그래밍 기능이 좋지 않습니다. C 매크로는 특히 좋지 않기로 유명하죠.
진행자: 네, 문자열 조작보다 나을 게 없죠.
로리스 크로우: 네, 맞습니다. 문자열을 가지고 장난을 치다 보면 원치 않는 부작용이 많이 발생하죠.
진행자: 네, 보통 위험한 도구라고 생각합니다.
로리스 크로우: 네, 맞습니다. 반면에 C++는 앤드류가 실제로 사용하려고 시도했는지는 모르겠지만, 일반적으로 C++는 매우 강력하고 복잡한 언어입니다. C++는 추상화가 많이 사용되는 언어이기 때문에, 때로는 달성하려는 목표에서 벗어나게 되는 경우가 있습니다. 물론 C++를 잘 활용하여 생산성을 높이는 사람들도 있지만, 어떤 사람들에게는 C++가 도구로서 편하게 느껴지지 않습니다.
진행자: 언어 전쟁을 시작할 생각은 없지만, C++에 대해 그렇게 생각하는 사람들이 있다는 것을 알 수 있습니다.
로리스 크로우: 네, 저도 그런 의견에 전적으로 동의합니다. C++의 방식을 좋아하는 사람들이 C++를 잘 활용할 수 있다는 것을 충분히 이해합니다. 하지만 저에게는 그 방식이 맞지 않습니다. 저는 다른 도구가 필요합니다. 앤드류도 저와 같은 생각을 공유한다고 생각합니다. 그래서 그는 오디오 워크스테이션에 적합한 저수준 언어, 즉 기계를 완전히 제어할 수 있지만 지나치게 복잡하지 않은 언어를 만들고 싶어했습니다. Zig 웹사이트 첫 페이지에서 볼 수 있는 문구 중 하나는 "프로그래밍 언어 지식을 디버깅하는 대신 애플리케이션을 디버깅하는 데 집중하세요"입니다.
진행자: 네, 알겠습니다. 저수준 환경에서 C와 경쟁하는 무언가를 만들면 항상 누군가는 "왜 Go가 아니고 왜 Rust가 아닌가?"라고 묻습니다. 로리스는 이미 그 부분에 대해 답변을 시작했는데요, 왜 Go나 Rust가 아닌가요?
로리스 크로우: Rust가 아닌 이유는 C++가 아닌 이유와 거의 같습니다. Rust는 복잡한 언어이며, 그 복잡성에서 많은 힘을 얻는 것은 사실입니다. 하지만 복잡성은 여전히 존재합니다. 그리고 기계를 완전히 제어할 수 있게 한다는 측면에서 Rust는 Zig와 의견이 다릅니다. Rust는 안전을 위해 unsafe Rust 내부에 특정 기능을 넣고 싶어합니다. unsafe Rust는 가볍게 사용해서는 안 되는 Rust의 일부입니다. 따라서 최대 성능과 안전 중 하나를 선택해야 하는 라이브러리를 보면, unsafe를 사용하면 감사를 받아야 하기 때문에 종종 성능보다 안전을 선택할 것입니다. 요약하자면, Rust는 Zig와는 약간 다른 목표를 추구합니다.
로리스 크로우: 성능과 안전 사이의 절충뿐만 아니라 추상화, 그리고 복잡성 측면에서도 읽기 쉽다는 점에서도 그렇습니다. 추상화된 코드는 이해하고 읽기 어렵기 때문입니다. Go의 경우, Go와 Zig는 모두 단순함을 중요하게 생각하지만, Go는 단순할 뿐만 아니라 매우 미니멀합니다. 따라서 Zig와 Go 사이에는 유사점이 있지만 모든 면에서 똑같은 생각을 가지고 있는 것은 아닙니다. 원하시면 나중에 자세히 설명해 드리겠습니다. 궁극적으로 Go는 Rust나 Zig만큼 저수준 언어가 아닙니다. Go는 런타임과 가비지 컬렉터를 가지고 있기 때문에 오디오 워크스테이션이나 운영 체제에 가장 적합한 선택이라고 생각하지 않습니다. 또한 C와의 상호 운용성도 약간 복잡합니다. 런타임을 가진 모든 언어는 C와의 상호 운용성이 조금 더 복잡해집니다. 왜냐하면 런타임과 가비지 컬렉터에 메모리에서 무슨 일이 일어나고 있는지에 대한 정보를 제공해야 하기 때문입니다. 그래서 때로는 조금 어색한 부분이 있습니다. 하지만 Go 팀의 경우 C와의 상호 운용성은 우선순위가 아니었고, 그들이 좋아하는 것도 아니었습니다. Go는 C 함수를 호출할 수 있지만, 그 반대는 쉽게 할 수 없습니다. Go 팀의 철학은 "우리는 다른 것을 하고 싶고, 사람들이 C에 너무 의존하는 것을 원하지 않는다. C 라이브러리를 사용할 수 있기를 원하지만, 그 반대는 원하지 않는다"라고 생각합니다. Go를 사용한다면 Go만 사용해야 합니다. Go 팀의 철학이 그런 것 같습니다.
진행자: "기존 C 코드를 재사용할 수 있기를 원하지만, C와 동일한 생태계에 살고 싶지는 않다"라는 뜻이군요.
로리스 크로우: 네, 맞습니다. Go의 컴파일 방식에서도 그런 철학을 엿볼 수 있습니다. 컴파일러 측면에서 말이죠. 하지만 합리적인 선택이고, 의미가 있습니다. Zig의 목표와는 매우 다릅니다. 언어 전쟁은 필요 없다고 생각합니다. 탐험해야 할 디자인 공간이 너무나 넓고, 모두를 위한 공간이 충분하기 때문입니다.
진행자: 네, 맞습니다. Zig가 이러한 설계 제약 조건에 대한 답은 무엇인가요?
로리스 크로우: 시스템 프로그래밍, 즉 저수준 프로그래밍에 대한 답변 중 가장 흥미로운 부분은 모든 것을 처음부터 다시 만든다는 것입니다. 다른 모든 언어가 아니더라도, C를 구축하는 추상화의 최하위 계층으로 생각하는 것이 일반적입니다. 예를 들어 C 코드로 컴파일되는 프로그래밍 언어가 있습니다. Nim이 그 예입니다. Rust는 C로 컴파일되지는 않지만, 예를 들어 Rust는 대상 플랫폼의 C 표준 라이브러리에 의존합니다. 따라서 Linux에 배포하려는 Rust 프로그램을 작성하는 경우, Rust는 Linux 배포판의 libc를 사용합니다. Zig의 경우, 모든 것을 처음부터 다시 만든다는 아이디어입니다. 엄청난 규모의 작업이고, 쉬운 일이 아닙니다. 하지만 그만큼 좋은 결과를 얻을 수 있습니다. 그만한 노력을 기울일 의향이 있다면 말이죠. 따라서 Zig의 가장 흥미로운 점은 모든 타겟을 위해 모든 타겟에서 빌드할 수 있다는 것입니다. 즉, 일반 컴퓨터뿐만 아니라 매우 작은 임베디드 장치도 쉽게 타겟팅할 수 있습니다. 크로스 컴파일도 쉽게 할 수 있습니다. Linux 머신(아마도 x86_64)에서 컴파일하여 매우 작은 ARMv8 임베디드 장치를 타겟팅할 수 있습니다. 컴퓨터에서 컴퓨터로 크로스 컴파일하는 것도 가능합니다. Zig는 Linux, Windows, Mac OS용 프로그램을 이러한 OS 중 어느 OS에서든 빌드할 수 있습니다. Linux에서 Windows로, Windows에서 Mac으로 등등 말이죠. 놀랍게도 드문 기능이지만 매우 유용합니다. 여기서 끝이 아닙니다. Zig 애플리케이션뿐만 아니라 C와 C++도 크로스 컴파일할 수 있습니다. Go는 Go용으로, Rust는 Rust용으로 크로스 컴파일할 수 있지만, C는 크로스 컴파일할 수 없습니다. Zig 코드와 C 종속성이 있는 프로젝트가 있다면 Zig 부분뿐만 아니라 C 부분도 크로스 컴파일할 수 있습니다.
진행자: 정말요?
로리스 크로우: 네, 이것이 Zig의 가장 큰 장점이라고 생각합니다. 너무나 큰 장점이라서 실제로 Rust와 C, 또는 Go와 C가 혼합된 프로젝트를 크로스 컴파일할 때 Zig를 C/C++ 컴파일러로 사용할 수 있습니다. 예를 들어 Go 개발자들은 Zig 컴파일러를 사용하여 C/Go 프로그램의 완전한 크로스 컴파일을 가능하게 했습니다. C/Go는 Go와 C 코드를 컴파일하고 링크하는 Go 컴파일러의 구성 요소입니다. 따라서 Go 프로젝트를 사용하는 Go 개발자들은 Zig를 사용하여 크로스 컴파일하고 있습니다. Rust도 마찬가지입니다. 심지어 AWS도 Zig를 사용하여 Lambda 엔진용 Rust Lambda를 크로스 컴파일하고 있습니다. Rust는 타겟의 libc에 의존하고, Lambda 함수를 실행하는 AWS 머신은 이전 libc가 있는 특정 버전의 Linux를 실행하기 때문에 모든 것이 원활하게 실행되도록 하려면 올바른 버전의 libc를 타겟팅해야 합니다. 일반적으로 컴파일러가 할 수 있는 일이 아니죠. C 컴파일에 신경 쓰지 않는 Rust는 당연히 할 수 없습니다. Zig를 사용하여 올바른 libc 버전에 링크할 수 있는 cargo-zigbuild라는 패키지가 있습니다.
진행자: Zig에 C 컴파일러도 내장되어 있다는 말씀인가요?
로리스 크로우: 네, 그렇습니다.
진행자: 앤드류는 오디오 워크스테이션을 만들려고 했는데, C 컴파일러도 포함된 언어를 만들게 된 거군요!
로리스 크로우: 네, 맞습니다. 정말 대단하죠! 그리고 아직 절반밖에 오지 않았습니다. 더욱 하드코어한 목표를 가지고 있습니다. 주제를 바꾸고 싶으시면 말씀해 주세요. 아니면 계속 말씀드릴 수도 있습니다.
진행자: 아니요, 너무 흥미롭습니다. 계속 말씀해 주세요.
로리스 크로우: Apple이 M1 아키텍처를 출시했을 때 기억하시나요? Intel에서 ARM으로 전환했죠. 꽤 좋은 CPU, 꽤 좋은 아키텍처였기 때문에 큰 뉴스였습니다. 새로운 Mac은 강력하고 발열이 적어서 좋습니다. Apple이 M1을 출시했을 때 Zig는 다른 타겟, 즉 다른 머신에서 M1용으로 크로스 컴파일할 수 있는 최초의 컴파일러였습니다. 물론 Apple은 M1을 개발하는 동안에도 M1용으로 컴파일할 수 있는 도구를 가지고 있었지만, M1을 출시했을 때는 다른 머신에서 M1용으로 컴파일할 수 있는 도구를 공개하지 않았습니다.
진행자: 그래서 새로운 Mac용으로 빌드하려면 새로운 Mac을 사야 했군요.
로리스 크로우: 네, 맞습니다. macOS를 사용하면 LLVM의 포크인 clang을 얻게 됩니다. Apple이 자체 시스템에 맞게 만든 비공개 패치가 포함된 LLVM이라고 할 수 있습니다. M1이 출시되었을 때 Apple은 새로운 아키텍처에 맞는 패치를 가지고 있었기 때문에 M1에서 M1으로 컴파일할 수 있었습니다. 하지만 오픈 소스 프로젝트인 LLVM 자체는 아직 M1을 완전히 지원하지 않았기 때문에 Windows나 Linux에서 LLVM을 가져와서 M1용으로 컴파일할 수 없었습니다. Zig가 최초였습니다. Zig는 C 컴파일러이기 때문에, 사실 Zig 컴파일러는 그 자체로는 그렇게 인상적이지 않습니다. Zig는 LLVM을 사용하고, LLVM은 최적화된 머신 코드를 생성하기 위한 통합 프레임워크와 같은 라이브러리입니다. 컴파일러가 컴파일하려는 프로그램을 읽고 메모리에 데이터 구조를 빌드하고 의미 분석을 수행하는 등 언어가 해야 할 모든 일반적인 작업을 수행한 다음, 마지막 단계에서 LLVM에 정보를 제공하면 LLVM이 타겟팅하는 CPU에 맞는 정확한 명령어를 선택합니다.
진행자: 웹어셈블리와 비슷하다고 할 수 있겠네요. 실제로 최종 머신 코드를 생성하는 매우 저수준 언어 말이죠.
로리스 크로우: 과장된 비유일 수도 있지만, 맞는 말입니다. LLVM IR(중간 표현)이라고 합니다.
진행자: 일종의 바이트 코드를 생성하는 거죠. 웹어셈블리와 비슷하다고 생각합니다.
로리스 크로우: 네, 맞습니다. LLVM에 전달하면 LLVM이 이미 번들로 제공되기 때문에 LLVM에서 실행되는 C 컴파일러인 clang을 추가하는 것은 어렵지 않습니다. 이것이 Zig가 C를 빌드할 수 있게 해줍니다. 하지만 컴파일러는 최종 실행 파일을 만드는 데 필요한 한 단계일 뿐입니다. 마지막에 링크 단계도 있습니다. 새로운 M1 Mac의 주요 문제는 링크가 이전과 달라야 한다는 것이었습니다. Zig가 M1용으로 크로스 컴파일할 수 있는 최초의 컴파일러였던 이유는 자체 링커를 가지고 있었기 때문입니다. Zig 프로젝트의 핵심 팀원 중 한 명인 제이콥 코나는 Microsoft에서 일했었는데, 우리가 그를 영입했습니다. 아니, 그가 스스로 영입했다고 해야겠네요. 그는 링커 작업을 하고 싶어했습니다. Microsoft에서는 그렇게 흥미로운 일을 하고 있지 않았던 것 같습니다. 그래서 그는 Microsoft를 떠나 Zig 프로젝트에 풀타임으로 참여하기로 했습니다. 그래서 우리는 자체 링커를 가지고 있고, 대부분의 작업은 제이콥이 합니다. 제가 하고 싶은 말은 자체 링커를 가지고 있었기 때문에 LLVM보다 훨씬 빠르게 기능을 구현할 수 있었다는 것입니다. LLVM은 일반적으로 매우 좋은 프로젝트로 여겨지고 실제로 그렇습니다. 하지만 LLVM을 기준으로 생각하지 않고 LLVM을 넘어서 직접 작업을 수행할 의향이 있었기 때문에 이러한 결과를 얻을 수 있었습니다. 이제 앞으로 나아가서, 아까 말씀드렸던 우리 여정의 절반 지점에 왔다는 것과 연결되는데요, 앞으로 LLVM을 완전히 선택적인 구성 요소로 만들 계획입니다. 즉, LLVM이 하는 일부 기능을 자체적으로 구현할 것입니다. 우리는 이를 "머신 백엔드"라고 부릅니다. 컴파일러의 내부 표현, 즉 내부 데이터 구조를 읽고 출력할 명령어를 결정하는 자체 구현을 가지고 있습니다. 지원하려는 각 아키텍처마다 이러한 머신 백엔드를 작성해야 하기 때문에 많은 작업이 필요합니다. x86_64, ARM 32비트, ARM 64비트, x86 32비트 등등 많은 아키텍처가 있습니다. 각 아키텍처마다 특정 머신 백엔드를 작성해야 하고, 타겟팅하는 OS에 따라서도 다른 부분을 작성해야 합니다. x86_64 Windows는 x86_64 Mac과 약간 다릅니다. 프로그램의 명령어 측면에서는 그렇지 않지만, 실행 파일의 구조, 주변 메타데이터, 프레임 측면에서는 그렇습니다.
진행자: 일종의 프레임이라고 할 수 있겠네요.
로리스 크로우: 네, 맞습니다. 그리고 지금 그 작업을 하고 있습니다. 현재 LLVM의 최적화 기능을 대체하기 위한 작업은 아닙니다. LLVM이 하는 일의 대부분은 최적화이고, LLVM은 최적화 분야에서 최고 수준입니다. 아직 그 부분까지는 손대지 않고 있습니다. 현재 하고 있는 작업은 LLVM 없이도 디버그 빌드(최적화되지 않은 빌드)를 할 수 있도록 하는 것입니다. 이것이 시작점이지만, 앞으로 나아가서 경쟁력 있는 최적화 백엔드를 만들 계획입니다. 원한다면 여전히 LLVM을 사용할 수 있을 것입니다. 실제로 어떻게 될지는 중요하지 않다고 생각합니다. 패키지 관리자를 통해 LLVM을 가져와야 할 것입니다. 즉, 컴파일러 자체에 번들로 제공되는 대신 Zig 패키지 관리자를 사용하여 LLVM을 가져와야 할 것입니다. 하지만 LLVM으로 최적화된 최종 실행 파일을 얻을 수 있을 것입니다. 우리는 경쟁력 있는 버전을 개발할 것이고, 여러분은 어떤 버전을 더 좋아하는지 결정할 수 있을 것입니다. 시간이 지남에 따라 우리가 좋은 성능을 낼 수 있다면, 우리의 경쟁 버전이 LLVM보다 더 매력적이어서 사람들이 LLVM 대신 우리 버전을 사용하게 될 수도 있습니다.
진행자: 정말 최하위 수준까지 내려가는군요!
로리스 크로우: 네, 그렇습니다.
진행자: 엄청난 야망입니다! 하지만 사용자 공간으로 다시 돌아와서, Zig를 사용하는 것이 어떤지 알고 싶습니다. 프로그래머로서 무엇을 알아야 할까요?
로리스 크로우: Zig 자체는 매우 간단합니다. Zig에서 가장 복잡한 부분은 컴파일 타임에 코드를 실행할 수 있는 기능인 Comptime입니다. 런타임과 Comptime, 프로그램의 두 가지 수명 단계에 대해 생각해 본 적이 없다면, Comptime에 무엇을 할 수 있고 무엇을 할 수 없는지에 대해 약간 혼란스러울 수 있습니다. 하지만 전반적으로 핵심 원칙은 매우 간단하다고 생각합니다. 시스템 프로그래밍을 하게 되면 복잡해집니다. JavaScript나 Python 개발자이고 스택과 힙의 차이점에 대해 생각해 본 적이 없다면, 아니, Python 프로그래머라면 대학교에서 저도 그랬지만, 스택과 힙에 대해 들어봤더라도 그냥 철학적인 개념일 뿐입니다.
진행자: 흥미롭긴 하지만 실제로 사용할 일은 없죠.
로리스 크로우: 네, 맞습니다. 제어할 수 없으니까요. 하지만 저수준 시스템 프로그래밍을 하게 되면 갑자기 중요한 문제가 됩니다.
진행자: 네, 맞습니다.
로리스 크로우: 예를 들어, 길이가 6인 배열과 길이를 모르는 배열의 차이점을 이해하는 것이 어려울 수 있습니다. 길이가 6인 배열은 항상 6개의 요소를 가지거나, 최대 6개의 슬롯을 사용할 수 있고 실제로 사용하는 슬롯 수는 카운터로 표시됩니다. 하지만 6개가 한계입니다. Comptime에 정적으로 알고 있다면 스택에 넣을 수 있고, 6개 요소로 제한되어 있다는 것을 알기 때문에 이 메모리로 할 수 있는 몇 가지 작업이 있습니다.
진행자: 프로그램의 전체 수명 동안 정확히 얼마나 많은 메모리가 필요한지 알 수 있죠. 컴파일러에게 매우 중요한 정보입니다.
로리스 크로우: 네, 맞습니다. 저수준 언어는 정적으로 알고 있는 것과 정적으로 알 수 없는 것에 대한 개념을 중심으로 설계됩니다. 예를 들어, 사용자에게 입력할 항목 수를 묻고 사용자가 10,000개를 입력할 수 있는 프로그램이 있다면, 또는 더 현실적으로 JSON 파일을 파싱한다고 상상해 보세요. JSON 파일은 임의로 깊게 중첩되거나 크기가 클 수 있습니다. 이 경우 힙 할당에 신경을 써야 합니다. Python이나 JavaScript에서는 런타임이 관리해 주기 때문에 직접 할 필요가 없습니다.
진행자: 네.
로리스 크로우: 또 다른 예는 OS가 제공하는 API입니다. 사람들은 때때로 OS API를 언어가 제공하는 기능이라고 생각하지만, 언어는 중개자 역할만 할 수 있습니다. 예를 들어, 사람들은 때때로 Zig에서 터미널 창의 크기를 어떻게 얻는지 묻습니다. 진짜 질문은 "OS가 어떻게 정보를 얻을 수 있도록 하는가?"입니다.
진행자: 네, 맞습니다.
로리스 크로우: OS에서 정보를 얻을 수 있는 시스템 콜이 있을 것이고, 두 번째 질문은 "누군가가 시스템 콜에 액세스하기 위한 상용구 코드를 작성했는가? Zig 표준 라이브러리 어디에 있는가?"입니다. Zig 또는 각 특정 언어에서 이러한 작업을 수행하는 방법에 대한 질문은 완전히 틀린 것은 아니지만, 언어가 정확히 무엇을 보여주려고 하는지, 지나치게 단순화된 인터페이스를 제공하려고 하는지 이해하는 것이 조금 어려울 수 있습니다. 때로는 고수준 프로그래밍 언어에서도 이러한 인터페이스를 제공하는 경우가 있습니다. 고수준 프로그래밍 언어에서는 모든 것에 대한 저수준 액세스를 제공할 필요가 없기 때문입니다. 따라서 Zig를 배우는 데 있어 가장 어려운 부분은 이러한 부분이라고 생각합니다. 시스템 프로그래밍을 배우려면 "OS가 어떻게 작동하는가?"라는 관점에서 생각해야 합니다. 또 다른 예로, 사람들은 때때로 터미널에서 컬러 텍스트를 출력하는 방법, Zig에서 어떻게 하는지 묻습니다. 답은 Zig는 이러한 부분에 신경 쓰지 않는다는 것입니다. 이스케이프 코드이고, 사용하는 터미널 에뮬레이터와 관련된 여러 가지 문제가 있습니다. Zig와는 무관한 문제입니다. 하지만 사람들은 이러한 관점에서 생각하지 않습니다. 따라서 Zig를 배우는 데 있어 가장 어려운 부분이라고 생각합니다. 그리고 관련된 자료도 많지 않습니다.
진행자: 아직 모든 것에 대한 라이브러리가 없고 모든 것에 대한 문서가 없는 젊은 언어의 일반적인 문제인 것 같습니다.
로리스 크로우: 네, 맞습니다. 하지만 Zig가 시스템 프로그래밍을 처음 발명한 것은 아닙니다. 핵심 원칙을 너무 자세히 설명하지 않고 잘 가르쳐주는 좋은 책이 있으면 좋겠습니다. 실제로 이러한 내용을 가르치려는 책은 많지만, 제 경험상 제가 본 대부분의 책은 C와 OS를 혼동하는 경향이 있습니다. 예를 들어, 시스템 프로그래밍을 가르치려는 책이 있는데, C 컴파일 모델이 어떻게 작동하는지, 라이브러리가 어떻게 구성되어 있는지, 특정 파일이 시스템에 어떻게 배치되는지 등을 설명합니다. 이 모든 것은 사실이고 구체적이며, 40년 전에는 특히 사실이고 구체적이었습니다. 하지만 매크로와 같은 개념, 특정 라이브러리가 어디에 있는지와 같은 개념은 C에만 해당하는 것입니다. C를 사용하지 않는다면 이러한 내용은 책에서 생각하는 것만큼 시대를 초월하지 않습니다. 반면에 스택과 힙의 차이점은 훨씬 더 시대를 초월합니다. 따라서 개인적으로는 스택과 힙의 차이점과 같이 정말 시대를 초월하는 시스템 프로그래밍 개념과 더 이상 관련 없는 개념을 구분할 수 있는 학습 자료가 부족하다고 생각합니다.
진행자: 수명 주기는 길지만 수학적으로 순수하지는 않다고 할 수 있겠네요.
로리스 크로우: 네, 맞습니다. C 인터럽트가 어떻게 작동하는지 이야기해 보겠습니다. 아까 터미널 창의 크기를 얻는 예를 들었는데요, 실제로 코딩을 시작할 때 시스템 콜을 찾아보고 OS 라이브러리를 통해 액세스해야 할까요?
로리스 크로우: OS에 따라 다릅니다. 간단하게 Linux라고 가정해 보겠습니다. Linux에서는 시스템 콜이 OS의 공개 API로 간주되기 때문에 OS의 C 라이브러리를 사용할 필요가 없습니다. 시스템 콜을 직접 호출할 수 있습니다. Zig는 모든 것을 처음부터 다시 만들고 싶어하기 때문에 Zig 표준 라이브러리에서 OS에서 정보를 얻을 수 있는 시스템 콜인 ioctl을 구현했습니다. 따라서 Linux에서는 이렇게 정보를 찾을 수 있습니다. 하지만 다른 플랫폼에서는 C 라이브러리를 사용해야 합니다. Zig에는 C 라이브러리에 대한 바인딩도 있기 때문에 실제로 모든 것을 처음부터 다시 만들 필요는 없습니다. 하지만 SQLite와 같은 C 라이브러리를 사용하려는 경우를 상상해 보세요. SQLite는 Go에서도 많이 사용되는 인기 있는 라이브러리입니다. SQLite를 번들로 제공하는 Go 프로젝트가 많지만, SQLite는 C 프로젝트입니다. 따라서 SQLite를 번들로 제공하려는 경우 Zig를 사용하여 크로스 컴파일하는 것이 주요 사용 사례 중 하나입니다. 어쨌든 SQLite를 사용하려는 경우, 기본적으로 C에서는 구현이 포함된 C 파일과 정의가 포함된 헤더 파일(확장자가 .h인 파일)이 있습니다. 헤더 파일에는 전체 구현이 포함되어 있지 않고 함수의 시그니처만 포함되어 있습니다.
진행자: 원래 API 문서라고 할 수 있겠네요.
로리스 크로우: 네, 맞습니다. 원래 API 문서입니다. RESTful API를 문서화하는 데 사용되는 OpenAPI, 예전에는 Swagger라고 불렸던 것과 비슷합니다. 시스템 프로그래밍에서도 비슷한 개념입니다. SQLite는 여러 개의 C 파일과 하나의 헤더 파일을 제공합니다. 공개 API에 액세스하려면 프로젝트에 헤더 파일을 포함해야 합니다. Zig에서는 이 작업을 직접 수행할 수 있습니다. Zig에서는 C 헤더 파일을 가져올 수 있고 바로 작동합니다. 헤더 파일을 가져오면 바로 모든 정의에 액세스할 수 있습니다.
진행자: 흥미롭네요. 작성해야 하는 브리징 파일이 없는 건가요?
로리스 크로우: 브리징 파일은 자동으로 생성됩니다.
진행자: 브리징 파일을 볼 필요가 없다는 말씀이군요.
로리스 크로우: 네, 맞습니다. 원한다면 수동으로 할 수도 있습니다. 헤더 파일을 가져와서 Zig 정의로 변환하고, 수동으로 조정해야 할 부분이 있으면 조정할 수 있습니다. 하지만 가장 일반적인 방법은 헤더 파일을 직접 가져오고 Zig가 내부적으로 브리징을 처리하도록 하는 것입니다.
진행자: 네, 알겠습니다. 그러면 SQLite 함수를 바로 호출할 수 있겠네요. SQLite 문서를 읽어보면, SQLite를 초기화하는 sqlite_init 함수가 있을 것입니다. 그냥 호출하면 작동합니다. Zig는 C와의 상호 운용성을 돕는 몇 가지 다른 기능도 제공합니다. 예를 들어, C는 널 종료 문자열을 많이 사용합니다. 함수에 문자열을 전달할 때 문자열의 시작 부분에 대한 포인터를 전달합니다. 포인터에는 길이에 대한 정보가 포함되어 있지 않습니다. 길이는 호출하는 함수가 문자열을 반복하면서 널 문자(0 바이트)를 찾을 때까지 확인합니다. 널 바이트를 찾으면 문자열이 끝난 것을 알 수 있습니다. 최신 언어는 더 이상 이러한 방식을 좋아하지 않습니다.
진행자: 저도 나이가 들어서 예전에는 그렇게 하지 않았던 때를 기억합니다.
로리스 크로우: 네, 맞습니다. 예를 들어, Zig에서 일반적으로 문자열은 끝에 널이 있는 데이터의 시작 부분에 대한 포인터가 아니라 슬라이스를 사용합니다. 다른 언어에서는 이를 팻 포인터라고 부르기도 합니다. 포인터에는 길이에 대한 정보도 포함되어 있습니다.
진행자: 포인터와 길이, 두 가지 정보를 모두 가지고 있군요. 때로는 C에서도 길이를 원하는 API가 있습니다. 널 바이트를 찾는 대신 길이를 전달해야 합니다. 하지만 항상 두 개의 별도 인수, 두 개의 로리스 크로우: 별도의 값을 전달해야 합니다. 어쨌든 Zig는 C와의 상호 운용성을 어떻게 돕나요? Zig의 문자열 리터럴은 널 종료되지 않습니다. 기본적으로 "Hello world"라고 쓰고 Zig에서 해당 문자열 리터럴을 사용하려는 경우 포인터와 길이가 됩니다. "Hello world"가 몇 글자인지 모르겠네요. 10글자인가요, 9글자인가요? 전통적으로 끝에 느낌표를 넣는지 여부에 따라 다릅니다. 어쨌든 길이 정보를 가지고 있지만 문자열 끝에는 널 바이트도 있습니다. 따라서 Zig 문자열 리터럴을 가져와서 C에 투명하게 전달할 수 있습니다. 아무것도 할 필요가 없습니다. 항상 작동합니다. 더 일반적으로 Zig는 표준 라이브러리에 널 종료 문자열을 처리할 수 있는 함수를 많이 제공합니다. 널 종료 문자열은 Zig에서 선호하는 문자열 유형이 아닙니다. 일반적으로 Zig에서는 문자열을 널 종료로 처리하지 않습니다. 하지만 널 종료 문자열은 C와의 상호 운용성뿐만 아니라 OS와의 상호 운용성 때문에 현실입니다. OS API, libc는 C이고, 시스템 콜도 종종 C의 방식을 따릅니다. 데이터를 전달하는 방식이 C를 반영하는 경우가 많습니다.
진행자: 당연하죠. OS가 C로 작성되었으니까요.
로리스 크로우: 네, 맞습니다. OS가 C로 작성되었기 때문에 당시에는 그렇게 하는 것이 일반적이었고, 지금도 그러한 방식이 남아 있습니다. 요약하자면 Zig는 문자열 길이, C 함수와 함께 사용할 수 있는 다양한 연산자를 제공합니다. 투명하게, 쉽게 사용할 수 있습니다. 한 가지 예를 들자면, defer를 사용할 수 있습니다. defer는 Go의 defer와 거의 같은 개념입니다. 약간의 차이점이 있지만, 기본적으로 함수를 종료할 때 리소스를 해제하려는 경우 free 또는 fclose를 호출해야 합니다. 리소스를 해제하는 함수를 호출해야 합니다. 함수의 모든 종료 지점에서 호출을 복사하여 붙여넣는 대신 한 줄에 파일을 열고 그 아래 줄에 defer fclose를 넣을 수 있습니다.
진행자: 리소스를 생성한 직후에 정리 코드를 넣을 수 있고, 어떻게 범위를 벗어나든 범위를 벗어날 때마다 함수가 호출됩니다. 닫기 호출이나 해제 호출을 끝에 넣는 것을 잊어버리는 것은 항상 불만족스럽습니다. 언젠가는 잊어버릴 것이 뻔하니까요.
로리스 크로우: 네, 맞습니다. defer는 함수의 분기 경로에 대해 너무 신경 쓰지 않아도 됩니다. C++의 소멸자만큼 편리하지는 않을 수도 있습니다. C++ 소멸자는 자동으로 실행되기 때문에 defer를 작성할 필요조차 없습니다. 하지만 C++ 소멸자는 C++에서만 작동합니다. Zig의 defer는 C 함수에서도 호출할 수 있습니다. 완전히 투명합니다. 따라서 Zig는 C보다 C 라이브러리를 더 잘 사용할 수 있다는 재미있는 결과가 나옵니다. C에서는 동일한 정리 루틴을 구현하려면 goto를 사용해야 할 수도 있습니다. 정리 함수가 있는 함수 섹션에 레이블을 지정하고 goto를 사용하는 것은 드문 일이 아닙니다. 하지만 defer가 없기 때문에 C에서는 정리가 매우 복잡해질 수 있습니다.
진행자: 믿기지 않네요.
로리스 크로우: 네, 정말입니다. C에 새로운 구문을 추가하여 개선한 셈입니다. 이 주제를 떠나기 전에 포인터에 대해 질문해야겠습니다. Zig에서 포인터 연산을 사용할 수 있나요?
로리스 크로우: Zig에서 포인터 연산을 사용할 수 있습니다. 좋은 지적입니다. 잊고 있었네요. 상호 운용성을 돕는 또 다른 훌륭한 개선 사항입니다. Zig에서 포인터 연산을 사용할 수 있습니다. 기계가 허용하는 작업이고, 때로는 OS API에서 포인터 연산을 요구하는 경우도 있습니다. 하지만 일반적으로 Zig에서는 포인터 연산을 사용하지 않습니다. 특히 Zig에서는 타입 시스템에서 포인터 연산을 수행할 수 없습니다. 포인터를 가져와서 정수로 변환해야 합니다. 런타임에 아무것도 하지 않는 연산입니다. 타입 시스템 관련 작업입니다. 포인터를 가져와서 숫자로 해석하고 숫자에 연산을 적용한 다음 다시 포인터로 변환해야 합니다. 따라서 원하거나 필요한 경우 포인터 연산을 수행할 수 있지만, 언어가 쉽거나 편하게 만들어 주지는 않습니다. 약간의 마찰이 있습니다. 반대로 이러한 종류의 작업이 어디에서 발생하는지 매우 쉽게 파악할 수 있습니다.
진행자: Zig를 일상적으로 작성하기보다는 C와의 상호 운용성을 위해 주로 사용된다는 말씀이군요.
로리스 크로우: 네, 맞습니다. C와의 상호 운용성이라고 하지만, 다른 용도도 있을 수 있습니다. 펌웨어를 생각해 보세요. 작은 임베디드 장치를 프로그래밍하고 매우 저수준 작업을 수행해야 하는 경우 포인터 연산이 필요할 수 있습니다. C에만 해당하는 것은 아니지만, 매우 저수준의 비트 조작 작업을 수행해야 하는 경우 포인터 연산이 필요할 수 있습니다. 하지만 그 외에는 일반적으로 포인터 연산을 사용하지 않습니다. 포인터와 관련된 또 다른 중요한 점은 C의 포인터는 매우 불명확하게 지정되어 있다는 것입니다. char *를 보면 포인터라는 것을 알 수 있고, 역참조하면 문자를 얻을 수 있습니다. 하지만 포인터가 널일 수 있는지 여부는 알 수 없습니다. 문서에 나와 있을 수도 있지만 확실하지 않습니다. 두 번째 질문은 "포인터 끝에 문자가 있는데, 널이 아니라고 가정하면 문자가 하나만 있는 건가요? 아니면 문자열처럼 뒤에 문자가 더 있고, 널을 만날 때까지 문자가 계속 있나요? 아니면 다른 변수를 통해 몇 개의 항목을 가져와야 하나요?"입니다. C의 타입 시스템에는 이러한 정보가 전혀 인코딩되어 있지 않습니다. Zig에서는 이러한 모든 것이 다른 유형의 포인터입니다. 포인터가 널일 수 있다면 optional 포인터입니다. 최신 언어에서 하는 것처럼 optional 개념을 사용하고 optional을 역참조해야 합니다. Zig에서는 optional을 사용하여 널 포인터를 나타냅니다. 하지만 Zig 슬라이스(포인터와 길이) 외에도 하나의 항목, 하나의 특정 항목을 가리키는 포인터에 대한 유형이 있습니다. 하나의 문자, 하나의 char만 있다는 것을 명시적으로 나타냅니다. 알 수 없는 개수의 문자를 가리키는 포인터에 대한 구문도 있습니다. 특정 구문을 사용하여 "알 수 없는 개수의 항목을 가리키는 포인터입니다"라고 나타냅니다. 포인터 자체에는 개수에 대한 정보가 포함되어 있지 않습니다. 그리고 끝에 널 종료 문자가 있는 알 수 없는 개수의 문자를 가리키는 포인터가 있습니다. 이러한 정보는 타입 시스템에 있습니다. 예를 들어, 실수로 다른 문자열에서 문자열을 만들려고 하고 문자열 중간에서 작은 슬라이스를 가져와서 다른 애플리케이션에 전달하려고 합니다. 그런데 해당 API가 끝에 널 종료 문자를 기대한다는 것을 잊어버렸습니다. 문자열 중간에서 두 글자만 잘라냈기 때문에 널 종료 문자가 없습니다. Zig 타입 시스템은 컴파일 오류를 발생시킵니다. "널 종료 문자열이 필요한데, 다른 문자열에서 수행한 슬라이싱 작업은 널 종료 문자열을 생성하지 않습니다"라고 알려줍니다. 따라서 프로그램이 임의의 가비지를 읽고 충돌하는 대신 컴파일 오류가 발생합니다.
진행자: 네, 맞습니다. Zig가 C를 사용해야 하지만 C를 사용하고 싶지 않은 사람들에게, Rust를 사용해 봤지만 borrow checker와 친해지지 못한 사람들에게 이상적인 언어라는 것을 알 수 있습니다. 특히 문자열 주변의 타입 안전성 측면에서 말이죠. 메모리 관리는 어떤가요? 또 다른 큰 문제점이죠.
로리스 크로우: 네, 맞습니다. 인체 공학적 측면에서 말하자면, 아까 설명했던 defer 문은 메모리 관리에 큰 도움이 됩니다. 리소스를 할당하고 defer free를 사용하면 됩니다. C에서처럼 malloc, free를 명시적으로 호출할 필요가 없습니다.
진행자: 네, 맞습니다.
로리스 크로우: 좀 더 구체적으로 말씀드리자면, Zig에는 전역 할당자가 없습니다. C에서는 malloc이 할당자이고, 프로젝트마다 다른 malloc 구현을 사용할 수 있습니다. 경쟁하는 구현이 몇 가지 있습니다. Zig에서는 할당자를 항상 명시적으로 전달합니다. 함수가 메모리를 할당하려면 할당자를 입력으로 받아야 합니다.
진행자: 흥미롭네요.
로리스 크로우: 이렇게 하면 메모리를 할당하는 함수를 훨씬 쉽게 감사할 수 있습니다. 함수가 할당자 또는 할당자를 번들로 제공하는 데이터 구조를 받지 않으면 메모리를 할당할 수 없습니다. 예를 들어, C++의 vector와 같은 역할을 하는 arraylist가 있습니다. 크기가 조정 가능한 배열입니다. arraylist를 만들 때 할당자를 제공하고, arraylist를 전달할 때 arraylist는 할당자에 대한 참조를 내부적으로 번들로 제공하기 때문에 메모리를 할당할 수 있습니다. 편의를 위한 것입니다. 하지만 일반적으로 함수가 메모리를 할당할 수 있는지 여부를 매우 쉽게 감사할 수 있습니다. 함수가 메모리를 해제하는 것을 잊어버렸는지 여부를 정적으로 확인하는 데 도움이 되나요? 아니요. Rust처럼 될 의향이 없다면 그럴 수 없습니다. Rust는 borrow checker가 이해할 수 있는 메모리 관리 전략 유형에 제한이 있지만, 정적으로 모든 할당이 해제되었는지 여부를 확인할 수 있습니다. 하지만 Zig에서는 디버그 빌드에서 기본 할당자를 계측하여 누수를 검사할 수 있습니다. 기본적으로 테스트 스위트의 일부입니다. 특별히 계측할 필요가 없습니다.
진행자: 네, 좋네요.
로리스 크로우: 테스트를 실행할 때 할당자는 테스트가 끝날 때 메모리가 할당된 상태로 남아 있으면 테스트를 실패시킵니다.
진행자: 특별히 계측할 필요 없이 기본적으로 제공되는 기능이군요.
로리스 크로우: 네, 맞습니다. 또 다른 관점도 있습니다. 프로그램이 특정 상황에서 메모리를 누수하는 것은 정당한 동작일 수 있습니다. Zig 컴파일러 자체를 예로 들겠습니다. Zig 컴파일러는 디버그 모드로 빌드하면 종료할 때 모든 것을 해제합니다. 하지만 릴리스 모드로 빌드하면 종료할 때 메모리를 해제하지 않습니다. OS가 메모리를 정리해 줄 것이기 때문에 프로그램이 종료되기 직전에 할당된 모든 항목을 해제할 필요가 없습니다. 프로그램이 종료되기 직전에 작은 항목을 해제하는 것은 프로그램이 많은 메모리를 사용하거나 수명이 매우 긴 경우에만 의미가 있습니다. 그렇지 않으면 시간이 지남에 따라 점점 더 많은 메모리를 소비하게 되고 결국 사용 가능한 모든 메모리를 소모하여 모든 것이 폭발합니다. 하지만 컴파일러와 같이 한 번 실행되는 프로그램의 경우 유틸리티를 실행하고 끝까지 실행한 다음 닫으면 됩니다. 마지막에 정리하는 것은 시간 낭비입니다. Visual Studio를 사용해 본 적이 있나요?
진행자: 너무 오래 전이라 기억이 나지 않습니다.
로리스 크로우: 다행히도 저도 한동안 사용할 필요가 없었습니다. 하지만 7년 전쯤에는 계속 사용해야 했는데, 로드하는 데 시간이 너무 오래 걸리는 것도 짜증 났지만 닫는 데 시간이 너무 오래 걸리는 것이 정말 짜증 났습니다. 왜 닫는 데 시간이 오래 걸릴까요? 닫는 동안 모든 구성 요소와 하위 구성 요소, 하위 하위 구성 요소의 소멸자를 실행하고 해제하려고 하기 때문입니다.
진행자: Eclipse에서도 그런 경험을 한 적이 있습니다. 그냥 강제 종료하는 습관이 생겼습니다. 어차피 신경 쓰지 않으니까요.
로리스 크로우: 네, 맞습니다. Eclipse는 소멸자가 있는 또 다른 언어인 Java를 사용합니다. 그래서 사람들은 소멸자를 많이 사용하고 싶어하지만, 실제로는 기능 측면에서 사용자에게 제공하는 기능 측면에서는 그럴 필요가 없는 경우가 있습니다. 그냥 바로 닫고 싶을 뿐입니다. 요약하자면, 프로그램이 특정 상황에서 메모리를 누수하는 것은 정당한 동작일 수 있습니다. Eclipse와 Visual Studio가 바로 종료되면 사용자 경험이 크게 향상될 것입니다. 물론 모든 메모리를 정리하는 토글, 플래그가 있어야 합니다. 원치 않는 누수가 없는지 확인할 수 있도록 말이죠. Visual Studio는 오래 실행되는 프로그램이고 Eclipse도 마찬가지입니다. 따라서 정상적인 작동에서 메모리가 누수되어서는 안 됩니다. 따라서 여전히 테스트할 수 있어야 합니다. 메모리 관리 전략은 크게 두 가지입니다. 하나는 제한된 리소스이기 때문에 사용하는 메모리에 매우 신경 쓰는 것입니다. 하지만 프로그램이 종료될 때 사용하는 메모리는 그냥 버릴 수 있습니다.
진행자: 네, 맞습니다. 중요한 또 다른 주제가 있습니다. 평소보다 시간을 좀 더 할애해야 할 것 같습니다. 이 주제에 너무 흥미가 생겨서요.
로리스 크로우: 좋습니다. 저는 시간이 많습니다.
진행자: 좋습니다. Comptime에 대해 이야기해 보겠습니다. Lisp 프로그래머로서 Comptime은 익숙한 개념이지만 주류가 된 적은 없는 것 같습니다. 런타임 프로그램과 Comptime 프로그램의 차이점에 대해 이야기해 볼까요?
로리스 크로우: 네, Zig가 Comptime을 어떻게 처리하는지 좀 더 자세히 설명해 드리겠습니다. Zig의 Comptime은 흥미롭습니다. Zig는 런타임 타입 정보가 없는 언어이기 때문입니다. 예를 들어 JavaScript, Python, Go에서는 런타임에 프로그램에 타입에 대한 질문을 할 수 있습니다. 반면에 C 프로그램은 런타임이 없고 일반적으로 런타임 타입 정보가 없습니다. 항상 그런 것은 아니지만 일반적으로 런타임 타입 정보는 언어의 실제 런타임과 함께 제공되는 경향이 있습니다. 예를 들어 Python에서는 런타임에 새로운 타입을 만들 수 있고, introspection을 수행할 수 있습니다. 따라서 동적 속성을 제공할 수 있는 런타임은 일반적으로 런타임 타입 정보를 가지고 있는 것이 좋습니다. C에는 그러한 기능이 없습니다. C의 구조체는 결국 메모리의 오프셋으로 귀결됩니다. 구조체가 16바이트이고 첫 번째 필드가 오프셋 0에 있고 다른 필드가 오프셋 8에 있다면 그것으로 끝입니다. 다른 정보는 없습니다. 하지만 적어도 정적으로 타입을 검사하고 추론할 수 있는 것이 유용합니다. Zig가 바로 그렇게 합니다. Zig는 런타임 타입 정보를 제공하지 않지만 Comptime 타입 정보를 제공합니다. 따라서 런타임에 새로운 타입을 만들 수는 없지만 Comptime에 다른 타입을 기반으로 새로운 타입을 만들 수 있습니다. 다른 타입을 기반으로 새로운 타입을 만드는 방법은 다른 언어의 제네릭과 같습니다. 다만 다른 명령형 언어에서는 일반적으로 특이한 선언적 구문과 다이아몬드 괄호를 사용합니다. 다이아몬드 괄호를 사용하여 제네릭 타입을 나타내고 선언적 구문을 사용하여 제약 조건을 지정합니다. "T 타입은 인터페이스 A 또는 인터페이스 B를 준수해야 합니다"와 같이 말이죠.
진행자: Comptime을 사용하여 "A 리스트를 원하지만 8비트 정수 리스트로 고정해야 합니다"와 같은 작업을 수행한다는 말씀이군요.
로리스 크로우: 네, 맞습니다. 다른 기존 타입을 참조하여 새로운 타입을 만들고 있습니다. Zig에서는 이 작업을 사용자 정의 구문이 아니라 일반 Zig 구문을 사용하여 수행합니다. 예를 들어, 제네릭 리스트를 만들고 정수 리스트, 문자 리스트 등을 만들 수 있도록 하려면 타입을 입력으로 받는 list라는 함수를 만듭니다. 타입은 Comptime 매개변수로 표시되어야 합니다. 따라서 시그니처는 "fn list(comptime T: type)"과 같습니다. T라는 이름의 Comptime 매개변수로, 타입을 전달해야 합니다. 정수 또는 다른 타입을 전달할 수 있습니다. 그런 다음 이 함수는 다른 타입을 반환하고 함수 본문에서 구조체 정의를 만들고 반환합니다. 구조체 정의는 전달한 타입인 T 타입의 페이로드 필드를 정의합니다. 제네릭이 작동하는 방식과 비슷하지만 Comptime에 실행되는 일반적인 절차적 Zig 코드입니다. 예를 들어, 간단한 배열을 만들고 배열의 길이를 다른 추론의 결과로 지정하려면 Comptime에 피보나치 함수를 실행하고 배열의 길이를 10번째 피보나치 수로 지정할 수 있습니다. 10번째 피보나치 수가 얼마인지는 모르겠지만 10보다 클 것입니다. 컴파일하는 동안 컴파일러가 해석하는 일반 Zig 코드를 호출할 수 있습니다. 다른 언어에도 이러한 기능이 있지만 완전히 범용적이지는 않습니다. 속성을 지정하는 데 사용할 수 있는 제한된 언어를 제공하고 고유한 특별 규칙을 가지고 있습니다. Zig에서는 Zig 코드를 실행하고 컴파일러가 실행 횟수를 추적합니다. 예를 들어, 실수로 피보나치 수 1,000개로 배열을 만들었는데 피보나치 구현이 매우 나쁘면 컴파일러가 잠시 후 "10,000번 루프를 실행했는데 결론에 도달할 수 없어서 포기했습니다"라고 알려줍니다. 실제로 무한 루프가 아니라고 생각되면 실행 횟수를 늘려서 다시 시도할 수 있습니다. 이렇게 Zig 컴파일러는 무한 루프와 결정할 수 없는 문제를 처리합니다.
진행자: 컴파일 시간이 무한히 길어지는 것을 방지하는군요.
로리스 크로우: 네, 맞습니다. 앨런 튜링은 왜 자동화할 수 없는지에 대한 의견을 가지고 있습니다.
로리스 크로우: 네, 맞습니다. 우리의 경우 포기하는 것으로 해결 불가능성 문제를 해결하는 것이 실용적이라고 생각합니다. 궁극적으로 프로그램을 컴파일하려는 것이고 영원히 앉아서 기다릴 수는 없기 때문입니다.
진행자: 그렇군요. 그러면 두 가지 질문이 자연스럽게 떠오릅니다. 첫 번째 질문은 프로그래머로서 어떤 느낌인가요? 대부분의 프로그래머는 제네릭에 다이아몬드 괄호를 사용하는 데 익숙합니다. Zig 방식이 더 나은가요? 익숙해지면 자연스럽게 느껴지나요?
로리스 크로우: 엄청나게 자연스럽게 느껴집니다. 아까 Lisp를 언급하셨는데, 저도 Lisp를 좋아합니다. 전문적으로 사용해 본 적은 없지만 대학교에서 가장 좋아하는 과목 중 하나였습니다. Lisp 매크로 작성도 좋아했습니다. Zig의 Comptime은 Lisp 매크로 작성과 비슷하지만 훨씬 더 좋습니다.
진행자: 꽤 과감한 의견인데요.
로리스 크로우: Lisp 매크로가 좋다고 하는 이유는 Lisp가 homoiconic 언어이기 때문입니다. 언어 자체가 언어를 나타내는 데이터 구조인 리스트입니다.
진행자: 심볼릭 표현식이죠.
로리스 크로우: 네, 맞습니다. 하지만 프로그램이 데이터 구조이기 때문에 자연스럽게 프로그램을 데이터 구조로 취급하게 된다고 생각합니다. 텍스트 변환이 아니라요. 실제로 Lisp에서는 일반화가 잘 되지 않는 매크로, 특정 인수가 리스트인지 여부, 인용되었는지 여부에 대한 가정을 하는 매크로를 작성할 수 있습니다. Zig의 Comptime은 Lisp 매크로보다 제한적입니다. 의도적으로 그렇게 설계되었습니다. 80/20 규칙과 같은 것입니다. 80%의 성능을 제공하지만 사람들이 자주 사용하는 20%의 복잡성을 줄여줍니다.
진행자: 저주받은 코드를 작성하지 않도록 보호해 주는군요.
로리스 크로우: 네, 맞습니다. Zig에서는 타입을 확인할 때 typeinfo 함수를 호출하고 구조체를 전달합니다. person이라는 구조체를 만들고 person에 age와 name 필드가 있다고 가정해 보겠습니다. person에 typeinfo를 호출하면 해당 타입에 대한 모든 정보가 들어 있는 데이터 구조가 반환됩니다. 특히 필드가 몇 개인지, 필드의 이름은 무엇인지, 타입은 무엇인지 등에 대한 정보가 들어 있는 배열이 포함됩니다. 따라서 메타 프로그래밍은 항상 프로그램을 구문이 아니라 데이터로 봅니다. 이것이 Comptime을 이상하게 자연스럽게 만드는 비결이라고 생각합니다.
진행자: 네, 알겠습니다. Lisp 매크로의 문제점은 타입이 없어서 일반 Lisp보다 더 쉽게 문제를 일으킬 수 있다는 것이었습니다.
로리스 크로우: 네, 맞습니다. 하지만 좋은 점은 Comptime에 작동하는 프로그램과 런타임에 작동하는 프로그램을 작성하는 데 차이가 없다는 것입니다. 도구, 언어, 모든 것이 동일합니다.
로리스 크로우: Zig도 마찬가지입니다. 동일한 구문을 사용하기 때문입니다. Comptime 개념을 소개하려고 쓴 블로그 게시물에 예제가 있습니다. 가장 좋아하는 예제는 실제 경험에서 가져온 것입니다. Zig용 Redis 클라이언트를 작성하고 있었는데, Redis의 쿼리 언어는 대소문자를 구분하지 않는 명령어를 사용합니다. 대문자로 쓰든 소문자로 쓰든 상관없습니다. 따라서 클라이언트에서 이러한 명령어를 인식하고 싶었습니다. 두 문자열의 동일 여부를 확인하고 싶었죠. 성능을 약간 향상시키기 위해 프로그램에 하드 코딩한 상수 문자열, 즉 사용자가 제공한 문자열과 비교하는 데 사용하는 문자열 리터럴이 항상 대문자라는 것을 알고 있다면 비교 코드를 간소화할 수 있습니다. 비교에서 한 가지 분기를 제거할 수 있습니다. 하지만 equal 함수를 호출할 때 항상 첫 번째 인수로 대문자 문자열을 전달하도록 강제하고 싶습니다.
진행자: Comptime에 문자열을 확인하는 코드를 작성하고 싶다는 말씀이군요.
로리스 크로우: 네, 맞습니다. 다이아몬드 괄호를 사용해서 그렇게 할 수 있는지 모르겠습니다. Zig에서는 함수 본문에서 Comptime 블록을 엽니다. 먼저 첫 번째 인수를 항상 Comptime에 사용할 수 있는 것으로 표시해야 합니다. 따라서 사용자는 첫 번째 인수를 제공해야 합니다. 문자열 리터럴일 필요는 없고 변수 이름일 수도 있지만, 궁극적으로 변수에 포함된 값은 Comptime에 해결할 수 있어야 합니다. 네트워크와 같이 이상한 것에 의존해서는 안 됩니다.
진행자: 네.
로리스 크로우: Comptime 블록을 열고 그 안에서 for 루프를 사용하여 문자열을 반복하고 각 문자가 올바른 범위에 있는지 확인합니다. 그게 전부입니다. 이상한 것은 없습니다. 언어를 사용하여 문자열을 한 글자씩 확인하고 예상 범위에 없는 문자를 찾으면 컴파일 오류를 발생시킵니다. 제 경우에는 대문자 A에서 대문자 Z 사이였습니다. 컴파일 오류 메시지는 "대문자 문자열을 전달해야 하는데 대문자 문자열을 전달하지 않았습니다. 이 문자는 소문자입니다"와 같이 원하는 대로 작성할 수 있습니다. 이 메시지가 컴파일 오류가 됩니다. 따라서 API 사용자는 제약 조건을 준수해야 합니다. 잘못된 문자열을 전달하면 컴파일 오류가 발생합니다. 또한 컴파일 오류 메시지는 직접 작성한 것이기 때문에 사용자는 컴파일러에서 "대문자 문자열을 전달해야 하는데 그렇게 하지 않았습니다"라는 친절한 메시지를 받게 됩니다.
진행자: 좋네요. 새로운 언어를 배우지 않고도 맞춤형 컴파일러 확장을 만들 수 있군요.
로리스 크로우: 네, 맞습니다.
진행자: 언어의 범위를 잘 알겠습니다. 이제 중요한 주제에 대해 이야기해 보겠습니다. Zig 프로젝트의 자금 조달 방식이 매우 흥미롭다고 생각했습니다. 모든 언어, 특히 모든 오픈 소스 프로젝트는 충분한 작업을 수행하는 데 어려움을 겪습니다. 언어가 성공하려면 본업을 그만두어야 하기 때문입니다. Zig의 자금 조달 방식은 매우 참신합니다. 자세히 설명해 주시겠어요?
로리스 크로우: Zig는 501(c)(3) 비영리 재단입니다. 501(c)(3)는 미국 법률 시스템에서 자선 단체와 같은 것입니다.
진행자: 우리가 일반적으로 자선 단체라고 생각하는 것과 같은 것이군요.
로리스 크로우: 네, 맞습니다. 세금이 면제되고 배당금을 지급할 수 없습니다. 따라서 조직에 들어오는 모든 돈은 조직의 사명을 추구하는 데 사용해야 합니다. 기본적으로 회사를 운영하는 데 돈을 사용해야 합니다. 돈을 빼서 요트를 사거나 할 수 없습니다. Zig만 501(c)(3)인 것은 아닙니다. Python도 501(c)(3)입니다. 하지만 모든 언어가 그런 것은 아닙니다. 어떤 언어는 비영리이지만 다른 유형의 조직입니다. 세금을 내야 합니다. 일반적으로 501(c)(6)입니다. 3과 6의 차이가 별로 없어 보일 수도 있습니다. 특히 소프트웨어 버전 관리에서는 패치 번호가 있는 경우가 많기 때문에 "501(c)(6), 501(c)(3) 뭐가 다르지? 버그를 수정한 건가?"라고 생각할 수도 있습니다. 하지만 엄청난 차이가 있습니다.
진행자: 버전 번호가 항상 일관성 있는 것은 아니지만, 변호사들은 버전마다 규칙을 바꿀 수 있습니다.
로리스 크로우: Zig는 주로 기부금으로 운영됩니다. 수입의 대부분은 사람들이 재단에 기부하는 돈에서 나옵니다. Zig 개발을 계속할 수 있도록 말이죠. 다른 수입원도 있지만, 파이 차트를 그리면 기부금이 대부분을 차지할 것입니다. 수입원의 균형을 유지하려고 노력합니다. 한 개체 또는 소수의 개인이 재단을 통제하는 상황을 원하지 않기 때문입니다. 법적으로는 그렇지 않더라도 돈줄을 통제하면 궁극적으로 조직의 운명을 통제하게 됩니다.
진행자: 교묘하게 말이죠.
로리스 크로우: 네, 맞습니다. 우리는 사람들에게 "아니오"라고 말할 수 있어야 합니다. Uber와 지원 계약을 맺고 있습니다. Uber는 Zig를 사용하여 크로스 컴파일을 하고 있습니다. 현재 Uber는 크로스 컴파일이 필요한 모든 백엔드 서비스를 크로스 컴파일하고 있습니다. 주로 ARM 서버 때문입니다. Uber는 Intel(x86_64)뿐만 아니라 ARM 서버도 사용하고 싶어했습니다. 그래서 Zig를 사용하고 있으며, 현재 Uber는 모든 C/C++ 코드가 올바르게 크로스 컴파일되도록 작업을 수행했습니다. Uber와 지원 계약을 맺고 있지만, Uber에서 받는 돈은 수입의 큰 부분을 차지하지 않습니다. 수입과 관련된 이야기입니다. 우리는 독립적이기를 원하고, 이 부분에 대해 매우 진지합니다. 사람들은 때때로 "언어가 성공하려면 대기업의 지원을 받아야 합니다. 대기업의 지원 없이는 성공적인 언어를 만들 수 없습니다"라고 말합니다. 우리는 그렇게 생각하지 않습니다. 우리의 표준 제안은 "대기업이 재단의 0%와 이사회 의석 0개를 대가로 얼마든지 돈을 줄 수 있습니다"입니다. 물론 Zig는 얻을 수 있습니다.
진행자: 뭔가 얻는 것이 있긴 하지만 권력은 없습니다.
로리스 크로우: 네, 맞습니다. 권력도 없고 통제권도 없습니다. 절대 없습니다. 우리는 Zig가 BDFL(자비로운 종신 독재자)이 운영하는 프로젝트이기를 원하기 때문입니다. 다른 언어와 비교했을 때, 우리는 궁극적으로 창시자인 앤드류가 최종 결정권자입니다. 물론 앤드류 혼자서 결정하는 것은 아닙니다. 핵심 팀이 있고, 프로세스가 있습니다. 프로세스는 매우 공개적입니다. GitHub에서 언어 변경 제안을 읽을 수 있고, 공개적으로 토론이 이루어집니다. 누구나 참여할 수 있습니다. 하지만 민주적인 프로세스는 아닙니다. 기능 제안에 많은 찬성표가 있다고 해서 해당 기능이 Zig에 포함될지 여부가 결정되는 것은 아닙니다.
진행자: 단점도 있지만 일반적으로 디자인 일관성 측면에서는 장점이 있습니다.
로리스 크로우: 네, 맞습니다. 언어를 작게 유지하고 싶다면, 언어가 결국 모든 것을 다 갖춘 잡동사니가 되는 것을 원하지 않는다면 절대적으로 중요하다고 생각합니다. 501(c)(3)를 선택하고 501(c)(6)를 선택하지 않거나, 조직을 협회처럼 만들지 않는 것이 바로 그 방법입니다.
진행자: 회사 또는 금융 조직으로서 어떻게 구성되어 있는지가 언어의 설계에 영향을 미친다는 생각이 흥미롭네요.
로리스 크로우: 엄청난 영향을 미칩니다. 프로그래머들은 이런 것들을 생각하고 싶어하지 않습니다. "그냥 코드에 집중하고 싶어요"라고 말합니다. 저도 그 마음을 이해합니다. 저도 그냥 코드에 집중하고 싶습니다. 하지만 제가 배운 어려운 교훈은 최고의 기술을 만들려면 비즈니스 측면을 제대로 해야 한다는 것입니다. 비즈니스 측면이 항상 우선입니다. 비즈니스 측면에서 실수를 하면 기술이 영향을 받습니다. 장기적으로는 그렇습니다. 단기적으로는 그렇지 않지만 장기적으로는 큰 영향을 미칩니다.
진행자: 네, 맞습니다. Comptime에 대한 이야기가 흥미로웠습니다. 하지만 이제 마무리해야 할 것 같습니다. 청취자들이 런타임으로 돌아갈 시간을 줘야죠. 멋진 연결이네요! Zig를 시작하려는 사람들에게 조언을 해 주시겠어요? Zig는 LSP 지원, VS Code 플러그인 등 초보자를 위한 다양한 기능을 제공합니다. 하지만 어디서부터 학습을 시작해야 할까요?
로리스 크로우: 공식 웹사이트인 ziglang.org를 방문하는 것을 추천합니다. 학습 섹션에는 Zig를 다운로드하고 설치하는 방법에 대한 가이드가 있고, 학습 리소스에 대한 링크도 있습니다. 개인적으로 추천하는 세 가지 리소스는 언어 참조, 표준 라이브러리 문서, ziglings입니다. 시작점으로 언어 참조를 추천합니다. 언어 참조는 표준 라이브러리가 아니라 언어 자체에 대한 문서입니다. 언어의 구문에 대해 구체적으로 설명하고 있습니다. 한 페이지 분량으로, A4 용지나 US Letter 용지가 아니라 그냥 웹 페이지 한 페이지입니다. 그렇게 길지 않고, 처음부터 끝까지 다 읽을 필요도 없습니다. 훑어보면 됩니다. 하지만 언어 참조를 통해 Zig에 대한 기본적인 이해를 얻을 수 있습니다. 저수준 프로그래밍 경험이 없고 부드러운 학습 곡선을 원한다면 ziglings를 추천합니다. ziglings는 커뮤니티 프로젝트로, 저장소를 복제하면 컴파일되지 않거나 올바르게 동작하지 않는 작은 프로그램 모음을 얻을 수 있습니다.
진행자: 아, 네.
로리스 크로우: 주석에 수정 방법이 적혀 있습니다. 하나씩 수정해 나가면서 Zig를 배울 수 있습니다. 예를 들어, "이 프로그램은 'Hello world'를 출력해야 하지만 그렇지 않습니다. 수정하세요"와 같은 주석이 있습니다. 처음에는 매우 간단합니다. 문자열 리터럴을 수정하면 됩니다. 하지만 진행하면서 연습 문제는 점점 어려워지고 더 많은 구문을 이해해야 합니다. 저는 Closure를 그렇게 배웠습니다. Closure Koans라는 비슷한 프로젝트가 있습니다. 수정해야 하는 작은 실패 프로그램 모음이고, 점차 전체 언어를 배우게 됩니다. 새로운 언어를 배우는 좋은 방법입니다. ziglings는 매우 인기가 있습니다. Zig 생태계에서 가장 인기 있는 교육 콘텐츠라고 할 수 있습니다. ziglings라는 이름도 rustlings에서 영감을 받았습니다. Rust에도 같은 프로젝트가 있습니다.
진행자: rustlings라고 부르죠.
로리스 크로우: 네, 맞습니다. 쇼 노트에 두 프로젝트에 대한 링크를 추가해 두겠습니다. 시간 내주셔서 감사합니다, 로리스. Zig는 C보다 더 큰 범위를 가진 매력적인 언어입니다.
로리스 크로우: Zig는 C의 거의 모든 범위를 가지고 있습니다. C가 어떤 이유에서인지 수정하지 않은 모든 것을 수정하려고 합니다. Zig가 C를 크로스 컴파일할 수 있는 이유를 생각해 보세요. C 컴파일러는 그렇게 할 수 없습니다. 자세히 설명하지는 않았지만, Zig 컴파일러를 가져와서 C로 "Hello world"를 작성하면 Linux에서 Windows로 컴파일할 수 있습니다. clang으로는 할 수 없습니다.
진행자: 시도조차 해보지 않겠습니다.
로리스 크로우: 정말 말도 안 됩니다. 따라서 Zig의 범위는 C의 모든 범위, C가 해야 했지만 하지 않은 모든 것, 그리고 약간의 추가 기능입니다.
진행자: 한동안 바쁘게 지낼 수 있겠네요. 로리스, 시간 내주셔서 감사합니다.
로리스 크로우: 감사합니다.
진행자: 로리스와의 대화를 녹음한 이후로 로리스가 언급한 ziglings 튜토리얼을 사용해 보았습니다. 정말 좋은 학습 방법입니다. 예전에 사용했던 Arduino 마이크로컨트롤러를 다시 꺼내서 사용해 볼 계획입니다. 예전에 임베디드 하드웨어를 조금 다뤄봤지만 C를 작성하는 것은 만족스럽지 못했습니다. Rust를 사용하는 것은 좋았지만 임베디드 하드웨어에 컴파일하는 것이 쉽지 않았습니다. Zig가 드디어 저를 만족시켜줄 수 있기를 바랍니다. 납땜 인두, 전선, LED 등을 가지고 놀 때 말이죠. 그동안 우리가 이야기한 모든 것에 대한 링크를 쇼 노트에 추가해 두었습니다. Zig에 대한 정보, 학습 방법, Zig의 기능, 다루지 못한 추가 기능 등이 있습니다. 재미있는 이스터 에그도 하나 알려드리겠습니다. Zig를 설치하고 "zig zen"을 입력하면 Zig가 존재하는 이유를 알려줍니다. 여러분이 직접 확인해 보세요. 떠나기 전에 좋아요, 공유, 평점, 리뷰를 남겨 주세요. 어떤 주제에 가장 관심이 있는지 알려주시면 해당 주제에 대한 에피소드를 더 많이 만들 수 있습니다. 아직 구독하지 않았다면 구독 버튼을 눌러 다음 에피소드를 놓치지 마세요. 다음 에피소드까지, 저는 여러분의 호스트 크리스 젠킨스였습니다. 로리스 크로우와 함께한 개발자들의 목소리였습니다. 들어주셔서 감사합니다.
(음악)