들어가며 — 왜 2025년에도 Rust인가
Rust는 2015년 1.0 이후 10년 동안 "가능성 있는 언어"에서 "시스템 프로그래밍의 현대적 기본값"으로 이동했다.
**2024~2025 Rust의 현재 위치:**
- **Linux Kernel**: Rust for Linux 프로젝트가 커널 드라이버 작성의 공식 선택지 (2024~)
- **Windows**: MS가 Windows 커널 일부를 Rust로 재작성 (2023~)
- **Chromium**: Rust 컴포넌트 점진 도입 (QR 코드 생성기 등)
- **Cloudflare Pingora**: Rust 기반 차세대 프록시, NGINX 대체
- **Discord**: Read States 서비스 Go→Rust 전환으로 레이턴시 70% 감소
- **AWS Firecracker**: Rust로 작성된 MicroVM, Lambda·Fargate의 심장
- **Polkadot·Solana**: 블록체인 노드 기본 언어
- **Stack Overflow Developer Survey 2024**: 9년 연속 "가장 사랑받는 언어"
Rust는 더 이상 "배우기 어려워서 미루는 언어"가 아니라, 시니어 엔지니어가 읽어야 할 코드 베이스가 매년 늘어나는 언어다.
이 글은 Rust를 처음 시작하는 사람이 Ownership을 "마침내" 이해하고, 중급자가 Lifetime을 "마침내" 설계하고, 고급자가 async를 "마침내" 뚫을 수 있도록 썼다.
1부 — Rust의 언어적 야망: 3대 목표
1.1 메모리 안전성 (Memory Safety)
C/C++의 30년 악몽 — Use-After-Free, Double-Free, Data Race — 을 **컴파일 타임에 불가능하게 만드는 것**.
MS 보안팀 분석 (2019): MS가 수정한 보안 취약점의 70%가 메모리 안전성 이슈.
Google Android 팀 분석 (2024): Rust 도입 후 신규 메모리 안전성 버그 52%→26%→7%로 급락.
1.2 Zero-cost Abstraction
추상화를 썼다고 런타임 비용이 들면 안 된다. C처럼 빠르되, Haskell처럼 표현력 있게.
// 이 이터레이터 체인은 컴파일 후
// 손으로 쓴 C 루프와 동일한 어셈블리로 컴파일됨
let sum: i32 = (1..=100)
.filter(|n| n % 2 == 0)
.map(|n| n * n)
.sum();
1.3 Fearless Concurrency
"동시성은 어렵다"를 "동시성은 타입 시스템이 검증한다"로 바꾸는 것. `Send`, `Sync` trait가 컴파일러의 감시관.
2부 — Ownership: Rust의 첫 번째 관문
2.1 세 가지 규칙
1. 모든 값은 **단 하나의 소유자(Owner)**를 갖는다
2. 소유자가 Scope를 벗어나면 값은 **Drop**된다
3. 소유권은 **이동(Move)**할 수 있다
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1의 소유권이 s2로 이동
// println!("{}", s1); // 컴파일 에러! s1은 더 이상 유효하지 않음
println!("{}", s2); // OK
}
2.2 Copy vs Move
스택에 저장되는 Primitive 타입 (i32, bool, char 등)은 **Copy trait**를 구현하여 이동이 아닌 복사가 일어난다.
let x = 5;
let y = x;
println!("{}, {}", x, y); // OK — x는 여전히 유효
Heap 할당이 포함된 String, Vec, Box는 기본적으로 Move된다.
2.3 왜 Ownership인가 — 역사적 맥락
GC 없이 안전하려면 "이 메모리를 누가 해제할 것인가"를 명확히 해야 한다.
- **C**: 프로그래머가 전적으로 책임 → 실수하면 Use-After-Free
- **C++ RAII**: 객체 수명에 묶어둠 → 이동 시멘틱 복잡
- **Rust Ownership**: 컴파일러가 규칙을 **강제** → 실수 자체를 불가능하게
2.4 가장 흔한 초보자 실수 5가지
1. **함수에 소유권을 넘기고 이후 사용**
let s = String::from("hi");
print_it(s);
println!("{}", s); // 에러
2. **for loop에서 Vec 소비**
let v = vec![1, 2, 3];
for x in v { /* v 소비됨 */ }
// v 더 이상 사용 불가 → for &x in &v 사용
3. **Clone 남발**: 해결은 되지만 성능 저하
4. **`&mut` 두 개 동시 사용 시도**: Borrow Checker에 막힘
5. **Struct 필드만 이동하려 시도**: Partial Move의 복잡성
3부 — Borrowing: 빌려주고 빌려받기
3.1 두 가지 규칙 (Borrow Checker의 심장)
1. 한 시점에 **`&mut` 참조는 단 하나**만 존재할 수 있다
2. 또는 **여러 개의 `&` (불변 참조)**를 가질 수 있다. 둘을 섞을 수는 없다.
이것이 **데이터 경쟁을 컴파일 타임에 제거**하는 핵심이다.
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
// let r3 = &mut s; // 에러! 불변 참조가 살아있는 동안 가변 참조 불가
println!("{}, {}", r1, r2);
// 여기서 r1, r2 마지막 사용 → 이후 r3 가능
let r3 = &mut s;
r3.push_str(" world");
3.2 Non-Lexical Lifetimes (NLL, 2018 Edition)
Rust 2018부터 참조의 유효 범위가 "마지막 사용 시점"까지로 축소. 훨씬 유연해짐.
3.3 Slice — 참조의 응용
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
`&str`은 String의 부분 참조. 매우 흔한 패턴.
4부 — Lifetime: Rust의 두 번째 관문
4.1 Lifetime이란
**참조가 유효한 범위**를 컴파일러가 추적하는 시스템. 대부분 생략 가능하지만, 모호할 때 명시해야 함.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
`'a`는 "x와 y 중 짧은 쪽의 수명"을 의미. 반환 참조가 이 수명을 벗어나지 않도록 보장.
4.2 Lifetime Elision Rules (생략 규칙 3가지)
1. 입력 참조마다 각자의 Lifetime이 주어짐
2. 입력 Lifetime이 하나면 모든 출력 Lifetime은 그것과 같음
3. `&self`가 있으면 출력 Lifetime은 `&self`와 같음
이 규칙 덕에 90% 이상의 함수는 Lifetime을 명시할 필요 없음.
4.3 Struct의 Lifetime
struct Important<'a> {
part: &'a str,
}
impl<'a> Important<'a> {
fn announce(&self, announcement: &str) -> &str {
println!("Attention! {}", announcement);
self.part
}
}
Struct가 참조를 필드로 가질 때 Lifetime 필수.
4.4 `'static` — 프로그램 전체 수명
let s: &'static str = "hello"; // 문자열 리터럴
문자열 리터럴은 바이너리에 박혀 있어 프로그램 전체 생명주기.
4.5 Lifetime이 어려운 이유 3가지
1. **Invariant vs Covariant**: `&'a mut T`는 T에 대해 Invariant — 초심자는 알 필요 없음
2. **HRTB (Higher-Ranked Trait Bounds)**: `for<'a> Fn(&'a T)` — 클로저에서 가끔 등장
3. **Self-referential struct**: 불가능에 가까움. 필요하면 `Pin` 또는 외부 crate
5부 — Trait & Generic: Zero-cost Abstraction
5.1 Trait — Rust의 인터페이스
trait Area {
fn area(&self) -> f64;
}
struct Circle { r: f64 }
struct Square { s: f64 }
impl Area for Circle {
fn area(&self) -> f64 { std::f64::consts::PI * self.r * self.r }
}
impl Area for Square {
fn area(&self) -> f64 { self.s * self.s }
}
5.2 Trait Bound vs impl Trait vs dyn Trait
// 1. Trait Bound — 컴파일 타임 단형화(Monomorphization)
fn print_area<T: Area>(shape: &T) {
println!("{}", shape.area());
}
// 2. impl Trait — 위와 동일, 문법 설탕
fn print_area_impl(shape: &impl Area) { /* ... */ }
// 3. dyn Trait — 런타임 동적 디스패치 (vtable 조회)
fn print_area_dyn(shape: &dyn Area) { /* ... */ }
**선택 기준:**
- 대부분은 Generic (1, 2) — 빠르지만 바이너리 크기 증가
- Vec에 여러 타입 섞어 넣어야 하면 `Box<dyn Trait>` (3)
- 라이브러리 API는 impl Trait 선호
5.3 주요 Trait 15개 (외워두면 평생 써먹는)
| Trait | 용도 |
|-------|-----|
| `Clone` | 명시적 복사 |
| `Copy` | 암시적 복사 (Primitive) |
| `Debug` | `{:?}` 출력 |
| `Display` | `{}` 출력 |
| `PartialEq`/`Eq` | 동등 비교 |
| `PartialOrd`/`Ord` | 크기 비교 |
| `Hash` | HashMap 키 |
| `Default` | 기본값 |
| `From`/`Into` | 타입 변환 |
| `TryFrom`/`TryInto` | 실패 가능한 변환 |
| `Iterator` | for loop 가능 |
| `IntoIterator` | for loop 가능 (소비) |
| `Drop` | 소멸자 |
| `Send`/`Sync` | 스레드 안전성 |
| `AsRef`/`AsMut` | 저비용 참조 변환 |
5.4 Derive Macro — 반복 제거
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct User {
id: u64,
name: String,
}
6부 — Error Handling: Result와 `?`
6.1 Panic vs Result
- **`panic!`**: 복구 불가능한 에러. 프로그램 종료.
- **`Result<T, E>`**: 복구 가능한 에러. 호출자가 처리.
use std::fs::File;
use std::io::{self, Read};
fn read_username() -> Result<String, io::Error> {
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
}
`?` 연산자: Err면 즉시 반환, Ok면 값 추출. 에러 전파의 황금 패턴.
6.2 anyhow vs thiserror (2025 표준)
- **Application 코드**: `anyhow::Result<T>` — 단순, 편리
- **Library 코드**: `thiserror::Error` derive — 타입 명시, API 계약
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("데이터베이스 오류")]
Database(#[from] sqlx::Error),
#[error("사용자를 찾을 수 없음: id={0}")]
NotFound(u64),
#[error("인증 실패")]
Unauthorized,
}
7부 — async/await와 Tokio
7.1 async가 어려운 이유 — Future 모델
Rust의 `async fn`은 **Future를 반환**한다. Future는 **Poll될 때까지 아무것도 하지 않는다**.
async fn fetch_data() -> String {
// 호출 시점에는 실행되지 않음
// .await 하거나 executor에 spawn되어야 진행
"data".to_string()
}
let fut = fetch_data(); // Future 생성만 됨
let data = fut.await; // 실제 실행
**"async fn은 함수가 아니라 상태 기계 생성기"** — 컴파일러가 내부적으로 state machine으로 변환.
7.2 Tokio — 사실상 표준 런타임
[dependencies]
tokio = { version = "1.40", features = ["full"] }
#[tokio::main]
async fn main() {
let handle = tokio::spawn(async {
println!("task running");
});
handle.await.unwrap();
}
7.3 `async`의 3가지 함정
1. **블로킹 코드 혼합**: `std::fs::read`를 async 함수 안에서 쓰면 스레드 전체가 멈춤 → `tokio::fs::read` 또는 `spawn_blocking` 사용
2. **`Mutex` 혼용**: `std::sync::Mutex`는 await 포인트를 넘나들면 데드락 위험 → `tokio::sync::Mutex` 사용
3. **Send 오류**: `spawn`된 Future는 Send여야 함. Rc 같은 것 사용 시 컴파일 에러
7.4 Tokio Runtime 2가지
- **Multi-threaded** (기본, `#[tokio::main]`): 여러 Worker 스레드
- **Current-thread** (`#[tokio::main(flavor = "current_thread")]`): 단일 스레드, 테스트·임베디드에 적합
7.5 Channel — 비동기 메시지 전달
use tokio::sync::mpsc;
let (tx, mut rx) = mpsc::channel(32);
tokio::spawn(async move {
tx.send("hello").await.unwrap();
});
while let Some(msg) = rx.recv().await {
println!("{}", msg);
}
- `mpsc`: 다중 생산자, 단일 소비자
- `broadcast`: 다중 생산자, 다중 소비자 (Pub/Sub)
- `oneshot`: 일회성 (요청-응답)
- `watch`: 최신값만 유지 (설정 변경 등)
8부 — Web Framework: Axum vs Actix-Web
8.1 2025년 선택 기준
| 프레임워크 | 특징 | 적합 |
|-----------|-----|-----|
| **Axum** | Tokio 팀 제작, tower 미들웨어 생태계 | 신규 프로젝트 기본값 |
| **Actix-Web** | 오래된 성숙도, 성능 최상위 | 레거시·극한 퍼포먼스 |
| **Rocket** | 매크로 중심, 편리 | 교육·프로토타입 |
| **Poem** | OpenAPI 우선 | API 퍼스트 |
**추천 (2025)**: 새로 시작한다면 **Axum**. Tokio 생태계와 완벽 통합.
8.2 Axum 실전 예제
use axum::{
routing::{get, post},
Router, Json,
extract::{State, Path},
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Clone)]
struct AppState {
users: Arc<RwLock<Vec<User>>>,
}
#[derive(Clone, Serialize, Deserialize)]
struct User {
id: u64,
name: String,
}
async fn get_users(State(state): State<AppState>) -> Json<Vec<User>> {
let users = state.users.read().await;
Json(users.clone())
}
async fn create_user(
State(state): State<AppState>,
Json(user): Json<User>,
) -> Json<User> {
state.users.write().await.push(user.clone());
Json(user)
}
#[tokio::main]
async fn main() {
let state = AppState {
users: Arc::new(RwLock::new(vec![])),
};
let app = Router::new()
.route("/users", get(get_users).post(create_user))
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
8.3 Tower 미들웨어 — 표준 재사용
Axum은 `tower` 미들웨어 생태계를 공유한다:
- `tower-http::trace::TraceLayer` — 로깅
- `tower-http::cors::CorsLayer` — CORS
- `tower_governor::GovernorLayer` — Rate Limit
- `axum-extra::extract::cookie` — 쿠키
9부 — 실전 프로젝트 5가지 (레벨별 로드맵)
9.1 Lv.1 — CLI 도구 (1주)
- **목표**: Ownership·Error 처리 체득
- **예시**: `wc`, `grep`, `todo.txt` 매니저
- **주요 crate**: `clap` (CLI 파싱), `anyhow` (에러)
9.2 Lv.2 — WebSocket 채팅 서버 (2주)
- **목표**: async·Channel·공유 상태
- **예시**: Axum + `tokio-tungstenite` 채팅방
- **주요 crate**: `axum`, `tokio`, `futures`
9.3 Lv.3 — 병렬 웹 크롤러 (2주)
- **목표**: 동시성 제어·Rate Limit
- **예시**: Sitemap 크롤러, 링크 그래프 생성
- **주요 crate**: `reqwest`, `scraper`, `tokio::sync::Semaphore`
9.4 Lv.4 — 미니 Redis (3주)
- **목표**: 네트워크 프로토콜·상태 관리
- **예시**: RESP 프로토콜 구현, SET/GET/EXPIRE
- **주요 crate**: `tokio`, `bytes`
9.5 Lv.5 — 분산 KV Store (4~8주)
- **목표**: Raft·Replication
- **예시**: 3노드 KV Store, Log-structured storage
- **주요 crate**: `openraft`, `sled` 또는 자작 B+tree
이 5개를 다 하면 Rust 엔지니어로 면접 통과 가능.
10부 — Rust 생태계 2024~2025 현황
10.1 필수 crate 20개
| 카테고리 | Crate |
|---------|-------|
| **비동기** | `tokio`, `futures`, `async-trait` |
| **웹** | `axum`, `reqwest`, `hyper`, `tonic` (gRPC) |
| **DB** | `sqlx` (컴파일 타임 쿼리 검증), `diesel`, `sea-orm` |
| **직렬화** | `serde`, `serde_json`, `bincode` |
| **에러** | `anyhow`, `thiserror` |
| **로깅** | `tracing`, `tracing-subscriber` |
| **테스트** | `proptest` (속성 기반), `mockall` |
| **CLI** | `clap`, `indicatif` (프로그레스 바) |
| **날짜** | `chrono`, `time` |
| **암호** | `ring`, `rustls` |
10.2 2024~2025 생태계 뉴스
- **Rust 2024 Edition** (2024년 11월): `let-else`, `async closures` 안정화, `gen` 블록 실험
- **Axum 0.8** (2024): `impl Handler` 개선
- **sqlx 0.8**: 컴파일 타임 쿼리 검증 개선
- **tokio 1.40**: `spawn_blocking` 성능 향상
- **Cargo workspace lints**: 단일 설정으로 전체 워크스페이스 lint
10.3 학습 자료 Best
1. **The Rust Programming Language** (공식, 무료) — "The Book"
2. **Rust By Example** (공식, 무료)
3. **Rustlings** (공식 연습문제)
4. **Jon Gjengset YouTube** — 현존 최고의 Rust 교육자
5. **Zero to Production in Rust** (Luca Palmieri) — 실전 서버 개발
6. **Rust for Rustaceans** (Jon Gjengset) — 중급 이상 필독
7. **Programming Rust 2nd Ed** (O'Reilly) — 참조서
8. **tokio Tutorial** — async 최고의 문서
9. **Crust of Rust** 시리즈 — Jon Gjengset 유튜브
10. **This Week in Rust** — 매주 뉴스레터
11부 — Rust를 쓰지 말아야 할 때
Rust는 만능이 아니다. 쓰지 말아야 할 신호:
1. **프로토타이핑 속도가 생명**: Python, TypeScript가 더 빠르다
2. **팀에 Rust 경험자 전무**: 학습 곡선 3~6개월 각오 필요
3. **비즈니스 로직 위주 CRUD**: Go가 더 생산적일 수 있음
4. **GPU/SIMD 극한 퍼포먼스**: C++이 여전히 더 성숙
5. **모바일 앱 UI**: Swift/Kotlin이 표준
**Rust가 진가를 발휘할 때:**
- 높은 동시성 + 낮은 레이턴시 서버 (Discord, Cloudflare)
- 시스템 프로그래밍 (커널, 드라이버, DB)
- 임베디드 + IoT
- CLI 도구 (단일 바이너리, 빠름)
- WebAssembly 대상
- 금융/암호화처럼 "틀리면 안 됨"
12부 — 시니어로 가는 Rust 로드맵 (12개월)
Month 1~2: Ownership·Borrowing 체화
- The Book 1~10장
- Rustlings 완주
- CLI 프로젝트 1개
Month 3~4: Trait·Generic·Error
- The Book 11~17장
- Rust By Example 심화
- 미니 라이브러리 1개 (crates.io 등록까지)
Month 5~6: async·Tokio
- Tokio Tutorial
- Jon Gjengset "Crust of Rust: Channels"
- WebSocket 서버 프로젝트
Month 7~8: 실전 서버
- Zero to Production in Rust
- Axum + sqlx + PostgreSQL 서비스
- 관측성 (tracing, metrics)
Month 9~10: 고급 주제
- `unsafe` 정확히 사용하기
- FFI (C 라이브러리 바인딩)
- 매크로 (declarative + procedural)
Month 11~12: 기여·분야 선택
- 오픈소스 crate에 PR 3개 이상
- 분야 선택: 웹 백엔드 / 블록체인 / 임베디드 / OS / 게임
13부 — Rust 체크리스트 12
1. **소유권 규칙 3개**를 한 문장으로 설명할 수 있다
2. **Borrow 규칙**을 예시와 함께 말할 수 있다 (1 mut or N immut)
3. **Lifetime `'a`** 없이 컴파일 안 되는 함수 예시를 쓸 수 있다
4. **Clone vs Copy vs Move** 차이를 안다
5. **`Box`, `Rc`, `Arc`** 차이를 안다
6. **`RefCell`, `Mutex`, `RwLock`** 차이를 안다
7. **`?` 연산자**의 정확한 동작을 설명할 수 있다
8. **async fn이 반환하는 Future**의 의미를 안다
9. **Send vs Sync** 차이를 안다
10. **Trait Bound vs dyn Trait** 중 언제 어느 것을 쓸지 판단할 수 있다
11. **sqlx compile-time 쿼리 검증**의 장점을 안다
12. **Rust 2024 Edition** 주요 변경점 3개를 안다
14부 — Rust 안티패턴 10
1. **`unwrap()` 남발**: 프로덕션에서 패닉. `?`, `expect()`, 또는 매치로 처리
2. **`clone()` 남발**: 성능 저하. 참조로 해결 가능한지 먼저 고민
3. **`Rc<RefCell<T>>`에 의존**: 설계가 잘못됐을 가능성
4. **`async` 안에서 `std::sync::Mutex`**: 데드락 위험. `tokio::sync::Mutex`
5. **`async fn` 안에서 블로킹 호출**: 런타임 스타베이션. `spawn_blocking`
6. **거대한 `match` 블록**: Trait 다형성으로 리팩터 가능
7. **`String`만 사용**: `&str`로 받을 수 있으면 받자
8. **`unsafe` 블록 난발**: 정말로 필요한지 재검토. 대부분은 안전한 대안 존재
9. **Error를 `String`으로 반환**: `thiserror` 써서 타입화
10. **의미 없는 Generic**: "혹시 몰라서"의 Generic은 복잡도만 증가
마치며 — Rust의 학습 곡선은 평생의 자산이다
Rust를 배우는 것은 단지 언어 하나를 배우는 것이 아니다. **메모리·동시성·타입 시스템을 처음부터 다시 생각하는 것**이다.
이 과정에서 얻는 것:
- C/C++ 레거시를 읽는 능력
- Go·Java에서도 동시성 버그를 미리 감지하는 직관
- 타입으로 설계하는 사고방식
- "틀리면 컴파일 안 됨"의 철학
Rust는 도구이지 종교가 아니다. 하지만 도구로서 극한의 완성도를 가졌다.
2025년, 시니어 엔지니어라면 **Rust 코드를 읽을 수 있는 것은 기본, 쓸 수 있는 것은 경쟁력**이다.
다음 글 예고 — "Go 완전 가이드: Goroutine·Channel·Context·실전 마이크로서비스까지"
Season 2 Ep 3은 Rust의 대척점이자 동반자, **Go**. 다음 글은:
- Go의 철학: "Less is more"의 진짜 의미
- Goroutine과 Channel이 Rust와 어떻게 다른가
- Context·errgroup·sync 패키지 완전 이해
- 2024~2025 Go 생태계 (Gin, Echo, Chi, Fiber)
- gRPC·마이크로서비스 실전
- Rust와 Go 중 무엇을 선택할까
"배우기 쉽고 쓰기 어려운" Go의 진짜 얼굴, 다음 글에서 이어진다.
현재 단락 (1/353)
Rust는 2015년 1.0 이후 10년 동안 "가능성 있는 언어"에서 "시스템 프로그래밍의 현대적 기본값"으로 이동했다.