본문으로 건너뛰기
피드

Go에서 Rust로 옮길 때 진짜로 바뀌는 것들

backend 약 11분
vote
0
댓글
북마크

이 글은 Go 백엔드 서비스를 Rust로 옮길 때 속도보다 컴파일 타임 보장, 런타임 트레이드오프, 개발자 경험이 더 중요하다고 설명한다. nil 패닉, 데이터 레이스, 에러 처리, 제네릭, 비동기 모델, 마이그레이션 전략까지 실무 관점에서 Go와 Rust를 길게 비교한다.

  • 1

    Go에서 Rust로 가는 이유는 대개 성능보다 정확성 보장과 운영 안정성에 있음

  • 2

    Rust는 Option, Result, Send, Sync, 소유권 모델로 Go의 관례와 런타임 검사 영역을 타입 시스템으로 끌어올림

  • 3

    Go의 빠른 컴파일, 단순한 고루틴 모델, 쿠버네티스 생태계 강점은 여전히 유지할 가치가 있음

  • 4

    성공적인 전환은 전체 재작성보다 핫패스, 워커, 게이트웨이 뒤 특정 엔드포인트부터 나누는 방식에 가까움

왜 Go에서 Rust로 가려는가

  • 이 글의 전제는 “Rust가 Go보다 빠르냐”가 아님. Go는 이미 백엔드에서 충분히 빠르고, 툴체인도 좋고, 배포도 편함

    • 글쓴이는 Go를 별로 좋아하지 않는다고 솔직히 밝히지만, JetBrains 개발자 생태계 조사에서 Go가 17~19% 수준의 꾸준한 점유율을 가진 성공한 언어라는 점도 인정함
    • Rust 컨설팅을 하는 사람이라 편향도 있지만, Go 서비스도 프로덕션에 올려본 경험을 바탕으로 비교한다고 선을 그음
  • Go에서 Rust로 옮기는 진짜 이유는 대체로 “성능 뽕”이 아니라 “정확성 보장” 쪽임

    • Go는 가비지 컬렉터, 런타임 race detector, if err != nil 관례에 많이 기대는 언어임
    • Rust는 소유권, Option, Result<T, E>, Send/Sync 같은 타입 시스템으로 메모리 관리, 데이터 레이스, 에러 처리를 컴파일러가 잡게 함
    • 바꿔 말하면 Go에서 사람이 머릿속으로 챙기던 규칙들이 Rust에서는 타입 에러가 됨

중요

> 글의 핵심은 Rust가 Go보다 무조건 낫다는 얘기가 아니라, “버그를 프로덕션에서 잡을 것인가, 컴파일 단계에서 잡을 것인가”의 비용 배분 문제임.

