본문으로 건너뛰기

© 2026 Molayo

GeekNews헤드라인2026. 05. 25. 09:47

Go에서 Rust로 마이그레이션하기

요약

본 기사는 Go와 Rust의 근본적인 설계 차이점을 비교하며, 특히 메모리 안전성, 동시성 처리, 오류 관리 측면에서 Rust가 가지는 구조적 우위를 설명합니다. Rust는 컴파일러 레벨에서 데이터 레이스 방지 및 완전한 오류 처리를 강제하여 예측 가능하고 안정적인 시스템 구축에 유리합니다.

핵심 포인트

  • Rust의 Option<T>와 Result<T, E>는 런타임 에러를 타입 시스템으로 강제 처리합니다.
  • Rust는 Send/Sync 트레이트를 통해 스레드 간 가변 상태 공유 시 컴파일 타임 안전성을 보장합니다.
  • Go보다 예측 가능한 지연 시간(low-pause)이 필요한 고성능, 저지연 시스템에 Rust가 유리할 수 있습니다.
  • Rust의 제네릭은 단형화되어 런타임 비용 없이 높은 성능과 추상화를 제공합니다.

user.Account.Notify()는 user 또는 Account가 nil일 때 크래시될 수 있음

nilaway, staticcheck 같은 린터와 IDE 검사는 일부를 잡지만, 옵트인이고 확률적이며 패키지 경계를 안정적으로 넘지 못함

Rust의 Option<T>는 None 경우를 처리하지 않고는 역참조할 수 없게 해 이 범주의 장애를 제거함

-race가 잡지 못한 데이터 레이스

go test -race는 훌륭한 도구지만 런타임 탐지기이므로 테스트 중 실제 실행된 레이스만 찾음

Go에서는 두 goroutine이 락 없이 map을 변경하는 코드도 컴파일되고, 부하가 걸린 프로덕션에서 터질 수 있음

Rust에서는 스레드 간 가변 상태 공유에 Send와 Sync 구현 타입이 필요하며, 평범한 HashMap을 스레드 간 공유하려 하면 컴파일되지 않음

Arc<Mutex<...>>, Arc<RwLock<...>>, 채널 중 하나를 사용하도록 강제되며, 레이스 조건이 타입 오류가 됨

Paul Dix는 InfluxDB 3.0 재작성 동기로 데이터 레이스 제거를 직접 언급함

“[The main benefit is] fearless concurrency — eliminating data races essentially, which we had before. Really gnarly bugs in version 1 of Influx due to that.”

Rust의 Result<T, E>는 타입 시그니처 자체라서 잊을 수 없고, thiserror::Error로 정의한 enum과 #[from]을 통해 오류 변환과 완전성 검사를 받음

새 오류 variant를 추가하면 컴파일러가 갱신이 필요한 match 위치를 알려줌

박싱하지 않는 제네릭

Go 1.18 제네릭은 유용하지만, 타입 파라미터가 있는 메서드 부재, GC shape stenciling, 가끔 놀라운 성능 특성 같은 제약이 있음

Rust 제네릭은 단형화되어 각 인스턴스화가 특수화된 코드를 만들고 런타임 비용이 없음

트레이트와 결합하면 제로 비용 추상화가 가능함

핸들러 코드보다는 미들웨어, generic repository, decoder, parser 같은 공유 인프라에서 더 중요하며, Go는 이런 영역에서 interface{}/any와 타입 단언으로 돌아가는 경우가 많음

예측 가능한 지연 시간

Go의 GC는 우수하고 동시적이며 저지연이고 일반적인 서비스 워크로드에 잘 튜닝되어 있지만, “low-pause”는 “no-pause”가 아님

할당이 많은 상황에서는 핫패스에서 할당하지 않는 Rust 구현보다 P99 지연 시간 꼬리가 나빠질 수 있음

트레이딩, 실시간 입찰, 네트워크 프록시, 고처리량 수집처럼 지연 시간에 민감한 시스템에서는 GC pause 부재가 실제 장점임

