들어가며
Stack Overflow는 매년 개발자 설문에서 "써본 사람이 내년에도 계속 쓰고 싶어 하는 언어"를 집계합니다. 2016년부터 2022년까지는 Most Loved, 2023년부터는 Most Admired라는 이름으로 발표되는데, 이 항목의 1위를 2025년까지 10년 연속 지키고 있는 언어가 있습니다. 바로 Rust입니다.
리눅스 커널은 2022년 말 6.1 버전에서 C와 어셈블리 외의 언어로는 처음으로 Rust를 받아들였고, 2025년 12월에는 "실험" 딱지를 떼고 커널의 공식 언어로 확정했습니다. 마이크로소프트는 윈도우 커널의 일부 구성 요소를 Rust로 다시 쓰고 있고, 그중 일부는 이미 프로덕션 윈도우에 반영되어 있습니다. 2022년에는 미국 국가안보국(NSA)이, 2024년에는 백악관이 메모리 안전 언어 사용을 권고하는 보고서에서 Rust를 예시로 지목했습니다.
그런데 Rust는 왜 이렇게까지 좋은 평가를 받는 걸까요? 알아보다 보니 언어 설계 자체가 흥미로워서 내용을 정리해보려 합니다. 언어가 가진 특징을 먼저 살펴보고, 실제로는 어디에 쓰이고 있는지 다룹니다.
필자는 Rust에 대한 기초적인 이해만 가지고 있어, 이 글은 실무 팁이나 장기 운영 관점의 조언이 아닙니다.
본문을 읽는 데는 Java, TypeScript(Spring·NestJS), C에 대한 기본적인 지식이 필요합니다.
Rust란?
Rust는 2006년 모질라 직원이던 그레이든 호어(Graydon Hoare)가 개인 프로젝트로 시작했습니다. 처음부터 회사 프로젝트였던 것은 아니고, C++ 수준의 성능을 유지하면서 메모리 오류가 원천적으로 불가능한 언어를 만들어 보려는 개인 실험이었습니다.
2009년부터 모질라가 공식 후원을 시작하면서 회사 차원의 프로젝트가 됩니다. 모질라에게는 절실한 이유가 있었습니다. 파이어폭스의 브라우저 엔진이 C++로 작성되어 있었는데, 메모리 오류와 동시성 버그가 끝없이 나왔기 때문입니다. 모질라는 병렬 처리에 안전한 차세대 브라우저 엔진(Servo)을 만들 언어로 Rust를 택했고, "C++의 성능을 유지하면서 메모리 안전을 컴파일 타임에 보장한다"는 목표가 이때 언어의 정체성으로 자리잡습니다.
2015년 5월 1.0이 정식 출시됐고, 2021년부터는 모질라에서 독립한 러스트 재단(Rust Foundation)이 언어를 관리합니다. AWS, 구글, 마이크로소프트, 화웨이, 모질라가 창립 멤버로 참여했습니다.
런타임의 일을 컴파일러로
Rust의 설계를 관통하는 하나의 원칙은 "런타임에 하던 일을 모두 컴파일 타임으로 옮기자"입니다.
- C: 런타임도 컴파일러도 도와주지 않습니다. 메모리 관리부터 스레드 안전까지 전부 개발자 책임입니다. 빠르지만 위험합니다.
- Java·TS: 런타임(JVM, V8)이 안전과 편의를 담당합니다. GC가 메모리를 청소하고, 리플렉션이 런타임에 타입 정보를 읽습니다.
- Rust: 이 작업들을 전부 컴파일 타임으로 옮깁니다.
이 원칙을 zero-cost abstraction이라고 부릅니다. Java의 스트림이나 JS의 클로저 같은 고수준 추상화는 런타임(JIT)이 내부적으로 최적화하더라도 중간 객체 할당과 GC 부담 같은 비용이 남습니다.
Rust는 이러한 추상화를 빌드 시점에 모두 확정적으로 풀어내서 손으로 짠 루프와 같은 기계어를 만듭니다. 같은 편의를 누리면서 런타임 비용은 없다는 뜻입니다.
이러한 철학에서 아래의 특징들이 파생됩니다.
- GC가 없습니다. 메모리 해제 시점을 컴파일러가 계산합니다.
- 예외가 없습니다. Result 타입이 대신합니다.
- null이 없습니다. Option 타입이 대신합니다.
- 클래스 상속이 없습니다. 트레이트(trait)가 대신합니다.
본문을 읽기 위한 최소 문법
let a = 5; // JS의 const에 해당. 기본이 불변
let mut a = 5; // JS의 let에 해당. 수정하려면 mut를 명시
String::from("hi"); // ::는 정적 메서드 접근. Java의 Integer.parseInt 위치
vec![1, 2, 3]; // Vec은 ArrayList 포지션의 가변 길이 리스트
println!("{}", x); // console.log / printf. {} 자리에 값이 들어감
&x; // "빌린다"는 표시. C의 &(주소 연산자)와 기원이 같지만 의미가 확장됨
fn foo() -> i64 {} // 함수 선언. -> 뒤가 리턴 타입. i64는 Java의 long
|x| x + 1; // 클로저. JS의 (x) => x + 1. 인자가 없으면 ||
vec!, println!처럼 !가 붙은 것은 함수가 아니라 매크로입니다. 컴파일 타임에 코드로 펼쳐지는데, C의 #define처럼 단순 텍스트 치환이 아니라 문법 구조를 이해하는 안전한 매크로입니다. Rust는 printf처럼 인자 개수가 유동적인 함수를 일반 함수로 만들 수 없어서 이런 것들을 매크로로 제공합니다.
Rust는 기본적으로 모든 변수가 불변입니다. Java가 final을, C가 const를 붙이는 문화라면 Rust는 가변을 허가받는 문화입니다. TS의 const 우선 컨벤션, Kotlin의 val, Swift의 let처럼 불변을 우선하는 최신 언어들의 흐름과 같은 방향인데, 다른 언어에서 컨벤션이나 권장이던 것이 Rust에서는 언어 기본값입니다.
소유권(Ownership), 대입의 의미가 다르다
Rust의 핵심은 소유권입니다. 공식 문서는 소유권을 세 가지 규칙으로 정의합니다. 모든 값에는 주인이 있고, 주인은 한 번에 하나뿐이며, 주인이 스코프를 벗어나면 값은 해제됩니다.
C의 대입: 그냥 복사, 책임은 직접
char* a = malloc(6);
strcpy(a, "hello");
char* b = a; // 포인터 값 복사. 같은 메모리를 가리키는 포인터가 둘
free(a); // a로 반납했는데
printf("%s", b); // b는 그 사실을 모름. 컴파일 통과, 런타임에 미정의 동작
free(b); // double free. 힙 메타데이터 손상
C의 문제는 포인터가 몇 개 복사됐는지, 누가 free 책임자인지 언어가 전혀 추적하지 않는다는 점입니다. 전부 사람이 직접 신경 써야 합니다.
JS·Java의 대입: 참조 복사, 책임은 GC가 대신
const a = ["hello"];
const b = a; // 참조 복사. 배열은 하나, 가리키는 변수가 둘
b.push("world");
console.log(a); // ["hello", "world"]. a도 바뀜
이 배열이 메모리에서 언제 지워지는지 아무도 모릅니다. GC가 나중에 힙을 조사해 어디서도 접근할 수 없게 된 상태(도달 불가능, unreachable)를 확인한 뒤에야 제거합니다. 이 조사를 런타임 내내 주기적으로 수행합니다.
a와 b가 연결되어 있다는 사실이 코드에 드러나지 않아 공유 가변 상태 버그의 근원이 됩니다. C의 double free 같은 문제는 사라졌지만, 다른 데에서 b를 고쳤는데 a로 영향이 가는 문제는 그대로 남아 있습니다. Java 진영에서 Collections.unmodifiableList나 List.copyOf, 불변 객체 설계를 권장해 온 것도 이러한 이유입니다.
Rust의 대입: 소유권 이전
let a = vec![String::from("hello")]; // 리스트 생성. 주인은 a
let b = a; // 참조 복사가 아니라 소유권 이전(move)
println!("{:?}", a); // 컴파일 에러: borrow of moved value
let b = a;는 명의 이전입니다. 넘긴 순간 a는 사용할 수 없고, 컴파일러가 이를 차단합니다. 규칙은 단순합니다. 모든 값의 주인은 항상 정확히 하나입니다.
이런 규칙 때문에 "주인이 스코프를 벗어나는 순간 = 메모리 해제 시점"이 컴파일 타임에 확정되고, 컴파일러가 그 지점에 free에 해당하는 코드를 자동으로 삽입할 수 있게 됩니다. 다시 한번 말하지만, 런타임이 아니라 컴파일 타임입니다.
빌림(Borrowing), 소유권 규칙의 실용화
여기까지만 보면 깔끔한 규칙인데, 이 상태로 과연 편하게 개발할 수 있을까요?
fn validate(order: Order) -> Order {
// ...검사 로직...
order // 다 쓴 값을 되돌려줌. 안 돌려주면 호출한 쪽은 order를 더 못 씀
}
let order = Order::new();
let order = validate(order); // 넘겼다가 되돌려받고
let order = calc_total(order); // 또 넘겼다가 되돌려받고
save(order); // 함수를 지날 때마다 반복
함수 호출도 대입처럼 소유권을 넘깁니다. Java나 TS라면 validate(order) 한 줄이면 될 호출이, 소유권 규칙 아래에서는 함수마다 값을 리턴으로 돌려주고 되받는 코드로 불어납니다.
그래서 Rust는 명의는 두고 잠깐 빌려 쓰는 방법을 마련해 뒀습니다. 소유권을 가져가지 않고 값을 가리키기만 하는 참조(reference)를 &로 만들 수 있고, 참조를 만드는 것을 빌림(borrowing)이라고 부릅니다.
fn validate(order: &Order) { // &: 소유권 대신 참조를 받음
// ...검사 로직...
}
let order = Order::new();
validate(&order); // 빌려주기만 하고 주인은 그대로
calc_total(&order); // 되돌려받는 코드가 사라짐
save(order); // 마지막에만 소유권을 넘김
겉모습은 C의 포인터와 비슷하지만 결정적인 차이가 있습니다. C의 포인터는 원본이 free된 후에도 살아 있을 수 있습니다(dangling pointer). Java나 TS에서는 애초에 참조로 객체를 다루는 것이 기본이고, 참조가 살아 있는 한 GC가 원본을 지우지 않으므로 안전합니다. Rust는 &로 명시해서 쓰되, 참조가 원본보다 오래 살 수 없다는 것을 컴파일러가 검사합니다.
참조에도 변수와 같은 원칙이 적용됩니다. 변수가 기본 불변이듯 참조도 기본 불변이라, 참조는 두 종류로 나뉩니다.
| 구분 | 표기 | 할 수 있는 일 |
|---|---|---|
| 불변 참조(immutable reference) | &order | 읽기 |
| 가변 참조(mutable reference) | &mut order | 읽기 + 수정 |
빌린 쪽에서 값을 고치려면 가변 참조를 명시해야 합니다.
fn cancel(order: &mut Order) { // &mut Order: 고칠 수 있는 참조
order.status = OrderStatus::Canceled;
}
let mut order = Order::new(); // 원본이 mut로 선언돼 있어야 가변 참조도 가능
cancel(&mut order); // 호출부에도 &mut가 그대로 보임
그리고 가변 참조에는 강한 제약이 붙습니다. 공식 문서의 규칙을 그대로 옮기면 "어느 시점이든, 가변 참조 하나 또는 불변 참조 여러 개만 가질 수 있다"입니다. 여럿이 함께 읽거나 한 곳에서 혼자 고치거나, 값 하나는 둘 중 한 상태만 가능합니다.
let mut order = Order::new();
let r1 = ℴ // 불변 참조
let r2 = ℴ // 불변 참조는 동시에 여러 개 가능
let w = &mut order; // 컴파일 에러: 불변 참조가 살아 있는데 가변 참조 시도
println!("{:?}", r1); // r1을 여기서 쓰기 때문에 위 시점에 아직 살아 있는 것
가변 참조 제약의 의의
배열의 첫 요소를 가리켜 두고, 요소를 추가한 뒤, 가리켜 둔 것을 읽는 상황입니다. 먼저 C로 동적 배열에 값을 추가하는 코드를 작성해 보겠습니다.
// 가득 찬 배열에 값을 추가하는 함수. 이해를 위해 단순화한 구현
int* append(int* v, int len, int value) {
v = realloc(v, (len + 1) * sizeof(int)); // 더 큰 메모리를 새로 잡고 전체 이사
v[len] = value;
return v; // 이사한 새 주소를 돌려줌
}
int* v = malloc(3 * sizeof(int)); // 용량 3짜리 배열
int* r = &v[0]; // 첫 요소의 주소를 저장
v = append(v, 3, 4); // 내부에서 realloc. 메모리가 이사함
printf("%d", *r); // r은 옛 주소를 가리킴. 컴파일 통과, 런타임에 오동작
이사가 일어나는 순간 r이 들고 있는 옛 주소는 해제된 쓰레기 메모리가 됩니다. 컴파일러는 이를 잡지 못하고, 런타임에서야 문제가 발생합니다.
같은 코드를 Rust로 쓰면 컴파일이 되지 않습니다.
// Vec의 push. 표준 라이브러리 내부 구현을 이해를 위해 단순화한 것
pub fn push(&mut self, value: T) { // 자기 자신을 가변 참조로 받음
if self.len == self.cap {
self.grow(); // 가득 찼으면 더 큰 메모리로 전체 이사
}
// ...새 자리에 value 기록...
}
let mut v = vec![1, 2, 3]; // 고칠 것이므로 mut 선언
let r = &v[0]; // 첫 요소의 불변 참조
v.push(4); // 컴파일 에러: 불변 참조가 살아 있는데 가변 참조 시도
println!("{}", r);
v.push(4)는 push가 &mut self를 받으므로 v의 가변 참조를 만드는 호출이고, 불변 참조 r이 살아 있으니 규칙 위반입니다. Rust에서 값을 바꾸는 메서드는 &mut self로 선언하는 것이 원칙이므로, 값을 바꾸는 호출이면 가변 참조가 생긴다고 보면 됩니다. C는 이런 코드를 열어두고 오류 가능성을 감수하고, Rust는 열어두되 안전을 컴파일 타임에 증명합니다.
Java나 TS를 쓰면서 이런 오류를 겪어본 적이 없는 이유는, 애초에 이런 코드를 쓸 수 없기 때문입니다. Java에는 리스트 내부 요소의 메모리 위치를 가리키는 참조가 없습니다. 리스트 객체를 가리키는 참조만 있고, 내부 배열이 이사해도 런타임과 GC가 알아서 반영합니다.
메모리 안전이란?
앞에서 메모리 안전이라는 말을 여러 번 썼는데, 이것이 무엇을 뜻하는지 잠깐 짚고 가려 합니다.
메모리 안전이란 프로그램이 해제된 메모리, 권한 없는 메모리, 초기화되지 않은 메모리에 접근하는 일이 절대 발생하지 않음을 보장하는 성질입니다.
C는 이 성질을 보장하지 않습니다. 메모리를 확보하고 반납하는 책임이 전부 개발자에게 있고, 실수해도 컴파일러는 알려주지 않습니다.
char* a = malloc(6);
char* b = a; // 같은 메모리를 가리키는 포인터가 둘
free(a);
free(b); // double free. 컴파일은 통과하고 런타임에 힙이 손상됨
증상은 컴파일 시점이 아니라 나중에, 다른 곳에서, 비결정적으로 나타납니다. 해제된 자리에 이미 다른 데이터가 할당돼 있으면 그 데이터를 읽거나 덮어쓰게 되고, 공격자가 이 타이밍을 조작하면 그대로 보안 취약점이 됩니다.
Java와 JS는 이 성질을 확보했습니다. 참조가 남아 있는 객체를 GC가 해제하지 않으므로, 해제된 메모리에 접근하는 일 자체가 불가능합니다. 대신 아래와 같은 비용이 발생합니다.
- GC가 주기적으로 힙을 조사하면서 CPU를 소모합니다.
- 조사 중 stop-the-world가 응답 지연 스파이크로 이어집니다.
- 해제 시점을 GC에 맡기므로 실사용량보다 여유 있는 힙이 필요하고, 그만큼 메모리를 더 점유합니다.
그리고 두 스레드가 동시에 같은 데이터를 수정하는 data race는 GC만으로 막을 수 없고, Java에서도 그대로 컴파일된 뒤 런타임에 비결정적인 결과로 나타납니다.
Rust는 앞에서 본 소유권과 빌림 규칙으로 이 성질을 컴파일 타임에 확보합니다. 규칙을 지키는지 증명하지 못하는 코드는 빌드가 거부됩니다. 대표적인 메모리 오류 유형이 C와 Rust에서 각각 어떻게 되는지 정리하면 다음과 같습니다.
| 오류 유형 | C | Rust |
|---|---|---|
| use-after-free | 해제된 메모리에 접근. 런타임에 비결정적으로 오동작 | 컴파일 에러 |
| double free | 같은 메모리를 두 번 해제. 힙 메타데이터 손상 | 발생 불가 (해제는 주인 소멸 시 1회, 컴파일러가 삽입) |
| dangling pointer | 해제된 메모리를 가리키는 포인터가 남음. 읽으면 쓰레기값 | 컴파일 에러 (참조가 원본보다 오래 살 수 없음) |
| data race | 두 스레드가 동시에 같은 메모리를 수정. 결과가 매번 달라짐 | 컴파일 에러 (가변 참조 동시 1개 규칙) |
| buffer overflow | 할당된 범위를 넘어서 씀. 인접 메모리 오염 | 런타임에 즉시 패닉 (조용한 오염 없음) |
Java의 비용은 지불하지 않으면서, Java가 막지 못하던 data race까지 막는 셈입니다.
| 메모리 안전 | 비용 | 발견 시점 | |
|---|---|---|---|
| C·C++ | 보장 안 됨 | 없음 (그래서 빠름) | 운영 중, 비결정적 |
| Java·JS | 보장됨 | GC (CPU, stop-the-world, 메모리) | 레이스는 운영 중 |
| Rust | 보장됨 | 빌드 시간 + 학습 곡선 | 컴파일 중 |
동시성(Concurrency), 빌림 규칙이 스레드 안전까지
혹시 앞서 설명한 빌림 규칙에서 묘한 기시감을 느끼지 않으셨나요? "여럿이 함께 읽거나, 한 곳에서 혼자 고치거나"는 동시성 프로그래밍에서 data race를 막는 고전 규칙, 읽기-쓰기 락(readers-writer lock)과 같은 모양입니다.
백엔드 관점에서는 여러 트랜잭션이 함께 읽는 DB의 공유 락(shared lock)과 한 트랜잭션만 쓰기를 허용하는 배타 락(exclusive lock)이 각각 불변 참조와 가변 참조에 해당합니다. 이러한 동시성 문제를 Rust는 애초에 컴파일 타임에 검사합니다.
먼저 C의 상황입니다.
int counter = 0;
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
void* work(void* arg) {
for (int i = 0; i < 50000; i++) counter++; // 아무 보호 없이 전역을 수정
return NULL;
}
// pthread 두 개로 실행하면 결과가 매번 다름. 컴파일러는 침묵
// 해결: 락으로 감싼다
pthread_mutex_lock(&m);
counter++;
pthread_mutex_unlock(&m);
Java도 다르지 않습니다.
int[] counter = {0};
Object lock = new Object();
Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) counter[0]++; });
Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) counter[0]++; });
// 컴파일 정상. 실행 결과는 100000이 아니라 매번 다른 값
// 해결: synchronized로 감싼다
synchronized (lock) { counter[0]++; }
counter++는 읽기, 더하기, 쓰기 세 단계로 나뉘어 실행되므로 두 스레드의 단계가 겹치면 경합이 생기고 증가가 유실될 수 있습니다. 문제는 두 언어 모두 보호를 빠뜨린 코드도 경고 없이 컴파일된다는 점입니다. 동시성을 의식하는 개발자라도 실수할 수 있고, 그 실수는 운영 환경에서야 드러납니다.
Rust에서 보호 없이 같은 시도를 하면 이렇게 됩니다.
let mut counter = 0;
std::thread::spawn(|| { counter += 1; }); // 컴파일 에러
std::thread::spawn(|| { counter += 1; });
두 클로저가 counter의 쓰기 권한을 동시에 가져가려 하는데, 이는 "가변 참조는 동시에 하나" 규칙에 위배됩니다. 컴파일러는 여기에 더해 스레드가 지역 변수보다 오래 살 수 있다는 문제까지 지적하며 빌드를 거부합니다.
use std::sync::{Arc, Mutex}; // Java의 import, C의 #include 위치
let counter = Arc::new(Mutex::new(0)); // 값 0을 락으로 감쌈. Arc는 여러 스레드 공유를 허용하는 스마트 포인터
let c = Arc::clone(&counter); // 공유 카운트 +1
std::thread::spawn(move || { // move: 클로저가 c의 소유권을 가져감
for _ in 0..50000 { // 0..50000 = for (i = 0; i < 50000; i++)
*c.lock().unwrap() += 1; // lock()으로 락 획득. 스코프 종료 시 자동 해제
}
});
값에 동기화를 붙인다는 발상 자체는 AtomicInteger도 같습니다. 차이는 적용 범위입니다. Java의 원자 타입은 정수나 참조처럼 정해진 종류에만 있어서, 구조체나 컬렉션을 함께 갱신해야 하면 synchronized나 ReentrantLock으로 코드 블록을 감싸는 방식을 사용해야 하고, 언어가 이를 강제하지는 않습니다. Rust의 Mutex<T>는 T가 무엇이든 감싸고, lock()을 거치지 않고 내부 값을 꺼낼 수 없습니다. Rust에서 락은 타입입니다.
이와 같이 빌림 규칙은 스레드 경계에서도 그대로 안전 규칙이 됩니다. 메모리 안전을 위해 만든 규칙이 동시성 안전까지 함께 보장하는 것이고, Rust 진영은 이를 fearless concurrency라고 부릅니다.
null과 예외가 없는 Rust
이뿐만이 아닙니다. 런타임 에러의 흔한 원인인 null과 예외도 Rust에는 없고, 이를 대신하는 타입이 처리를 강제합니다.
null 대신 Option
User* user = find_by_email(email); // 실패하면 NULL 반환하는 관례
printf("%s", user->name); // NULL 체크 깜빡. 세그폴트
C는 null을 타입에 표현할 방법이 없습니다. Java에는 Optional이 있고 실제로 널리 사용되지만, 이를 강제하지 않습니다.
Rust는 이렇게 됩니다.
fn find_by_email(email: &str) -> Option<User> // &str: 소유권 이전 없이 문자열을 빌려 받음
let user = find_by_email("a@b.com");
// user.name; // 컴파일 에러. Option은 상자이지 User가 아님
match user { // match: 모든 경우의 처리를 강제하는 switch
Some(u) => println!("{}", u.name),
None => println!("없음"), // 이 줄을 지우면 컴파일 에러
}
상자를 열지 않으면 내용물에 접근할 수 없고, 열 때 None 경우를 누락하면 컴파일이 실패합니다. TS에서 !로 null 검사를 우회하는 것처럼 Rust에도 unwrap()이라는 탈출구가 있긴 합니다. 다만 !는 틀리면 undefined가 조용히 흘러다니다 엉뚱한 곳에서 터지는 반면, unwrap()은 값이 없으면 그 자리에서 즉시 패닉한다는 차이가 있습니다. 패닉도 프로그램 중단이므로 유의해서 사용해야 합니다. 2025년 11월 클라우드플레어 대규모 장애의 직접 원인도 unwrap() 패닉이었습니다.
예외 대신 Result
int port = Integer.parseInt(config.get("port"));
// NumberFormatException은 unchecked. 시그니처만 봐선 던지는지도 모름
C의 리턴 코드는 무시해도 아무 일이 없고, Java의 예외는 시그니처에 없거나(unchecked) 있어도 빈 catch로 지나칠 수 있습니다.
Rust는 실패 가능성을 리턴 타입 자체에 넣습니다.
fn load_port(config: &Config) -> Result<u16, ConfigError> {
// 성공 시 u16, 실패 시 ConfigError
// u16은 0~65535 정수. "포트에 음수나 70000 불가"까지 타입으로 표현
let raw = config.get("port").ok_or(ConfigError::Missing)?;
let port = raw.parse().map_err(|_| ConfigError::Invalid)?;
Ok(port)
}
?는 "실패면 에러를 들고 즉시 return, 성공이면 값을 꺼내 계속 진행"입니다. Java로 옮기면 ? 하나가 try-catch 여섯 줄에 해당합니다. 실패가 예외가 아니라 리턴 값이므로 시그니처만 봐도 실패 가능성이 드러나고, 받은 Result를 처리하지 않고 버리면 컴파일러가 경고까지 합니다.
단점
어느 정도 예상하셨겠지만, 언어의 특성상 아래와 같은 단점이 뒤따릅니다.
- 학습 곡선: borrow checker 개념이 익숙지 않고, async와 라이프타임이 만나는 부분의 컴파일 에러가 상당히 난해합니다. 컴파일 시점에 잡아주는 것이 많아 AI로 작성하기 좋은 언어로 보일 수 있지만, 훈련 데이터가 적어 AI도 주류 언어보다 Rust를 서툴게 다룬다는 보고가 있습니다.
- 컴파일 속도: 컴파일러가 하는 일이 많은 만큼 느립니다.
- 프로토타이핑: 컴파일러가 항상 온전히 완성된 코드를 요구하므로, 소유권 개념을 신경 쓰고 모든 상황에서 null과 예외를 고려해야 하는 등 프로토타이핑에 불리합니다.
- 생태계와 채용: Spring Security, Batch, Cloud급의 통합 솔루션이 아직 없어 인증·배치 등을 라이브러리 조합으로 직접 구성해야 하는 경우가 많고, Rust가 가능한 서버 개발자 풀도 작은 편입니다.
실제 사용처
Rust는 예상보다 가까운 곳에서 많이 쓰이고 있습니다. 실제로 어디에서 사용되고 있는지 살펴보려 합니다.
MSA 내부의 백엔드 서비스
MSA에서는 서비스 간 통신이 gRPC·Kafka 같은 언어 중립 계층이라 각 서비스의 구현 언어가 무관합니다.
Rust는 비즈니스 로직 중심 애플리케이션을 통째로 대체하려는 언어라기보다 성능과 신뢰성이 병목인 지점에 투입되는 언어에 가깝습니다. 그래서 CRUD 중심의 대다수 서비스는 생산성 좋은 Spring·Nest를 유지하고, 트래픽이 집중되거나 p99가 SLA를 위협하는 한두 서비스만 Rust로 재작성합니다.
디스코드의 경우엔 메시지 읽음 표시를 담당하는 Read States 서비스가 Go로 작성되어 있었는데, Go 런타임이 힙 상황과 무관하게 최소 2분마다 GC를 강제 실행했고 그때마다 거대한 LRU 캐시를 스캔하면서 주기적인 레이턴시 스파이크가 발생했습니다. 해당 서비스만 Rust로 재작성하자 스파이크가 사라졌고 레이턴시·CPU·메모리 전 지표가 개선됐습니다.
웹 프레임워크는 Tokio 팀이 만든 Axum이 가장 널리 쓰입니다. 리플렉션이 없는 언어 특성상 Spring의 애노테이션이나 Nest의 데코레이터 기반 DI 컨테이너 같은 개념이 없고, 의존성 조립을 main에 직접 작성합니다. ORM도 빈약한데, 대신 주류인 sqlx는 query_as! 매크로가 빌드 시점에 실제 개발 DB에 접속해 SQL을 검증합니다. 비동기 처리는 Node처럼 이벤트 루프 방식인데, 차이가 있다면 런타임인 Tokio가 코어 수만큼 워커 스레드를 띄워 전 코어를 활용한다는 점입니다.
인프라 / 미들웨어
모든 요청이 거쳐 가는 컴포넌트라, 작은 오버헤드도 요청마다 반복되어 전체 규모에서는 큰 비용이 됩니다.
대표 사례가 클라우드플레어의 Pingora입니다. 오리진 서버와의 연결을 담당하던 nginx(C로 작성됨) 기반 프록시 계층을 자체 Rust 프록시로 교체한 것으로(2022년 발표, 2024년 오픈소스), 같은 트래픽 기준 CPU 약 70%, 메모리 약 67%를 절감했다고 밝혔습니다. C의 성능은 유지하면서 C의 메모리 취약점 리스크를 제거하려는 목적으로, "C의 자리를 Rust가 대체한다"는 흐름의 상징적인 사례입니다.
AWS Lambda와 Fargate를 구동하는 마이크로VM Firecracker, Deno 런타임의 코어도 Rust로 작성됐습니다. 메모리 수십 MB, 레이턴시 1ms의 작은 차이도 서버 수천 대 규모에서는 큰 비용 차이가 되므로, Rust 도입 비용을 감수할 만한 영역입니다.
개발 도구의 엔진으로
사용자가 쓰는 언어는 그대로 두고, 내부 엔진만 Rust로 교체하는 방식입니다.
프론트엔드 도구가 대표적입니다. Next.js 12부터 기본 컴파일러가 된 SWC, Next.js 16부터 기본 번들러가 된 Turbopack, Vite 8부터 기본 번들러가 된 Rolldown, Tailwind CSS v4가 채택한 Lightning CSS가 모두 Rust로 작성됐습니다. ESLint와 Prettier를 통합 대체하는 Biome, Electron 대체 데스크톱 프레임워크 Tauri도 Rust입니다.
브라우저 안에서 네이티브급 연산이 필요할 때는 WebAssembly를 씁니다. JS 대신 C++나 Rust 같은 언어로 작성한 코드를 브라우저가 실행할 수 있는 바이너리로 컴파일하는 기술로, Figma의 편집 엔진, 구글 어스, 포토샵 웹이 이 방식으로 돌아갑니다. 여기서도 새로 작성하는 모듈은 Rust가 1순위 언어로 꼽히는데, GC가 없어 런타임을 바이너리에 포함할 필요가 없기 때문입니다.
파이썬 진영도 같은 구조입니다. pandas를 대체하는 Polars, pip를 대체하는 uv, 린터 ruff가 모두 Rust 코어입니다. 특히 uv와 ruff는 기존 도구보다 10배에서 100배 빠른 속도를 앞세워, 몇 년 만에 파이썬 생태계의 표준 도구로 자리 잡아가고 있습니다.
HFT / 초저지연 시스템
마이크로초 단위로 경쟁하는 고빈도 트레이딩에서는 GC의 stop-the-world 한 번이 곧 거래 기회의 상실입니다. 전통적으로 C++가 독점해 온 영역인데, Rust는 동일한 성능 모델(GC 없음, 메모리 직접 제어, zero-cost)을 유지하면서 메모리 오류의 컴파일 타임 차단을 더합니다.
마무리
처음의 질문으로 돌아가서, Rust는 왜 사랑받을까요? C·C++ 사용이 필수인 performance-critical 환경에서, 같은 속도를 유지하면서 두 언어의 고질적인 메모리 오류를 컴파일 타임에 막아주기 때문이지 않을까 싶습니다. C에서 개발자가 하던 일과 Java·JS에서 런타임이 하던 일을 컴파일러가 대신합니다. 메모리 해제는 컴파일러가 계산해서 넣고, null과 예외는 리턴 타입이 처리를 강제합니다.
실무에서는 전면 재작성이 아니라 성능과 신뢰성이 병목인 지점에만 투입하는 방식이 자리 잡았습니다. 앞으로 서버를 개발하면서 병목을 만난다면, Rust로 재작성하는 선택지를 고려해 볼 수 있을 것 같습니다.
참고 자료
- Stack Overflow Developer Survey 2025 — Technology
- LWN — The (successful) end of the kernel Rust experiment
- The White House ONCD — Back to the Building Blocks: A Path Toward Secure and Measurable Software (2024)
- NSA — Software Memory Safety (2022)
- Discord — Why Discord is switching from Go to Rust
- Cloudflare — How we built Pingora, the proxy that connects Cloudflare to the Internet
- The Rust Programming Language — Fearless Concurrency
- Tokio 공식 문서 · sqlx 매크로 문서