Go 패턴이 Rust에서 어떻게 바뀌는가

  • Go의 nil 포인터 문제는 Rust에서 Option로 강제 처리됨

    • Go에서는 repo.Find가 (*User, error)를 돌려주고 error가 nil이어도 user가 nil일 수 있음
    • 이걸 깜빡하고 user.Account.Notify() 같은 코드를 호출하면 몇 달 잘 돌던 서비스가 특정 코드 경로에서 터질 수 있음
    • Rust에서는 find가 Option를 반환하면 None 케이스를 처리하지 않고는 내부 값을 꺼낼 수 없음. “나중에 조심하자”가 아니라 “컴파일이 안 됨”임
  • Go의 go test -race는 훌륭하지만, 어디까지나 런타임 탐지기임

    • 테스트 중 실제 실행된 레이스만 잡을 수 있고, 부하 상황에서만 터지는 공유 map 변경 같은 문제는 프로덕션까지 갈 수 있음
    • Rust에서는 평범한 HashMap을 여러 스레드에서 마음대로 공유하려 하면 컴파일이 안 됨
    • Arc<Mutex<...>>, Arc<RwLock<...>>, 채널 같은 구조를 쓰도록 강제되면서 “락 까먹음” 류의 버그가 타입 오류로 바뀜
  • 에러 처리도 철학이 꽤 다름

    • Go의 if err != nil { return err }는 명시적이고 읽기 쉽다는 장점이 있음
    • 하지만 큰 코드베이스에서는 비즈니스 로직보다 에러 처리 보일러플레이트가 더 눈에 띄고, fmt.Errorf로 컨텍스트를 감싸는 것도 사람의 습관에 달림
    • Rust는 Result<T, E>, ? 연산자, thiserror의 #[from] 같은 패턴으로 전파와 변환을 간결하게 처리함
    • 새 에러 변형을 추가하면 match를 쓰는 곳에서 컴파일러가 빠진 분기를 알려주는 것도 큰 차이임
  • 제네릭은 Go와 Rust의 철학 차이가 가장 크게 드러나는 부분임

    • Go는 1.18에서야 제네릭을 받았고, 표준 라이브러리도 여전히 제한적으로만 사용함
    • Rust는 Option, Result<T, E>, Vec, HashMap<K, V>, Iterator처럼 표준 라이브러리의 바닥부터 제네릭 위에 세워져 있음
    • Go 제네릭은 메서드 자체 타입 파라미터가 안 되고, associated type이나 blanket impl 같은 trait 시스템도 없음
    • 그래서 복잡한 추상화로 가면 Go는 여전히 any, reflection, code generation으로 돌아가는 경우가 많음

동시성과 비동기에서 얻는 것과 잃는 것

  • Go의 고루틴 모델은 여전히 엄청난 생산성 장점임

    • 함수 앞에 go만 붙이면 런타임이 green thread처럼 돌려주고, 함수 시그니처를 바꿀 필요도 없음
    • async fn, await, executor 선택, Send/Sync bound 같은 “함수 색깔” 문제가 없음
    • 글쓴이도 Go 개발자가 Rust로 넘어오면 가장 그리워하는 게 이 부분이라고 봄
  • Rust의 async는 더 명시적이고 더 빡빡함

    • 백엔드에서는 거의 Tokio를 쓰고, async 함수는 Future를 반환하며 await되거나 spawn되기 전까지 실행되지 않음
    • await 지점 너머로 non-Send 값을 들고 있으면 컴파일러가 왜 문제인지 설명하며 막음
    • CPU를 오래 쓰는 작업을 async task 안에서 돌리면 executor를 굶길 수 있어서 spawn_blocking이나 rayon 같은 우회가 필요함
  • 그래도 Go 동시성도 공짜는 아님

    • WaitGroup과 sync.Once는 잘못 쓰면 고루틴 누수나 데드락이 생길 수 있음
    • context.Context는 취소 전파의 좋은 관례지만, 모든 호출 경로에 넘기는 건 개발자가 직접 챙겨야 함
    • 공유 mutable state에 Mutex를 빼먹어도 Go 컴파일러는 모름. Rust는 그걸 타입으로 더 많이 끌어올림
sequenceDiagram
    participant 개발자
    participant 고서비스 as Go 서비스
    participant 러스트컴파일러 as Rust 컴파일러
    participant 운영환경 as 운영 환경
    개발자->>고서비스: nil 체크나 락을 관례로 작성
    고서비스->>운영환경: 테스트를 통과한 코드 배포
    운영환경-->>개발자: 특정 경로에서 패닉이나 레이스 발생
    개발자->>러스트컴파일러: Option, Result, Send/Sync로 모델링
    러스트컴파일러-->>개발자: 누락된 None, 에러 분기, 공유 상태를 컴파일 오류로 지적
    개발자->>운영환경: 더 적은 런타임 장애로 배포