Stephen Blum은 PubNub 규모에서 필요한 가격 대비 성능 용량을 얻기 위해 Rust가 필요하다고 말함

“Go is great at our scale, but we really need something that is going to give us the price-per-dollar performance capacity that we need, and Rust is going to get us there. That’s why basically everything is heading towards Rust these days.”

“Go doesn’t have a way to tell a goroutine to exit. There is no stop or kill function, for good reason. If we cannot command a goroutine to stop, we must instead ask it, politely.”

Go에서는 이 “정중한 요청”이 관례적으로 전달되는 context.Context이고, Rust에서는 CancellationToken 또는 watch 채널이지만 컴파일러가 누락을 알려줄 수 있음

&str: 다른 문자열 데이터에 대한 borrowed view이며, 대부분의 경우 Go의 string parameter에 대응함

경험칙은 인자에는 &str을 받고, 새 데이터를 만들 때는 String을 반환하는 것임

&str과 String의 분리는 Rust의 “borrow vs own” 모델을 축소해 보여줌

Go 제네릭에 대한 평가

Go는 1.18, 2022년 3월에 제네릭을 도입했으며, 언어 출시 13년 뒤였음

제네릭은 유용하지만 Rust·Haskell·현대 C++에서 기대하는 장점을 충분히 주지 못하고, 제네릭 타입 시스템의 단점 상당수를 함께 가진 것으로 평가됨

표준 라이브러리가 거의 쓰지 않음

제네릭 도입 3년 뒤에도 Go 표준 라이브러리는 대부분 제네릭을 피함

sort.Slice는 여전히 cmp.Ordered constraint 대신 func(i, j int) bool closure를 받음

sync.Map은 여전히 any/any로 타입화됨

존재하는 generic helper는 slices, maps, cmp, sync 아래 일부 항목 같은 소수 패키지에 있음

Go 1 호환성 약속 때문에 기존 non-generic API를 개조하기 어렵다는 점은 일부 설명이 되지만, Rust처럼 제네릭을 주된 도구로 쓰지는 않음

Rust는 초기부터 Option<T>, Result<T, E>, Vec<T>, HashMap<K, V>, Iterator, From/Into, 모든 컬렉션과 스마트 포인터에 제네릭이 스며 있음

트레이트 시스템이 없고 구조적 constraint만 있음

Rust 제네릭은 ad-hoc 다형성, supertrait, associated type, blanket impl, coherence를 담당하는 trait와 묶여 있음

Go constraint는 type-set membership을 위한 ~ 연산자가 추가된 interface에 가까움

Go에는 Rust의 trait Ord: Eq + PartialOrd 같은 supertrait hierarchy, Iterator의 type Item; 같은 associated type, impl<T: Display> ToString for T 같은 blanket impl이 없음

C/C++이나 Python에서 Rust로 옮기는 건 여러 이유로 이해되지만, 웹 백엔드라면 Go가 잘 맞는 선택으로 보임
거의 Rust만 쓰지만, 마지막으로 Rust로 웹 서버 쪽 작업을 했을 때는 Go를 쓸 걸 그랬다고 느꼈음
원문은 Go의 오류 처리 문법이 장황하다고 짚는데, 맞는 지적임. Rust도 같은 문제가 있다가 오류면 오류 값을 반환하는 ? 문법을 추가했음. Go의 오류 처리는 대부분 이걸 풀어 쓴 형태임
Rust에는 통일된 오류 타입이 없고, io::Error, thiserror, anyhow 같은 주요 오류 체계가 있어 호출 체인을 따라 위로 전달할 때 번거롭다
새 언어에서 빠졌다가 나중에 덧붙이기 어려운 것들이 있음. 상수 타입, 불리언 타입, 오류 타입, 다차원 배열 타입, 2/3/4 크기의 벡터·행렬 타입과 표준 연산 등이 그렇다. 초기에 표준화하지 않으면 같은 개념의 여러 표현을 맞추느라 시간을 많이 쓰게 됨
오류 처리를 제외하면 웹 개발에는 덜 영향이 있지만, 수치 계산·그래픽·모델링에서는 숫자 배열에 표준 연산을 적용해야 해서 큰 고통이 됨
Go가 웹 서비스에서 가진 장점은 두 가지임. 첫째는 원문이 말한 고루틴이고, 둘째는 원문이 많이 다루지 않은 라이브러리임. Go에는 웹 서비스에 필요한 대부분의 라이브러리가 있고, Google 내부에서도 쓰이는 것들이라 매우 혹독한 환경을 견뎌냈음. 반면 Rust crate들은 성숙도가 낮고 공식 품질 보증이 없는 경우가 많다

