let-go: Go를 위한 고성능 Clojure 방언
Clojure의 표현력 있는 기능을 좋아하지만 Go의 배포 단순함이 필요한 개발자에게 let-go는 매력적인 절충안을 제공합니다. let-go는 속도, 작은 점유 공간, 그리고 배포의 용이성을 위해 특별히 설계된, Go로 구현된 Clojure 방언의 바이트코드 컴파일러이자 VM입니다.
경량 CLI 도구, WASM 기반 웹 애플리케이션, 또는 기존 Go 애플리케이션을 위한 스크립팅 레이어를 구축하려는 경우, let-go는 약 7ms 만에 부팅되는 런타임을 제공하여 JVM이나 GraalVM-native 바이너리의 매력적인 대안이 됩니다.
핵심 아키텍처 및 성능
let-go의 핵심은 바이트코드 VM입니다. Clojure와 유사한 소스 코드를 사전 컴파일된 바이트코드 형식(.lgb)으로 컴파일함으로써, 런타임은 로드 시점에 reader, parser, compiler 단계를 건너뛰어 즉각적인 시작이 가능합니다.
다른 런타임과의 벤치마크
성능 향상은 다른 Clojure 구현체와 비교할 때 가장 분명하게 나타납니다. Apple M1 Pro에서 let-go는 점유 공간과 시작 속도 면에서 상당한 이점을 보여줍니다:
| Metric | let-go | Babashka | Joker | Clojure JVM | | :--- | :--- | :--- | :--- | :--- | | | Binary Size | 10MB | 68MB | 26MB | 304MB (JDK) | | Startup Time | 7ms | 20ms | 12ms | 331ms | | Idle Memory | 14MB | 27MB | 21MB | 92MB |
시작 속도 외에도 let-go는 연산 집약적인 작업에서 매우 경쟁력이 있습니다. 예를 들어, 피보나치 계산에서 Babashka의 4% 이내 성능을 보여주며, 바이트코드 VM 대신 트리 워크 인터프리터를 사용하는 Joker보다 약 10배 빠릅니다.
주요 기능 및 역량
독립 실행형 바이너리 및 WASM
let-go의 주요 목표 중 하나는 런타임의 필요성을 없애는 것입니다. 사용자는 lg -b를 사용하여 프로그램을 단일 독립 실행형 바이너리로 컴파일할 수 있으며, 이는 바이트코드를 lg 실행 파일에 포함시킵니다.
또한, let-go는 WASM 컴파일을 지원합니다. lg -w를 사용하면 프로그램이 자가 포함된 HTML 페이지로 컴파일됩니다. 여기에는 xterm.js를 통한 전체 터미널 에뮬레이션이 포함되어 있어, Clojure와 유사한 프로그램을 브라우저에서 네이티브한 터미널 경험과 함께 직접 실행할 수 있습니다.
Go 상호 운용성
let-go는 임베딩되도록 설계되었습니다. Go 개발자가 Go 값과 함수를 VM에 노출함으로써 해당 언어를 스크립팅 레이어로 사용할 수 있게 합니다. 주요 상호 운용 기능은 다음과 같습니다:
- Struct Mapping: Go 구조체는 let-go 레코드로 등록 및 매핑될 수 있어, 변경되지 않은 데이터에 대해 비용 없는 왕복이 가능합니다.
- Channel Integration: Go 채널(
chan)은let-go의core.async구현에 직접 연결되어 두 언어 간의 원활한 이벤트 파이핑을 가능적으로 합니다. - Function Calls: Go 함수는 let-go 코드에서 직접 호출할 수 있으며, 그 반대도 마찬가지입니다.
생태계 호환성
Clojure와의 높은 호환성을 유지하기 위해 let-go는 clojure-test-suite를 대상으로 테스트되었으며, 95% 이상의 어설션(assertion)을 통과했습니다. clojure.core, clojure.string, clojure.set, 그리고 core.async와 같은 필수 네임스페이스를를 구현합니다.
흥미롭게도, let-go는 Babashka pods도 지원합니다. 이를 통해 바이너리 프로토콜을 통해 네임스페이스를 노출하는 방대한 독립 실행형 프로그램 생태계를 활용할 수 있으며, 데이터베이스, AWS, Docker 통합을 위해 라이브러리 생태계를 다시 작성할 필요 없이 즉시 접근할 수 있습니다.
표준 Clojure와의 차이점
let-go가 높은 충실도를 목표로 하지만, JVM Clojure의 완전한 대체재는 아닙니다. 개발자는 몇 가지 주요 차이점을 인지해야 합니다:
- Concurrency: Refs와 STM (Software Transactional Memory)은 부족하지만, atoms와 channels은 완전히 지원됩니다.
- Numeric Behavior:
int64에서의 숫자 오버플로(overflow)는 BigInt로 자동 승격되지 않고 조용히 래핑(wrap)됩니다. 사용자는 명시적인 BigInt 산술 함수(예:+'대신+)를 사용해야 합니다. - Async Implementation: JVM 버전과 달리,
let-go의go블록은 IOC 상태 머신이 아닌 실제 goroutine입니다. 이는 비용이 저렴하지만 직접적인 차단(blocking) 작업을 호출할 수 있게 합니다. - Regex: Java의 정규 표현식 대신 Go의
re2방식의 정규 표현식을 사용합니다.
시작하기
실험해보고 싶은 분들은 Homebrew 또는 Go를 통해 let-go를 설치할 수 있습니다:
brew tap nooga/let-go
brew install let-go
또는 Go install을 통해:
go install github.com/nooga/let-go@latest
설치 후, lg로 REPL을 시작하거나 lg myfile.lg로 파일을 실행할 수 있습니다. 고급 워크플로우를 위해, lg -n 명령은 CIDER, Calva, 그리고 Conjure와 호환되는 nREPL 서버를 시작하여, Clojure의 강력한 대화형 개발 경험을을 Glo ecosystem에 가져옵니다.