마이그레이션은 전면 재작성보다 작게 쪼개는 쪽

  • 성공 사례들은 대부분 “전부 갈아엎자”가 아니라 전술적 선택에 가까움

    • 마이크로소프트의 빅터 치우라는 “재미로 전부 Rust로 다시 쓰는 게 아니라, 새 컴포넌트에 Rust가 더 낫다고 판단될 때 고른다”는 식으로 설명함
    • 글쓴이도 Go에서 Rust로 가는 전환은 메서디컬하게 해야 한다고 강조함
  • 첫 번째 전략은 문제가 많은 핫패스를 별도 서비스로 떼는 것임

    • CPU를 많이 쓰거나, 지연 시간에 민감하거나, 신뢰성 이슈가 반복되는 서비스를 같은 API 계약 뒤에서 Rust로 다시 구현함
    • 나머지 Go 서비스는 HTTP나 gRPC로 그대로 호출하므로 내부 언어가 바뀌었는지 몰라도 됨
  • 워커나 사이드카도 좋은 첫 타깃임

    • 큐 컨슈머, ingestion pipeline, CPU-bound 배치 작업은 입력과 출력 경계가 명확함
    • 공유 인프로세스 상태가 적어서 Rust로 떼어내기 쉽고, 실패해도 blast radius가 비교적 작음
  • cgo로 Go에서 Rust를 직접 부르는 건 가능하지만 백엔드 서비스에는 별로 추천하지 않음

    • 빌드 복잡도와 FFI 오버헤드가 커져서, 보통은 네트워크 경계 뒤에 Rust 서비스를 세우는 편이 낫다고 봄
    • 라이브러리나 CLI 도구라면 얘기가 조금 다르지만, 일반 서비스 마이그레이션에서는 우선순위가 낮음

숫자로 보면 기대치는 현실적으로 잡아야 함

  • 글쓴이가 본 Go-to-Rust 전환의 대략적인 개선 폭은 꽤 현실적임

    • CPU 사용량은 20~40% 개선되는 경우가 있었음
    • 메모리는 30~50% 줄어드는 편이라고 함. 주로 GC 오버헤드와 런타임 크기 차이 때문임
    • P99 지연 시간은 평균보다 꼬리 구간이 더 안정되는 쪽의 효과가 큼
  • 하지만 10배 처리량 같은 드라마틱한 숫자를 기대하면 실망하기 쉬움

    • Go는 이미 충분히 빠른 언어라 Python에서 Rust로 옮길 때 같은 극적인 차이는 잘 안 나옴
    • 더 큰 가치는 “바보 같은 실수”가 줄고, GC로 인한 지터가 줄고, 온콜에서 이상한 멀티스레드 버그를 덜 보는 쪽임
  • Go를 계속 써야 할 영역도 분명함

    • 쿠버네티스 operator, controller, CRD 같은 생태계는 Go가 압도적으로 강함
    • 빠른 컴파일, 쉬운 cross-compilation, 단순한 배포가 중요한 CLI나 개발 도구도 Go가 잘 맞음
    • 얇은 API 레이어, 프록시, 포맷 변환기처럼 속도보다 팀 생산성이 중요한 glue service는 Rust의 보일러플레이트가 오히려 부담일 수 있음

💡

> 팀에서 Go를 Rust로 옮기고 싶다면 “가장 중요한 서비스”가 아니라 “경계가 명확하고 장애 비용이 큰 서비스”부터 고르는 게 현실적임. API 계약을 그대로 두면 클라이언트는 마이그레이션을 몰라도 됨.