Rust보다 Go가 가진 가장 큰 장점은 컴파일 속도라고 봄
또 Rust는 Go와 비교해 여전히 많은 C/C++ 라이브러리에 의존해서, 교차 컴파일이나 재현 가능한 빌드, 정적 바이너리 생성이 문제 되기 쉽다
Go의 단점은 가비지 컬렉터가 너무 단순하다는 점임. 지연 시간 스파이크가 생기면 고통스러운 재작성 말고는 대응 수단이 별로 없음

Rust에 사실상 하나의 오류가 있는데, 바로 Error trait임
나열한 것들은 그걸 사용하는 흔한 방식일 뿐이고, Box만 써도 전혀 문제 없음. 이는 대체로 anyhow::Error가 하는 것과 비슷함

한동안 Go를 꽤 좋아했지만, 최근 Swift와 Rust를 더 많이 쓰다 보니 널 포인터 역참조를 막아주지 않고 동시성 안전 보장도 없는 컴파일러는 조금 선사시대적으로 느껴짐
다만 표준 라이브러리 쪽에서는 Go가 Rust보다 훨씬 잘했다고 봄

동의함. 초반에 이 글이 백엔드 서비스를 위한 것이라고 한 부분이 눈에 띄었음
Rust 언어를 좋아하고 임베디드 펌웨어와 PC 애플리케이션에 쓰지만, 웹 백엔드는 여전히 Python을 씀. Rust에는 Django나 Rails급 도구 묶음이 없기 때문임
Flask 비슷한 것은 있지만 Flask의 탄탄한 생태계는 없음. Go 경험은 적지만, 웹 백엔드라면 Rust보다 Go를 고를 것 같음. 이유는 라이브러리와 프레임워크 생태계 때문임
또 일반적으로 말하는 이유들 때문에 Async Rust를 그다지 좋아하지 않음. Rust 웹 생태계는 거의 전부 비동기 사용이 필수에 가까움

Rust에는 오류 체계가 세 개 있는 게 아니라 하나, 즉 Error trait이 있음 io::Error는 이를 구현한 여러 타입 중 하나일 뿐 특별하지 않음. thiserror로 정의한 오류도 이 trait을 구현함 anyhow는 함수가 뱉을 수 있는 오류 타입을 API 계약으로 자세히 쓰고 싶지 않을 때 “어떤 Error”라고 편하게 말하게 해줄 뿐임

뻔하고 반복적으로 들릴 수 있지만, Rust에 대한 가장 큰 불만은 패키지 관리 상황이고, 이는 전적으로 개발자 사고방식의 결과라고 봄
Rust 쪽 사용성은 좋아함. 데이터 타입에 대한 함수형 접근은 아름답다. 그런데 지금 Rust 프로젝트와 Go 프로젝트를 나란히 작업 중인데 의존성 트리가 완전히 다른 짐승임
Go 프로젝트는 대부분 표준 라이브러리로 해결되지만, Rust 프로젝트는 rusqlite(sqlite), clap(CLI), ratatui(TUI), tauri(GUI)만 요청했는데도 의존성이 400개가 넘는 듯함. 특히 tauri가 압도적인 주범이고, 그걸 빼도 거의 100개라서 미쳤다고 느껴짐
의존성을 합리적으로 다루는 잘 관리된 Rust crate 대안이 있다면 훨씬 나을 텐데, 아직 찾지 못했음. 시스템에 shai hulud를 들이고 싶지 않을 뿐인데, Rust 웹 쪽 사람들은 그 면에서 cargo를 npm처럼 만들고 싶어 하는 것처럼 보임

