요약
힙 할당기도 가비지 컬렉터도 없이 메모리 안전을 노린 시스템 언어 Goose가 공개됐습니다.
Lobster 언어를 만든 바우터르 판오르트메르선이 오래전 구상한 설계를 바탕으로 했습니다. 컴파일러 구현과 벤치마크, 문서 작성은 대부분 Claude Fable이 맡았다고 밝혔습니다.
왜 중요한가
- 수명 표기(참조가 언제까지 유효한지 적는 표시)나 unsafe 없이 안전한 참조를 보장하겠다는 점에서 Rust와 다른 길을 택했습니다.
- 사람이 언어를 설계하고 AI가 약 3만 줄짜리 C++ 컴파일러를 거의 다 짠 사례라는 점도 눈길을 끕니다.
핵심 내용
- 모든 동적 값은 컴파일러가 관리하는 데이터 스택에 그대로 놓이고, 스코프를 벗어나면 저장 한 번으로 해제됩니다.
- 16개 벤치마크에서 평범한 C++보다 3.3배, 가장 잘 짠 안전한 Rust보다 1.12배 빠르다고 주장합니다.
- 예시 주문 레코드는 C++에서 160바이트와 할당 1번이 들지만 Goose에서는 할당 없이 29바이트라고 합니다.
- 스레드는 메모리를 공유하지 않고 참조 없는 값만 큐로 복사해 주고받으므로 락과 원자 연산이 필요 없습니다.
- 대신 데이터의 소유와 수명 범위를 직접 설계해야 하고, 함수 포인터나 동적 디스패치는 쓸 수 없습니다.
HN 반응
- 크기가 제각각인 문자열을 자유롭게 넣고 빼려면 할당기를 손수 짜는 셈이라는 지적에, 작성자는 풀 기능으로 대응한다고 답했습니다.
- 문서를 AI가 써서 어디까지가 작성자의 설계인지 알기 어렵다며 직접 다시 쓰라는 요구가 나왔습니다.
참고로 이 언어는 제가 만들었습니다. 뭐든 물어보세요. 저장소: https://github.com/aardappel/goose
저라면 문서를 전부 버리고 직접 쓰신 말로 처음부터 다시 쓰겠습니다. 기계가 코드 대부분, 심지어 전부를 쓰는 시대라는 건 압니다. 그건 받아들일 수 있습니다. 다만 이 프로젝트를 쓰고 확장할 생각이라도 해 보려면, 설계 중 어디까지가 당신의 의도이고 어디부터가 Claude가 우연히 만든 결과인지는 알아야 합니다. Claude 말투로 쓰인 글에서는 그걸 알 수가 없습니다.
인터넷에서나 그렇지 현실에서는 아닙니다. 저는 이런 도구들이 일을 제대로 해낸다고 믿지 않고 믿을 수도 없습니다. 제가 시킨 일 중에 성공한 경우가 단 한 번도 없었거든요. 제가 거래하는 회사 중에도 그렇다고 주장하는 곳은 없습니다.
안타깝게도 온라인에서 논쟁해 봤자 소용이 없는 것 같아요.
아이디어를 키우려면 머릿속에 모델이 있어야 합니다
알려진 패턴이라면 라이브러리를 쓰면 되죠!
알려지지 않은 패턴이면 AI는 지어냅니다
AI로 코딩하는 게 아무리 "좋아져도" 배우는 과정을 건너뛸 방법은 없고, 무슨 일이 벌어지는지 이해하고 책임질 사람이 있어야 합니다.
형식 검증이나 퍼징을 하거나, 버그가 어디서 나왔을지 힌트를 얻는 건 저도 좋습니다. 하지만 그건 디버거, 엄격한 타입, 함수형 프로그래밍, 그리고 퍼징처럼 예전부터 쓰던 도구를 개선한 것일 뿐이에요.
저는 아직 완전히 이해하지 못한 단계를 AI에게 반복시켜 본 적이 있는데, 그때마다 후회했습니다. 서로 얽힌 부분이 많은 프로젝트는 제대로 배우기 전까지 멈춰 서고 말아요.
의료 분야에서 사양이 아주 엄격한 소프트웨어를 만드는 사람들에게 물어보세요. 사양이 아주 훌륭해도 자연어에는 모호함이 있어서, 자연어는 믿을 수 있고 관리 가능한 소프트웨어의 원천이 될 수 없다고 할 겁니다.
그런데 말이죠, 40년대쯤 똑똑한 사람들이 컴퓨터에게 모호함 0으로 할 일을 알려 줄 형식 언어가 필요하다는 걸 깨달았다는 글을 읽은 적이 있어요. 그걸 "프로그래밍 언어"라고 불렀던 것 같은데요.
맞아요, 지금은 방향만 잘 잡아 주면 개별 함수 정도는 믿고 맡길 수 있는 수준까지 온 것 같아요. 꾸준히 써 보려고 할 때마다 대체로 돌아가는 것만 만들다가 결국 터지거나, 아예 빠진 게 있거나, 반쪽짜리 진실만 담긴 결과가 나와서 헛바퀴만 돌았거든요. 제 실력 문제든 아니든 저한테는 안 맞아요.
그래도 빌드 문제나 메모리 오류를 디버깅할 일이 사실상 없어진 건 좋네요.
코드가 별로면 기대에 맞게 고치라고 시키면 됩니다. 가장자리 부분에서만 일일이 말해 줘야 하죠.
문서가 부실한 건 예전엔 단골 농담거리였습니다.
2019년의 goose_spec.txt는 "goose는 C나 Rust 같은데 문법이 array{;}[] 이런 식이고, 참고로 malloc 대신 스택을 조금 씁니다."0 10klocs 정도였을 겁니다.
이제는 AI가 나중에 자기가 하던 일을 파악할 수 있도록 문서를 남기게 합니다.
최신 Claude보다 몇 세대, 가중치로는 한참 뒤처지는 로컬 모델도 제가 만든 새 알고리즘을 구현하고, 제가 쓰지 않을 문서까지 써 줍니다.
그래요, 유치원생 수준의 CS 실수를 하는 일도 많지만, 가끔씩만 최적해 쪽으로 나아가면 되니까요.
음, 튜토리얼은 꽤 읽을 만하다고 생각해요. 충분히 간결하고요.
튜토리얼을 보고 나니 요약도 더 이해가 됩니다. 다른 부분은 아직 안 읽었지만 지금까지는 마음에 들어요.
전체적으로 취향에 꽤 맞고, 지금까지 본 건 좋습니다. 저자가 직접 낸 아이디어든, AI가 낸 걸 저자가 승인한 거든 상관없어요.
이걸 쓰려면 그걸 "알아야만" 한다는 이유를 모르겠어요. 아이디어는 어디서 나왔는지와 상관없이 따로 판단할 수 있고, 그래야 하잖아요.
그럴 수는 있지만 현실에서는 시간이 없어서 아무도 그렇게 하지 않습니다. 예를 들어 괴짜가 내놓은 아이디어를 몇 번 검토해 보고 나면 다시는 안 하게 되죠. 인생은 짧습니다.
저는 이 언어가 읽기 어려웠습니다. 문맥상 말이 안 되는 구절도 있고요. 간결하지도 않습니다. 저도 AI로 썼는지 아닌지는 상관없고, AI 이전에도 형편없는 문서는 많았습니다. 그래도 이 문서를 "좋다"고 하지는 않겠습니다.
요즘 많은 사람이 이해하지 못하는 것 같은데, 정말 좋은 지적이에요!
100% 동감해요.
몇 문장도 못 읽고 속에서 구역질이 올라와서 그만 나왔어요. 편집도 안 한 Claude발 쓰레기 같은 글을 알면서 올리고도 부끄러워하지 않는 것 같은 사람들이 있다는 게 충격이에요. 2026년에는 기술 쪽 사람의 주된 역할이 에이전트가 내놓은 결과물과 다른 사람들 사이를 가로막는 방벽이 된 것 같네요.
회사에서 일주일에 몇 시간씩 Claude 안 쓰세요? 저는 그 글투가 익숙해서 오히려 편한데요.
그리고... Lobster 언어, Cube2/Sauerbraten 3D 슈팅 게임, voxray/voxile 같은 멋진 것들도 많이 만드셨죠.
https://strlen.com/
Sauerbraten은 정말 대단한 게임이에요. 가끔씩 켜서 venice 맵에서 instafrag을 하곤 해요.
와, 옛날 생각 나네요. 어릴 때 인터넷 연결이 아주 형편없어서, 형이랑 저는 방 사이로 크로스오버 케이블을 길게 늘어뜨려 놓고 Sauerbraten이랑 몇 가지 다른 게임을 질리도록 했어요.
Amiga E 언어도 빼놓으면 안 되죠... 정말 멋진 물건이에요.
링크에 있는 이 문장이 무슨 뜻인가요?
마지막 문장이 도무지 해석이 안 됩니다.
벡터나 문자열처럼 크기가 가변인 것을 다루려면 C++는 대개 그 내용을 동적으로 할당해야 한다는 뜻입니다. 그러면 여러 번의 할당에 걸쳐 64비트 포인터가 가득한 자료구조가 만들어집니다.
서로 다른 크기의 값을 같은 공간에 담을 수 있는 Rust의 "enum"조차 가장 작은 변형이 가장 큰 변형만큼 자리를 차지하는 대가를 치르고 그렇게 합니다.
Goose는 이 모든 걸 인라인(포인터 없음)으로 처리할 수 있을 뿐 아니라 크기도 가변이라서, 작은 항목은 그만큼 공간을 덜 씁니다. 직렬화된 데이터 표현과 비슷한데 순회할 수 있는 셈이죠. 이 언어들에 비해 공간을 크게 아낄 수 있습니다.
제가 이해한 바로는 동적 크기 변수마다 사실상 새 스택을 만드시는 것 같습니다(그렇지 않으면 이동 없이 범프 할당만으로 커질 수 있는 게 없을 테니까요). 그렇다면 객체마다 가상 메모리 할당을 따로 만든다는 뜻인데, 그러면 (아마도?) 크기가 OS의 최소 페이지 크기(4KiB 정도)로 올림될 겁니다.
공간 낭비가 크지 않나요? 아니면 제가 놓친 부분이 있나요?
또 이를 피하는 요령이 따로 없다면, 모든 할당이 OS 커널을 거쳐야 할 것 같습니다.
놓치고 계신 부분은, 일종의 생존 분석(liveness analysis)으로 동적 크기 변수가 동시에 최대 몇 개나 "깨어 있는지" 알아낼 수도 있다는 점 같습니다. 그러면 그 수만큼만 스택을 만들면 됩니다. "잠든" 변수의 스택은 재사용할 수 있으니까요("깨어 있는" 변수는 자기 블록의 스코프를 벗어나면 반드시 해제되고, 스레드끼리는 메모리를 공유하지 않으니 이 스택들에 대한 데이터 레이스도 걱정할 필요가 없습니다).
예를 들어 늘어나는 배열을 가진 함수가, 그 배열을 넘기지 않고 다른 함수를 호출한다고 해 봅시다. 호출된 함수도 임시 작업 공간으로 늘어나는 배열을 씁니다. 그러면 두 번째 함수가 실행되는 동안 첫 번째 함수의 배열은 쓰이지 않으니(잠든 상태이니), 안쪽 함수의 배열은 같은 스택을 안전하게 공유할 수 있습니다. 두 번째 함수가 끝날 때 포인터 하나만 되돌리면 전부 해제되니까요.
사실 저는 컴파일러가 어떻게 동작하는지 모릅니다. 다만 제 이해로는 필요한 스택 수를 최적화하려고 제 예시와 비슷한 일을 하는 것 같습니다(제 이해가 현실과 맞는지는 WvO가 확인해 주기를 기다려 보죠).
당연히 궁금한 건, 연결 리스트나 트리처럼 스택 모양이 아닌 자료구조에서는 어떻게 동작하느냐는 겁니다.
튜토리얼에 예제가 있고, 원형 연결 리스트도 있어요.
리스트/트리 등의 항목을 전부 스택에 저장하면서 필요한 만큼 키우면 됩니다. 아니면 특정 스택에 "프리리스트"를 따로 추가해서 항목을 재사용할 수도 있고요.
결국 트리도 메모리에는 선형으로 저장됩니다. 각 노드가 배열의 원소이고, 포인터 대신 인덱스를 쓰는 배열이라고 생각해 보세요.
대부분의 트리는 커지기만 합니다. Goose에서는 그게 일반적인 배열이고요. 하지만 트리가 계속 바뀐다면 커졌다 줄었다 하는 배열도 쓸 수 있습니다.
어느 쪽이든 트리 전체에 스택은 하나입니다.
최상위 수준의 늘어나는 배열만 자기 "스택"을 가집니다. 배열의 배열을 만들면 스택은 하나만 씁니다.
안쪽 스택이 커지는 문제를 어떻게 다룰지는 경우에 따라 다릅니다. 이상적으로는 커지지 않게(생성 시에만 채우게) 하거나, 고정 크기로 만들거나, 용량을 지정할 수 있고, 최악의 경우엔 풀(pool)로 쓰면 됩니다. 언어에 이 모든 방법을 위한 기능이 있습니다.
"풀" 경우는 어떤 건가요? 함수가 일반 힙이나 가상 메모리에 늘어나는 아레나를 할당하는 건가요?
https://aardappel.github.io/goose/docs/reference.html#14-reusable-pools
하는 일은 주소 공간을 할당하는 것뿐입니다. 그리고 64비트면 그 공간은 천문학적으로 많죠. 실제 메모리가 되는 건 쓸 때뿐이고, 한 번에 한 페이지씩입니다.
아니요, 일반적으로 가상 주소 공간은 48비트까지만 가정할 수 있습니다. 커널이 절반을 가져가니 실제로는 47비트뿐이에요. 최신 CPU는 57비트(예: Zen 4)나 52비트(일부 ARM64 모델)를 제공합니다. 그것도 커널이 협조해 줄 때 얘기고요. 일부 안드로이드 구성은 사용자 가상 주소 공간이 39비트까지만 지원된다고 합니다.
AI가 생성한 이 문서는, 완전한 예시를 이루는 경우가 아니면 숫자를 넣지 말라고 지시하면 훨씬 이해가 잘 될 것 같습니다.
C++에서도 그냥 그렇게 할 수 있습니다. 저는 보통 메시징에서 그렇게 해서, 객체를 단순 재배치할 수 있고 reinterpret_cast도 가능하게 만듭니다. 정규 타입들을 조합할 때만 통하는 방식이지만, 그건 C++26 리플렉션으로 검증하거나 변환할 수 있어요.
저는 인라인 시퀀스와, 아웃오브라인 시퀀스(포인터 대신 객체 자체에 대한 오프셋을 저장)를 둘 다 갖고 있습니다.
크기와 오프셋은 전부 32비트로 했는데, 16비트로 해도 됐을 것 같아요.