기술 맥락

  • 이 글에서 가장 중요한 선택은 Go의 관례 기반 안정성을 Rust의 타입 기반 안정성으로 바꾸는 거예요. nil 체크, 에러 전파, 락 사용 같은 걸 사람이 리뷰와 습관으로 챙기던 방식에서, 컴파일러가 빠진 경로를 막는 방식으로 옮기는 거거든요.

  • 왜 Rust를 고르느냐는 단순한 성능 문제가 아니에요. Go도 백엔드에서 충분히 빠르기 때문에, 핵심은 장애 비용이에요. 데이터 레이스, nil 패닉, 누락된 에러 처리 때문에 온콜이 자주 깨지는 팀이라면 Rust의 빡빡함이 비용이 아니라 보험처럼 작동해요.

  • 반대로 모든 서비스를 Rust로 바꾸는 건 좋은 전략이 아니에요. 쿠버네티스 주변 도구나 얇은 glue service처럼 Go 생태계와 빠른 반복이 더 중요한 곳은 Go가 계속 이겨요. 그래서 글에서 말하는 현실적인 방향은 언어 교체가 아니라 역할 분담에 가까워요.

  • 구현 방식도 네트워크 경계가 있는 서비스부터 떼는 쪽이 좋아요. 같은 REST나 gRPC 계약을 유지하고, 게이트웨이 뒤에서 특정 엔드포인트나 워커만 Rust로 바꾸면 실패했을 때 되돌리기도 쉽고 팀 학습 부담도 줄어들어요.

Go를 까고 Rust를 찬양하는 글처럼 보이지만, 핵심은 “언어 교체”가 아니라 “어떤 장애 비용을 컴파일러에게 미리 넘길 것인가”에 있다. 한국 백엔드 팀도 트래픽, 온콜, 채용, 빌드 시간까지 같이 놓고 봐야 현실적인 판단이 됨.

댓글

댓글

댓글을 불러오는 중...

backend

Python 3.15에서 헤드라인은 못 탔지만 꽤 쓸만한 기능들

Python 3.15에는 lazy imports나 Tachyon profiler 같은 큰 기능 말고도 실무에서 바로 체감될 만한 작은 개선들이 들어가. TaskGroup 취소, 컨텍스트 매니저 데코레이터 개선, 스레드 안전 이터레이터처럼 평소 애매하게 불편했던 지점들이 꽤 깔끔해졌어.

backend

심평원, DUR부터 의료영상 심사까지 클라우드로 갈아엎는다

심평원이 정보시스템 클라우드 전환과 함께 병·의원 업무에 직접 닿는 DUR, 의료영상 AI 심사, 요양급여내역 조회 시스템을 고도화한다. 핵심은 설치형 프로그램 중심이던 연계를 웹과 API 기반으로 넓히고, 진료·청구 과정에서 실시간 확인과 자동 판독을 강화하는 쪽이다.

backend

윈도우 에러 코드 7번 ‘ERROR_ARENA_TRASHED’는 어디서 왔을까

ERROR_ARENA_TRASHED는 Win32에서 실제로 쓰이는 현대적 에러라기보다 MS-DOS 시절 메모리 관리 구조에서 넘어온 잔재야. MS-DOS가 메모리 블록 앞의 arena 시그니처를 훑다가 예상한 값이 아니면 ‘arena가 망가졌다’고 보고 이 에러를 냈다는 이야기야.

backend

C/C++ 컴파일러의 느슨한 메모리 동시성 버그를 자동으로 잡는 박사논문

C와 C++ 컴파일러에서 relaxed memory 동시성 버그를 찾는 자동 테스트 프레임워크를 다룬 박사논문이 공개됐어. Téléchat, Atomic-mixer 같은 도구로 소스 수준 동작과 컴파일된 프로그램 동작을 비교하고, LLVM과 GCC 툴체인에서 실제 버그를 찾아낸 내용이 핵심이야.

backend

자바 Valhalla가 도메인 원시 타입의 성능 핑계를 지워가고 있음

글은 Project Valhalla의 value class가 자바에서 도메인 제약을 타입으로 표현할 때 생기던 성능 비용을 크게 줄인다고 설명한다. PositiveInt 같은 래퍼 타입이 기존엔 객체 헤더와 포인터 추적으로 비쌌지만, Valhalla에서는 정적 타입이 맞을 때 배열 슬롯이나 객체 필드에 평평하게 저장될 수 있다. 다만 제네릭 박싱, 프레임워크 어댑터, == 의미 변화 같은 현실적인 주의점은 아직 남아 있다.