많은 Rust 라이브러리가 여러 crate로 나뉘어 있고, 이들이 모두 의존성 그래프에 들어간다는 점을 감안해야 함
그래서 의존성 수가 실제보다 커 보임. 별도 crate라도 유지보수자가 같고 같은 upstream Git 저장소의 일부인 경우가 많다
그래도 전반적인 느낌에는 동의함. Rust에는 0.x 버전에 반쯤 방치된 crate도 많고, 더 나은 대안이 없는 경우가 자주 있음

표준 라이브러리는 좋은 아이디어가 죽으러 가는 곳이라고 봄
그러고 나면 httplib3 다음에 httplib4가 나옴
다시 말해 Rust 접근을 훨씬 선호함. 표준 라이브러리에 의존하든 다른 의존성에 의존하든 나에게는 큰 차이가 없음. 어쨌든 의존성임
표준 라이브러리라서 품질이 더 좋거나 유지보수가 더 잘된다고 생각하는데, 이건 별개의 개념임
결국 전부 자원에 달렸음. 물론 표준 라이브러리가 더 많은 자원을 받을 수는 있지만, 반대로 비대해지고 유지보수 불가능해질 수도 있음

패키지 관리는 거의 모든 언어와 기술의 골칫거리임
아무도 이를 “해결”하지 못했고, 앞으로도 하나의 해법이 나오긴 어렵다고 봄
Go에서는 라이브러리 개발자가 시맨틱 버전 관리를 정확히 지키리라 믿어야 하고, 버전을 고정할 수 없음. 이건 개인적으로도 꽤 거슬리는 부분임
우회책은 몇 가지 있음. Git 커밋 해시처럼 SHA를 써서 유사 버전을 만들거나, 알려진 의존성 캐시인 vendoring을 쓰는 방식임. 다만 vendoring은 캐시 관리 문제를 동반함
주말에 Python 가상 환경을 써야 했는데 좋게 끝나지 않았고, 왜 Python에서 벗어났는지 다시 떠올랐음
Perl의 CPAN, Java의 Maven/Gradle, Ruby의 gems, Go의 dep/glide/vgo/modules, Rust의 Cargo, Node의 npm/yarn 등 모두 비슷한 문제가 있음
운영체제도 Redhat의 yum/rpm, Debian의 apt, Ubuntu의 snap 같은 식임. 특히 snap은 대체 왜 그런지 모르겠음

Go에 익숙하지 않은데, Go 표준 라이브러리에서 Tauri에 해당하는 것이 무엇인지 궁금함
사용 사례에서 프런트엔드는 계속 Go로 두고 백엔드만 Rust로 하는 게 말이 될 수도 있나 싶음

이 문서는 마이그레이션 가이드이면서 동시에 Rust 옹호 문서가 되려 해서 이상하게 느껴짐
결국 Rust와 Go 중 무엇을 쓸지 고민한다면 핵심은 거의 전적으로 “관리형 런타임을 원하는가”로 귀결됨. 한 세대의 Rust 프로그래머들은 관리형 런타임이 나쁘고, 없는 것이 중요한 기능이라고 스스로 설득해 왔음
하지만 이는 명백히 틀렸음. 관리형 런타임을 원하는 프로그래밍 영역이 원하지 않는 영역보다 더 많다
그렇다고 그런 경우에 모두 Go를 기본 선택해야 한다는 뜻은 아님. Rust를 선호할 주관적 이유도 많음. Go를 쓸 때는 match가 그립지만, tokio와 Async Rust는 그립지 않음
둘 다 문제 공간을 억지로 비틀 필요가 없는 거의 모든 경우에 정당한 선택임. 예컨대 Go로 Linux 커널 모듈을 작성하려는 건 이상한 선택이겠지만 말임
Rust 대 Go 싸움은 우리 분야의 이상하고 민망한 변방 같음. 업계의 큰 부분은 Python이나 Node로 전체 시스템을 잘 만들고 있고, 어느 정적 타입 컴파일 언어를 쓸지 다투는 괴짜들을 비웃고 있음. 진짜 질문은 Python 대 Rust/Go이지, Rust 대 Go가 아님

