2P by GN⁺ 9시간전 | ★ favorite | 댓글 1개
  • Zig 0.15 버전에서 새로운 IO 인터페이스(std.Io.Reader, std.Io.Writer) 가 도입됨
  • 기존 IO 방식의 복잡성 및 퍼포먼스 이슈 개선 목적, 하지만 실제 사용법 혼란 발생
  • tls.Clientbuffer 사용 관련, 불일치된 파라미터 전달 방식이 혼란을 더함
  • 기본적인 사용 예시 구현 중에도 여러 버퍼 크기, 옵션 필드 지정 등 복잡한 요구사항 존재
  • 공식 문서와 코드 예시, 편의 함수 부족으로 입문자에게 직관적이지 않음

Zig 0.15에서 도입된 새로운 IO 인터페이스와 배경

  • Zig 0.15 버전에서 std.Io.Readerstd.Io.Writer라는 새로운 IO 타입이 도입됨
  • 이전 IO 인터페이스는 성능 문제와 타입 혼합, 그리고 anytype 남용 등으로 인해 복잡성 유발
  • 새로운 IO 구조에서 인터페이스 간 명확한 타입 구분 및 성능 개선이 주요 목표임

tls.Client와 IO 인터페이스 사용의 실제 문제점

  • 기존 smtp 라이브러리 갱신 과정에서 tls.Client.init 함수 사용법에서 혼란 발생
  • 문서상 init 함수는 Reader와 Writer 포인터, 옵션 세트를 인자로 받도록 명시됨
  • Zig의 net.Stream은 각각 reader() , writer() 메서드로 Stream.Reader/Writer를 반환
    • 하지만 Stream.Reader/Writer와 std.Io.Reader/Writer는 정확히 같은 타입이 아니기에 변환 필요
    • Reader는 interface() 메서드 호출, Writer는 &interface 필드를 사용해야 하므로 일관성 부족

버퍼와 옵션 필드 설정 문제

  • stream.writer, stream.reader는 각각 버퍼를 인자로 받음
    • Buffer가 새로운 IO 인터페이스에서 필수적인 요소로 강조됨
  • tls.Client.init 호출 시 ca_bundle, host, write_buffer, read_buffer 등 네 가지 옵션 필드가 반드시 필요함
    • 옵션 파라미터로 넘기는 값과, 인자로 직접 넘기는 값 분리 규칙이 불명확하게 느껴짐
var tls_client = try std.crypto.tls.Client.init(
  reader.interface(),
  &writer.interface,
  .{
    .ca = .{.bundle = bundle},
    .host = .{ .explicit = "www.openmymind.net"; } ,
    .read_buffer = &read_buf2,
    .write_buffer = &write_buf2,
  },
)
  • 실제로 buffer 포인터가 제대로 주어지지 않으면 프로그램이 제대로 동작하지 않거나 hang, crash 등 다양한 문제가 발생함

Reader 사용시 직관성 문제

  • tls.Client의 reader 필드 자체는 "Decrypt된 스트림"임에도, 실제로 std.Io.Reader에는 일반적인 read 메서드가 존재하지 않음
  • 대신 peek, takeByteSigned, readSliceShort 등 덜 직관적인 메서드만 제공됨
  • 그나마 사용에 가까운 API는 stream 메서드를 통해 버퍼로 데이터를 읽어오는 방식임
var buf: [1024]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);
const n = try tls_client.reader.stream(&w, .limited(buf.len));

전체 코드 예시와 실전 문제

  • 전체 동작하는 최소 단위 예시를 만들려 해도 옵션, 버퍼 크기, 타입 변환 등 신경 쓸 부분이 많음
  • 테스트/문서/예시 부족으로 학습난이도와 진입장벽 높음
  • Zig 언어 내에서의 일관성 혹은 underlying design에 대한 이해가 부족할 경우, 이상하게 느껴지는 포인트 많음
  • 표준 라이브러리 내에서도 해당 방식이 많이 쓰이지 않아 실전 참고자료가 부족함

경험과 결론

  • std.fmt.printInt 등 네이밍 변경, API design 변화 등으로 migration 과정 자체가 쉽지 않음
  • reader.interface(), &writer.interface 방식이나 옵션 전달 방식, 여러 개의 버퍼 필요성 등 반복되는 여러 어려움 경험
  • TLS 등의 네트워크/보안 프로토콜이 익숙하지 않은 입장에서 요구사항 파악이 더욱 어렵게 느껴짐
  • 종합적으로, 기존 대비 명확성과 문서화, 편의성 개선 측면에서 아직 미흡한 부분 다수 존재
