- Authors

- Name
- Youngju Kim
- @fjvbn20031
들어가며 — 왜 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 세 가지 규칙
- 모든 값은 **단 하나의 소유자(Owner)**를 갖는다
- 소유자가 Scope를 벗어나면 값은 Drop된다
- 소유권은 **이동(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가지
- 함수에 소유권을 넘기고 이후 사용
let s = String::from("hi"); print_it(s); println!("{}", s); // 에러 - for loop에서 Vec 소비
let v = vec![1, 2, 3]; for x in v { /* v 소비됨 */ } // v 더 이상 사용 불가 → for &x in &v 사용 - Clone 남발: 해결은 되지만 성능 저하
&mut두 개 동시 사용 시도: Borrow Checker에 막힘- Struct 필드만 이동하려 시도: Partial Move의 복잡성
3부 — Borrowing: 빌려주고 빌려받기
3.1 두 가지 규칙 (Borrow Checker의 심장)
- 한 시점에
&mut참조는 단 하나만 존재할 수 있다 - 또는 **여러 개의
&(불변 참조)**를 가질 수 있다. 둘을 섞을 수는 없다.
이것이 데이터 경쟁을 컴파일 타임에 제거하는 핵심이다.
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가지)
- 입력 참조마다 각자의 Lifetime이 주어짐
- 입력 Lifetime이 하나면 모든 출력 Lifetime은 그것과 같음
&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가지
- Invariant vs Covariant:
&'a mut T는 T에 대해 Invariant — 초심자는 알 필요 없음 - HRTB (Higher-Ranked Trait Bounds):
for<'a> Fn(&'a T)— 클로저에서 가끔 등장 - 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::Errorderive — 타입 명시, 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가지 함정
- 블로킹 코드 혼합:
std::fs::read를 async 함수 안에서 쓰면 스레드 전체가 멈춤 →tokio::fs::read또는spawn_blocking사용 Mutex혼용:std::sync::Mutex는 await 포인트를 넘나들면 데드락 위험 →tokio::sync::Mutex사용- 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— CORStower_governor::GovernorLayer— Rate Limitaxum-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
- The Rust Programming Language (공식, 무료) — "The Book"
- Rust By Example (공식, 무료)
- Rustlings (공식 연습문제)
- Jon Gjengset YouTube — 현존 최고의 Rust 교육자
- Zero to Production in Rust (Luca Palmieri) — 실전 서버 개발
- Rust for Rustaceans (Jon Gjengset) — 중급 이상 필독
- Programming Rust 2nd Ed (O'Reilly) — 참조서
- tokio Tutorial — async 최고의 문서
- Crust of Rust 시리즈 — Jon Gjengset 유튜브
- This Week in Rust — 매주 뉴스레터
11부 — Rust를 쓰지 말아야 할 때
Rust는 만능이 아니다. 쓰지 말아야 할 신호:
- 프로토타이핑 속도가 생명: Python, TypeScript가 더 빠르다
- 팀에 Rust 경험자 전무: 학습 곡선 3~6개월 각오 필요
- 비즈니스 로직 위주 CRUD: Go가 더 생산적일 수 있음
- GPU/SIMD 극한 퍼포먼스: C++이 여전히 더 성숙
- 모바일 앱 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
- 소유권 규칙 3개를 한 문장으로 설명할 수 있다
- Borrow 규칙을 예시와 함께 말할 수 있다 (1 mut or N immut)
- Lifetime
'a없이 컴파일 안 되는 함수 예시를 쓸 수 있다 - Clone vs Copy vs Move 차이를 안다
Box,Rc,Arc차이를 안다RefCell,Mutex,RwLock차이를 안다?연산자의 정확한 동작을 설명할 수 있다- async fn이 반환하는 Future의 의미를 안다
- Send vs Sync 차이를 안다
- Trait Bound vs dyn Trait 중 언제 어느 것을 쓸지 판단할 수 있다
- sqlx compile-time 쿼리 검증의 장점을 안다
- Rust 2024 Edition 주요 변경점 3개를 안다
14부 — Rust 안티패턴 10
unwrap()남발: 프로덕션에서 패닉.?,expect(), 또는 매치로 처리clone()남발: 성능 저하. 참조로 해결 가능한지 먼저 고민Rc<RefCell<T>>에 의존: 설계가 잘못됐을 가능성async안에서std::sync::Mutex: 데드락 위험.tokio::sync::Mutexasync fn안에서 블로킹 호출: 런타임 스타베이션.spawn_blocking- 거대한
match블록: Trait 다형성으로 리팩터 가능 String만 사용:&str로 받을 수 있으면 받자unsafe블록 난발: 정말로 필요한지 재검토. 대부분은 안전한 대안 존재- Error를
String으로 반환:thiserror써서 타입화 - 의미 없는 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의 진짜 얼굴, 다음 글에서 이어진다.