Node를 PureScript와 함께 쓰는 건 괜찮을 수도 있다고 봄
하지만 전반적으로 Rust와 Go 쪽은 동적 타이핑의 악에 맞서 힘을 합쳐야 함. 타입 힌트가 이제 모범 사례로 여겨진다면, 이는 사실상 결함이었다는 인정 아닌가 싶음
좋은 타입 힌트가 있어도 타입 추론보다 못함. 타입 추론은 타입 변경 시 많은 코드를 그대로 둘 수 있게 해주면서도 의도치 않은 타입 변경은 막아줌

Node 쪽은 정적 컴파일 타입을 원해서 TypeScript를 받아들였음
TS에 런타임이 좀 더 있었으면 함. Python에서 부러운 유일한 점은 HTTP 엔드포인트에서 JSON 스키마 검증을 아주 자연스럽게 할 수 있다는 것임
Zod로 통과해야 하는 절차는 계속 짜증의 원천이고, TS 팀이 교조적이라서 생긴 문제라고 봄

LLM 글쓰기의 흔적이 점점 미묘해지고 있지만, 여전히 눈에 확 띔. 특히 genuine이라는 단어가 그렇다
“This is the area where Go genuinely shines, and it’s worth being precise about why”, “the lack of GC pauses is a genuine selling point”, “Humans are genuinely bad at reasoning about memory”, “There are cases where the borrow checker is genuinely too strict” 같은 식임
글 전체가 AI 생성이라고 보진 않고, AI 보조를 받은 것 같음. 그렇다면 작성자는 genuinely 잘 해낸 셈임
다른 사람들이 이를 언급하지 않는 걸 보면 내용 자체를 크게 해치진 않은 듯하지만, 이런 일이 점점 흔해지고 감지하기 어려워지는 게 이상하게 느껴짐

동의하지만 왜 그런지는 잘 모르겠음. 무엇이 AI 생성처럼 들리게 하는지 정확히 알지는 못함
“Go is clearly working for a lot of people,” 정도까지 읽고 AI 보조를 의심하게 됐음. 물론 아닐 수도 있고, 나는 판별을 잘 못함
구체적인 단서라기보다 아이러니하게도 느낌에 가까움. 어떤 글이 AI 보조처럼 “들리면” 글 자체가 괜찮아도 곧바로 흥미를 잃게 됨
사람들이 자기 생각을 떠오르는 방식대로 직접 쓰는 데 더 편해졌으면 함

완전히 주제에서 벗어나지만, it's worth being precise about ...는 genuine 사용보다 훨씬 강한 AI스러운 표현임

글 전체가 AI 생성이라고 생각함. 작성자가 초안을 입력으로 주고 출력 일부를 고쳤을 수는 있음
예를 들어 이 문단을 보면 그렇다: “Go got generics in 1.18, and they’re useful, but the implementation has constraints (no methods with type parameters, GC shape stenciling, occasional surprising performance characteristics). Rust generics monomorphize, each instantiation produces specialized code with zero runtime cost. Combined with traits, this gives you real zero-cost abstractions.”
모든 문장이 무언가를 말하고, 모든 문장이 중요하며 자기 몫을 함. 이런 글은 블로그 글보다는 매우 전문적인 책이나 논문에서 기대할 만함
그래서 오히려 글을 더 읽기 어렵고 지루하게 만듦

지난 1년 동안 LLM 글쓰기는 표면과 특히 기반층에 대해 말하는 경향이 유난히 강하다고 느꼈음
LLM 생성 텍스트가 진부한 표현으로 가득하지 않을 거라고 기대하지는 않음. 다만 우리 모두가 더 나은 편집 감각을 보여서 같은 목소리를 반복해서 읽지 않게 되길 바람