Hacker News 의견
  • Author임을 밝힘. 드디어 제대로 동작하게 만듦. 암호화 writer와 stream writer 모두 flush 과정이 필요했고, 동시에 읽기 쪽에도 문제가 있었음. 스트리밍은 동작하긴 하지만, Writer.Fixed가 sendFile을 구현하지 않아 처음 읽을 때는 항상 0을 반환함. 첫 호출 이후 내부적으로 스트리밍 모드에서 읽기 모드로 전환되면서 갑자기 모든 게 동작하기 시작함(관련 코드 링크: Zig File.zig #L1318). 지금은 websocket 라이브러리에 압축 기능을 다시 켜려고 노력 중임

    • "Flush를 잊지 마세요"라는 유튜브 밈이 떠오름 (유튜브 영상)

    • 최소 놀람의 원칙(principle of least surprise)은 대체 어디로 갔는지 궁금함

    • 이전 인터페이스에서 지금 이 상황으로 넘어온 것이 참 대단하게 느껴짐. 깜짝 놀람이 큼

  • Zig PM(프로덕트 매니저)은 아니지만, OP가 겪은 문제에 대한 명백한 첫 번째 해결책은 더 나은 문서화와 더 많은 사용 예시를 만드는 일임(너무 많아도 상관없음). 이런 작업을 하며 사용자에게 너무 많은 걸 시키고 있진 않은지 돌아볼 좋은 기회가 될 수 있음. 만약 추구한 목표가 절대적인 성능 또는 성능 저하를 부르는 추상화의 도입을 피하는 것이었다면 그 목표는 달성한 것 같지만, DX(개발자 경험)는 안드로메다로 간 느낌임

    • Zig 커뮤니티의 문화를 잘 모르는 것 같음. 문서 부족을 불평하면 누구나 "stdlib 코드 직접 읽으라"는 댓글이 쏟아질 준비를 해야 함. 대부분의 API가 이 글에서처럼 사용이 어렵고, HTTP나 파일 시스템 같은 기본 작업조차 익숙지 않으면 정말 힘듦. 그래서 정말 실력 있는 사람만 살아남음

    • 문서 작성에는 비용이 들고 시간도 걸림. 그 시간에 Zig의 다른 부분을 개선할 수도 있음. 작업 중인 코드라면 완전히 자리잡을 때까지 문서를 미루는 것도 합리적인 선택임. 물론 문서화는 좋지만, 새로운 기능, 중요한 버그 수정, 또는 문서 작업 중에서 우선순위를 정해야 하는 경우 항상 모두 가질 수 있는 것은 아님

    • zig는 무엇을 하지 말아야 하는지 지시하는 데 너무 초점을 맞춘 것 같음. 여러가지 방법과 사용 예시를 모아서 잘 정리하고 알려주는 쪽으로 발전했으면 좋겠음. 이 인터페이스의 문서 누락이 대표적인 예임

    • 좋은 문서나 예제를 쓰는 데에는 많은 노력이 필요함. 지금 zig에서 벌어지는 변화 폭을 보면, 제대로 정착되기 전에 문서를 만들어도 금방 소용이 없어짐

    • 나는 Zig 개발자는 아니지만, Zig의 문서가 너무 간단한 이유 중 하나는 언어가 아직 젊고 계속 진화 중이기 때문일 것이라 생각함. 지금 쓴 문서가 미래에 곧 틀릴 걸 알면서도 시간과 에너지를 들이는데 어려움이 있다는 점이 이해됨

  • Zig 언어 그 자체는 정말 괜찮은데, 표준 라이브러리는 아직 많이 미완성이고, 계속 바뀌고, 많이 부족하고, 일부는 지나치게 추상적이거나 또 너무 저수준임. 지금은 표준 라이브러리 대신 OS API를 직접 쓰는 게 낫다고 생각함. 베타 테스터 각오가 아니라면 표준 라이브러리 피하는 걸 추천함

    • 실제로 나도 zig를 쓸 때는 대개 OS API 위주로 사용 중임. cImports가 잘 되어 있어서 zig 정의 만들기 귀찮을 때도 쉽게 쓸 수 있음

    • 내가 볼 때 Zig는 너무 많은 걸 한꺼번에 하려는 경향이 있어서, 내가 생각하는 최소 품질선조차 도달하지 못하고 있음. 사용자에게 급격한 변화와 실험을 감수하도록 강요하는 모습에서, 충분히 많은 사람이 투자해서 "1.0 전이면 깨져 있어도 괜찮아, 언젠가 좋아질거야"라는 허상을 믿을 수밖에 없는 상황임(결론: 그 날은 절대 오지 않을 것 같음). 다른 사람에게 자신의 실험을 부담시키는 것은 바람직하지 않다고 생각함. 불안정하다고 미리 고지했다 해도, 의존하지 말라고 했다 해도, 이미 rug pull(갑자기 모든 게 바뀜)을 당하는 사람 입장에서 문제임. zig가 무엇인지 잘 모르겠음. Matklad는 machine level language라고 하며(관련 인터뷰: lobste.rs - Matklad 인터뷰), 공식 페이지에서는 robust, optimal, reusable general-purpose 언어라고 함. 이 둘은 서로 모순됨. 그리고 수동 메모리 관리가 필요 없는 문제도 많아서, zig는 결코 범용 언어가 아님. 결국 이 모든 혼돈이 zig의 불안정성과 비대한 표준 라이브러리에 드러남. 심플함과 범용성을 주장하면서 이 정도의 큰 라이브러리는 모순임. Async 역시 모든 플랫폼에서 보편적으로 효율적으로 구현될 수 없는 기능임에도 만능 해결책인 것처럼 약속함. 예전엔 함수 착색 문제를 해결했다며 홍보하다가 그 시도는 이미 버려졌음. 제대로 해낼 수 있다고 다시 믿으라는 논리가 이상함. 실제로 모든 플랫폼에서 컴파일러를 구현하는 데 필요한 기본 어셈블리 명령만 있으면 되는데, luajit은 아예 파서를 순수 어셈블리로 구현했고, 어디서든 잘 동작함. 나는 대부분 lua로 프로그래밍하고, 인터프리터에서 버그를 거의 만난 적 없음. zig가 luajit보다 잘 해결해줄 문제도 떠오르지 않음. 만약 zig로만 해결되는 게 있다 해도, 그걸 lua 코드에 embed해서 FFI로 연동하면 됨. 대부분의 코드가 그 정도의 저수준 최적화가 정말 필요하지 않음. zig 도입하면 오히려 골치 아파짐. 요즘 zig에 대한 과장된 기대가 AI 수준의 현실과 괴리에 도달함. zig를 믿으려면 없는 능력을 언젠가 갖게 된다는 허망한 희망을 믿어야 함. 실제로 실행 계획도 없이, 그냥 "조금만 더 기다려"라는 식임

  • 나는 라이브러리나 인터페이스가 내 타입의 버퍼 할당을 요구하는 게 이해가 안 됨. 내가 파싱하는 거라면 굳이 라이브러리가 필요 없고, 쓸 거라면 교환을 깨뜨릴 수 있는 일이기도 함. go의 독특한 인터페이스는 일부 인터페이스가 writer 인터페이스를 확장하거나(hijacker 인터페이스 참고), request 객체가 여러 미들웨어에서 다양하게 재활용되는 점 때문임. 요약하면 요청(request)은 확장될 필요가 없지만, 응답(response)은 websocket, tcp 래퍼 등 다양한 형태로 변화할 수 있음

    • 라이브러리가 외부 버퍼 할당을 요구하는 것이 이상하게 느껴지지 않음. 유연성을 주는 대신 더 많은 수작업이 필요함. 예를 들어, 이미 만들어둔 버퍼 풀이 있다면 재사용하고 싶을 수 있음. 만약 타입이 내부에서 독자적으로 할당하면 그게 불가능함. 혹은 미리 모든 리소스를 할당해두어야 하는 환경에서는 나중에 할당이 불가함. 단점은 전체 사용자 중 10%만 이런 유연성이 필요하고 90%는 그냥 버퍼 할당 후 넘길 텐데, 모두가 더 복잡한 일을 하게 됨. 가장 좋은 방법은 높은 유연성을 주면서도, 간단한 경우에는 쉽게 처리되는 것이어야 함. 예를 들면, 0길이 버퍼(또는 Zig의 null)를 넘기면 타입이 직접 할당하도록 하고, 추가로 버퍼 없이 간단하게 생성하는 생성자(constructor)도 제공하면 되겠음. 물론 이런 건 전적으로 문서화가 고생임은 분명함

    • 이건 각 언어마다 선택하는 관례의 차이와 비슷함(라디안/도 등). 어떤 IO도 자유롭게 변환 가능함. 한쪽에선 mock이라 부르고, 어떤 언어에선 unsafeFoo라 할 수도 있음. Andrew Kelley가 live stream에서 Haskell 커뮤니티가 30년간 논의한 패턴을 독자적으로 재발견함. 그래서 미래는 Zig임. 그가 먼저 깨달음

    • 외부 버퍼의 의미는 함수가 버퍼 할당을 생략하는 것임

  • zig 사이드 프로젝트를 0.15.x로 올릴 생각 없음. Andrew가 왜 릴리스를 선택했는지, 새 Io를 얼리어답터 손에 쥐어주는 걸 존중함. 하지만 readers/writers 대대적인 변경 직후라서 며칠밖에 지나지 않았음. 표준 라이브러리 작업자에게는 좋은 일이지만, 나처럼 취미삼아 zig 쓰는 사람은 0.16.0에서 안정화 이후까지 기다리는 게 현명하다고 느낌

    • 언어 이름이 Zig이라면, 가끔은 Zag도 해야 하지 않을까라는 농담이 떠오름

    • Zig 코어 멤버인 Loris Cro도 최근 인터뷰에서 IO 변경 여진이 가라앉을 때까지 자신의 프로젝트에 zig 업데이트를 미룬다고 언급함. 하지만 이후 전망은 긍정적임. Andrew와 Loris 모두 이것이 마지막 주요 변화가 될 거라 보고 있어, 1.0이 머지않아 나오지 않을까라는 기대를 함. (재)도입되는 stack-less coroutine의 영향만이 현재 가장 큰 변수임

  • 새 IO 인터페이스에 대한 글을 본 뒤 zig를 피하는 쪽을 택했음. 다행히 내 본능이 맞았다는 생각임. 사유는 다르더라도 결과적으로 C++11 이전의 장황함과 비슷한 복잡성이 느껴짐. 새 언어가 대체 역할을 하려다 결국 기존 언어만큼 복잡해지는 패턴이 익숙하게 반복되고 있음

    • 내 게시글도 그 중 하나임을 밝힘. 이런 글을 보고 zig에 도전하는 걸 겁낼 필요는 없다고 생각함. zig 팀은 더 나은 해결책이 보이면 과감히 바꿀 태세임. zig를 미래 먹거리로 생각한다면 이런 변화가 맞지 않을 수 있지만, 개인 또는 소규모 팀에는 이미 목적이 분명하고 툴링이 좋은 멋진 언어임
  • Stream.Reader를 std.Io.Reader로 변환하려면 interface() 메소드를 호출, Stream.Writer에서 std.io.Writer를 얻으려면 &interface 필드의 주소가 필요하다는 점이 일관적이지 않다는 OP의 지적은, Go 커뮤니티라면 아예 거부했을 것이라 생각함. Go는 사소한 변화라도 극도로 깊이 있는 분석 끝에 결정하는 편임. 내 최애 Go 이슈 사례: Go github issue #45624. 4년간 논의 후 결론을 내리는 식임. 느릴 수 있지만 일관성, 설계적 고민, 실제 코드 사용까지 꼼꼼히 챙김. 느리지만 그만큼 필요한 속도라고 생각함. 그렇게 나온 결정이 결과적으로 매우 품질이 좋음

    • Rust 역시 마찬가지임. nightly rust에만 있고 stable에는 없는 유용한 기능들(예: generator)이 많음. 답답하지만, stable에 들어가는 기능은 매우 깊이 검증됨. 나는 조급하지만, Rust 팀의 접근법이 바람직하다고 생각함

    • Go 1.0 전에는 그 정도로 느리지 않았음. 더 근본적이지는 않더라도 큰 변화가 자주 있었고(세미콜론 제거, 에러 타입 변경 등), 자동 변환 툴도 지원했음. 1.0부터는 안정성을 약속하며 지금의 방식이 된 것임

  • Zig는 로우레벨 작업에 쓸 언어로 제일 먼저 떠오름. Zig를 C/C++ 크로스 컴파일러로도 쓸 수 있다는 점이 매우 멋짐

  • 딱히 이슈 대부분이 문서 부족 또는 부실함에서 나온 문제로 보임

    • Zig에서 너무 많은 부분이 자주 바뀌다보니, 문서화가 우선순위가 아닌 것 같음. Zig 튜토리얼도 거의 "예시 코드 모음" 느낌이고(최신 컴파일러에서 안 돌아가는 예시도 많음), 많은 표준 라이브러리 정의도 직접 소스 코드를 읽어야 함. Zig 문법의 트릭을 다 알면, 단순한 함수들은 짧고 논리적이며, 이름도 명확해서 작성자는 쉬움. Allocators 개념도 개념적으로는 어렵지 않으나, 직접 만들고 싶지는 않음. 하지만 복잡한 개념들에서는 한계가 명확히 드러남. Zig의 새로운 IO 시스템은 마치 자바의 Streams/Readers/Writers 구조처럼 여러 레이어로 감싸져 있음. 쉬운 출력을 위해 단순하게 output.write("hello")처럼 쓰게 해주려 하지만, 실제로는 사용법 설명이 부족해서 혼란을 주고 있음. 이런 복잡한 타입 시스템을 굳이 표준 라이브러리에서 표현해야 하는지도 의문임. Zig 전체가 명확하고 간결하고 읽기 쉬운 메소드로 이루어져 있는데, 새 IO 시스템은 그와 거리가 멀고 비직관적임
  • (zig의 새 시스템은) 그냥 실행 경계를 나누는 용도였던 개념을 전체 런타임 엔진에 섞으면서, 그 양쪽을 어떻게 연결하는지는 명확히 제시하지 않아서 문제임