신규 프로젝트라면 얼마든지 Rust로 쓰면 됨
하지만 기존 코드가 있고, 작동하며 수익을 내는 시스템이라면 다시 써야 할 부분만 원래 언어로 고쳐서 계속 가는 게 맞음
아는 언어와 신뢰할 수 있는 팀으로 시스템을 작고 측정 가능한 방식으로 개선하라. 그 외는 낭비적인 종교 논쟁임

팀이 C#/Java/Go 등으로 성공적으로 출시했고 편하게 쓰고 있다면, Rust를 쓸 이유가 보이지 않음

벤치마크를 돌리기 전에도 Rust를 좋아했지만, 대부분의 LLM이 Rust와 Go를 작성하는 효율 차이가 생각보다 훨씬 컸음. 특히 초기 환경 문제를 고칠 수 있는 에이전트형 하네스에서는 더 그랬음
그걸 보고 꽤 강한 Rust 전도사가 됐음. 기존 코드베이스에서 호출할 배치 처리 도구를 Rust로 작성해 좋은 성과를 냈지만, 전체 프로덕션 마이그레이션은 아직 시도하지 않았음
글에서 말한 Go의 문제들, 특히 nil 처리 관련 문제는 Codex로 철저히 코드 리뷰하면 점점 해결되고 있다고 봄. 애초에 문제가 없으면 더 좋지만, 설계와 구현에 들인 노력만큼 리뷰와 이해에도 노력하는 개발자에게는 이런 보안 버그가 선택 사항이 되어가고 있음
언어 데이터는 https://gertlabs.com/rankings?mode=agentic_coding에 있음

자세한 컴파일러 오류와 강한 타입 시스템 덕분에 에이전트가 수정 → 컴파일 → 수정 반복을 다루기 쉬움
Rust는 사용자를 강하게 정해진 궤도 위로 올려놓음. Codex는 항상 뭔가 컴파일되게 만들어냄
단점은 때로는 관용적인 접근이 불가능할 때 실패해야 할 수도 있는데, 대신 컴파일되고 요청을 만족하는 멍청한 구현을 만들어낼 수 있다는 점임

LLM 관점에서 Rust의 약점은 컴파일 시간임
LLM은 사람보다 코드를 빠르게 쓰기 때문에, 상대적으로 컴파일을 기다리는 시간이 더 커짐. 10만 줄 이상 같은 어느 정도 규모 있는 프로젝트에서는 Rust의 약 10배 느린 컴파일이 병목으로 나타나기 시작함
핵심 인프라를 작성한다면 그 비용을 치를 만하지만, 인터넷에 공개되지 않은 내부 서비스를 만든다면 개발 속도가 더 큰 관심사일 수 있음
느린 컴파일은 사람의 개발 속도에도 영향을 준다고 보지만, 이상하게도 개발자들은 이를 정량화하려는 경우가 매우 드묾

“조직이 의존하고, 높은 가동 시간이 필요하며, 사업에 치명적인 서비스”라는 표현이 재미있음
그 Rust 서비스가 Kubernetes 위에서 돌 때는 특히 그렇다

이미 Rust를 쓰고 Go 경험은 없어서, 이 글이 나에게 아주 맞는 글은 아닐 수 있음
다만 한 가지 걸리는 점이 있음. Rust에서 데이터 경합이 “컴파일 타임에 잡힌다”고 말하는 건 적어도 조금은 과장처럼 느껴짐
이 표현은 Rust가 상호 잠금 기아 같은 것들이나 다른 동시성 문제까지 처리할 수 있다는 인상을 줄 수 있음. 실제로는 그렇지 않음 데이터 경합이 좁은 범위를 가진 형식적 용어라는 건 알지만, 그래도 더 명확하게 쓸 수 있다고 봄

AI 자동 생성 콘텐츠

본 콘텐츠는 GeekNews의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
2

댓글

0