✍️ 필사 모드: Rust 완전 정복 — 소유권, 빌림 검사기, Lifetime, Async/Tokio, 임베디드, Linux Kernel, Cargo, Unsafe 완벽 가이드 (2025)
한국어왜 2025년에 Rust를 배워야 하는가
2015년 1.0 출시 때만 해도 "실험적 언어"였다. 2020년경부터 변화:
- Microsoft — 윈도우 Boot Manager·Hyper-V 일부를 Rust로 재작성. Azure CTO Mark Russinovich: "우리는 새 메모리 안전 않은 코드를 쓰지 말아야 한다."
- Google — Android 13의 새 코드 21%가 Rust. Chromium이 Rust 수용(2023).
- Linux 6.1 (2022) — 역사상 처음으로 Rust가 커널에 진입.
- Amazon — Firecracker(Lambda 기반), Bottlerocket OS, S3의 일부.
- Cloudflare — Pingora(2022, Nginx 대체), 하루 1조 요청 처리.
- Discord — Go에서 Rust로 전환해 GC pause 제거.
- Figma — 멀티플레이어 동기화 엔진.
- 1Password, Dropbox, Mozilla, Meta.
2024년 Stack Overflow 개발자 설문에서 9년 연속 "가장 사랑받는 언어".
그런데 왜 학습 곡선이 악명 높은 Rust가 이기는가? 답: 메모리 안전성 + 성능 + 현대적 도구를 동시에 제공하는 유일한 선택지라서.
Part 1 — 소유권(Ownership) — 모든 것의 중심
문제: 메모리 안전성 vs 성능
- C/C++: 빠르다. 메모리 버그 위험(use-after-free, double free, data race). Microsoft에 따르면 보안 취약점의 70%가 메모리 관련.
- Java/Go: GC로 안전. Pause time·메모리 오버헤드·예측 불가능성.
- Rust: 컴파일 타임에 메모리 안전 보장. GC 없음. C++에 근접한 성능.
3가지 규칙
- 각 값은 정확히 하나의 소유자를 가진다.
- 한 시점에 값의 소유자는 하나.
- 소유자가 스코프를 벗어나면 값은 drop 된다.
fn main() {
let s = String::from("hello"); // s가 소유자
takes_ownership(s); // 소유권 이전
// println!("{}", s); // 오류! s는 이미 이전됨
}
fn takes_ownership(s: String) {
println!("{}", s);
} // 여기서 s drop, 메모리 해제
Copy vs Move
- Stack 데이터(i32, bool, f64 등): Copy trait → 복사.
- Heap 데이터(String, Vec, Box): Move → 소유권 이전.
이 구분이 "처음 Rust는 왜 이래?"의 첫 관문.
Part 2 — 빌림(Borrowing)과 참조
소유권 이전 없이 쓰고 싶다면? → 빌림
fn main() {
let s = String::from("hello");
let len = calculate_length(&s); // 불변 참조
println!("{} ({})", s, len); // s 여전히 유효
}
fn calculate_length(s: &String) -> usize {
s.len()
}
빌림 규칙 (이게 핵심)
- 불변 참조는 여러 개 가능.
- 가변 참조는 동시에 하나.
- 불변과 가변이 동시에 존재할 수 없다.
이 규칙이 데이터 레이스를 컴파일 타임에 제거한다.
let mut s = String::from("hello");
let r1 = &s; // OK
let r2 = &s; // OK (불변 여러 개)
let r3 = &mut s; // 오류! 불변 참조가 살아있는 동안 가변 불가
Non-Lexical Lifetimes (NLL, 2018)
초기엔 규칙이 너무 엄격해 개발자 불편 많았음. NLL로 "참조가 실제로 안 쓰인 시점부터 유효하지 않다"고 판정 → 훨씬 자연스러운 코드.
Part 3 — Lifetime — 왜 이게 필요한가
매달린 참조(dangling reference) 방어
fn dangling() -> &String {
let s = String::from("hello");
&s // s는 여기서 drop되는데 참조를 반환? 오류!
}
컴파일러가 이를 감지해야 하는데, 함수 시그니처만 봐서는 알 수 없을 때 lifetime 파라미터가 필요.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
'a는 이름일 뿐. "반환값의 유효 기간은 x와 y 중 짧은 쪽과 같다"는 선언.
Lifetime Elision — 대부분 자동
대부분 간단한 경우는 컴파일러가 lifetime을 추론. 명시가 필요한 경우는 주로 복잡한 구조체나 반환값.
'static Lifetime
프로그램 전체 수명. 문자열 리터럴이 대표.
let s: &'static str = "I live forever";
Part 4 — 타입 시스템의 무기들
Enum + Pattern Matching
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
match msg {
Message::Quit => println!("Quit"),
Message::Move { x, y } => println!("Move to ({}, {})", x, y),
Message::Write(text) => println!("Write: {}", text),
Message::ChangeColor(r, g, b) => println!("Color: {},{},{}", r, g, b),
}
컴파일러가 모든 케이스 처리를 강제 — missing case는 오류.
Option<T> — null 없는 세상
let some_number = Some(5);
let no_number: Option<i32> = None;
match some_number {
Some(n) => println!("{}", n),
None => println!("Nothing"),
}
NullPointerException은 존재하지 않는다. "The billion-dollar mistake"라 불린 null을 타입으로 해결.
Result<T, E> — 예외 없는 에러
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 { Err(String::from("Division by zero")) }
else { Ok(a / b) }
}
// ? 연산자로 에러 전파
fn calculate() -> Result<f64, String> {
let x = divide(10.0, 2.0)?;
let y = divide(x, 1.0)?;
Ok(y)
}
Trait — 다형성
trait Summary {
fn summarize(&self) -> String;
fn default_greeting(&self) -> String {
String::from("Hello") // 기본 구현
}
}
struct Article { title: String }
impl Summary for Article {
fn summarize(&self) -> String {
format!("Article: {}", self.title)
}
}
Generic + Trait Bound
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest { largest = item; }
}
largest
}
Monomorphization으로 각 타입에 대해 별도 코드 생성 — zero-cost.
Part 5 — 에러 처리 철학
panic! vs Result
- panic!: 복구 불가능한 상황. 프로그램 종료.
- Result: 복구 가능한 에러. 호출자가 처리.
anyhow + thiserror
실무 패턴:
// 라이브러리: 구체적 에러 타입
#[derive(thiserror::Error, Debug)]
enum MyError {
#[error("Not found: {0}")]
NotFound(String),
#[error("IO error")]
Io(#[from] std::io::Error),
}
// 애플리케이션: 모든 에러를 anyhow::Error로
fn main() -> anyhow::Result<()> {
let data = read_config()?;
Ok(())
}
Box<dyn Error>
여러 에러 타입을 하나로 묶는 전통 방식. anyhow가 나온 뒤 라이브러리 제외하곤 덜 쓰임.
Part 6 — 동시성 — "Fearless Concurrency"
Send + Sync
- Send: 다른 스레드로 소유권 이전 가능.
- Sync: 여러 스레드에서 불변 참조 공유 가능.
대부분의 타입이 자동으로 이 trait을 구현. 비구현 타입(예: Rc)을 잘못 쓰면 컴파일 에러.
공유 메모리
use std::sync::{Arc, Mutex};
use std::thread;
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for h in handles { h.join().unwrap(); }
- Arc: Atomic Rc, 스레드 안전 참조 카운팅.
- Mutex: 배타적 접근.
Mutex를 잘못 쓰면 컴파일 오류가 나는 것이 Rust의 힘. C++에선 런타임에 데드락으로 터지는 버그가 여기선 안 컴파일 된다.
채널
use std::sync::mpsc;
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
tx.send("hello").unwrap();
});
println!("{}", rx.recv().unwrap());
Go의 채널과 비슷. std::sync::mpsc, crossbeam-channel, tokio::sync::mpsc 각각 상황에 맞게.
Part 7 — Async/Await와 Tokio
Future — Rust 고유의 지연 평가
async fn fetch_url(url: &str) -> Result<String, reqwest::Error> {
reqwest::get(url).await?.text().await
}
async fn은 Future를 반환한다. 호출만으로는 실행되지 않는다 — 런타임이 poll 해야 함.
왜 이 모델? Zero-cost — async 코드가 state machine으로 컴파일, heap 할당 없이.
Tokio — 사실상 표준 런타임
#[tokio::main]
async fn main() {
let handle = tokio::spawn(async {
println!("from spawned task");
});
handle.await.unwrap();
}
특징:
- work-stealing 스케줄러 (Go 비슷).
- io_uring 실험적 지원.
- tokio::select!, tokio::join! 매크로.
Async의 어려움
- Send bounds — async 함수가 스레드 경계 넘으면 trait bound 에러.
- pin/unpin — self-referential future의 기술적 문제.
- cancellation safety —
select!사용 시 각 가지가 취소 안전해야. - runtime 혼재 금지 —
tokio::fs를async-std런타임에서 쓰면 패닉.
다른 런타임
- async-std — 표준 라이브러리와 비슷한 API (주도 떨어짐).
- smol — 가벼운 런타임.
- glommio, monoio — io_uring 기반, thread-per-core.
- embassy — 임베디드 async.
2024 현실: 새 프로젝트는 거의 Tokio. embassy는 임베디드, glommio는 고성능 서버 니치.
Part 8 — Cargo와 생태계
Cargo.toml
[package]
name = "my-app"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
anyhow = "1"
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
주요 도구
- cargo fmt — rustfmt, 자동 포맷.
- cargo clippy — 린트 (수백 개 규칙).
- cargo test — 통합된 테스트.
- cargo bench — 벤치마크.
- cargo expand — 매크로 펼친 결과.
- cargo audit — 보안 감사.
- cargo deny — 라이선스·중복 검사.
- cargo nextest — 빠른 병렬 테스트 러너.
workspaces와 대규모 프로젝트
[workspace]
members = ["crates/*"]
resolver = "2"
모노레포 친화. 공통 의존성·캐시 공유.
크레이트 생태계 — 2025년 기준
- 직렬화: serde (사실상 표준), bincode, postcard (임베디드).
- 웹 서버: axum (2024 최강), actix-web, rocket, warp.
- 데이터베이스: sqlx (async, 컴파일 타임 검증), diesel (ORM), sea-orm.
- CLI: clap (최강), structopt (clap에 병합), argh.
- HTTP 클라이언트: reqwest, ureq (동기), hyper (저수준).
- TUI: ratatui (tui-rs 포크), crossterm.
- 로깅/트레이싱: tracing + tracing-subscriber (권장), log + env_logger.
- 에러: anyhow, thiserror, eyre.
- 테스트: proptest (property-based), mockall, insta (snapshot).
Part 9 — Unsafe Rust — "탈출구"
Unsafe는 왜 필요한가
- 하드웨어 접근(임베디드).
- FFI (C 라이브러리 호출).
- Raw pointer 조작.
- 상호 재귀 자료구조(연결 리스트).
- 컴파일러가 증명 못 하는 안전한 패턴.
Unsafe로 가능한 것
- Raw pointer 역참조.
- unsafe 함수 호출.
- union 필드 접근.
- 가변 static 접근.
- unsafe trait 구현.
좋은 unsafe 코드의 규칙
- 최소한으로, 그리고 안전 래퍼 안에.
// SAFETY:주석으로 왜 안전한지 명시.- 모듈 경계에서는 safe API만 노출.
- 테스트 + MIRI(undefined behavior 검출기)로 검증.
unsafe fn dangerous() { /* ... */ }
fn safe_wrapper() {
// SAFETY: 호출 전 x != null 확인, y가 x 뒤 4바이트에 할당됨
unsafe { dangerous(); }
}
Part 10 — 임베디드 Rust
#![no_std]
표준 라이브러리(운영체제 의존) 없이 작동하는 Rust. core만 사용.
#![no_std]
#![no_main]
use panic_halt as _;
use cortex_m_rt::entry;
#[entry]
fn main() -> ! {
loop {}
}
HAL과 프레임워크
- embedded-hal — 하드웨어 추상 표준.
- Embassy — async 임베디드.
- RTIC (Real-Time Interrupt-driven Concurrency) — 정적 검증 가능한 RTOS 대안.
- probe-rs — 디버깅/플래싱 도구.
언제 임베디드 Rust가 빛나나
- 안전 필수 시스템 — 의료기기, 자동차.
- Long-running 장비 — 메모리 릭이 치명적.
- 멀티태스킹 MCU — Embassy의 async가 단순화.
Part 11 — Rust in Linux Kernel
2022년 10월 — 역사적 순간
Linus Torvalds가 Linux 6.1에 Rust 지원 머지. 역사상 두 번째 언어(C 이후).
왜 커널에 Rust?
- C의 매년 같은 버그 반복 — use-after-free, double free, buffer overflow.
- 안전한 추상화 가능 — Ownership, Lifetime.
- 대안 없음 — Zig, D는 아직 충분히 성숙 X.
현재 상태 (2025)
- NVMe 드라이버 — Rust 구현 등장.
- Apple M1/M2 GPU 드라이버 (Asahi Linux) — Rust로 작성.
- 커널 서브시스템들이 점진적으로 Rust 바인딩 추가 중.
논쟁
Linus는 찬성, 일부 서브시스템 메인테이너는 반대. 2024년 BCachefs·Rust-for-Linux 둘러싼 갈등이 공개화. 사회적 변화가 기술적 변화만큼 어렵다는 것을 보여줌.
Part 12 — Rust 성능 튜닝
컴파일 최적화
[profile.release]
opt-level = 3 # 최대 최적화
lto = "fat" # Link Time Optimization
codegen-units = 1 # 단일 단위, 느리지만 최적
panic = "abort" # panic 시 unwind 안 함 → 바이너리 작음
strip = true # 심볼 제거
PGO (Profile-Guided Optimization)
cargo pgo instrument run # 프로파일 수집
cargo pgo optimize build # 최적화 빌드
핫 경로 10-30% 개선 보고됨.
주의 포인트
- Arc 남용 — Rc/Arc 잦은 복제는 원자적 연산 비용.
- 불필요한 clone() — "빠르게 통과시키려" 남발하면 힙 할당 폭주.
- String vs &str — 소유 필요 없으면
&str. Box<[T]>vsVec<T>— 크기 고정이면Box<[T]>가 공간 효율.
SIMD
#[cfg(target_arch = "x86_64")]
use std::arch::x86_64::*;
unsafe {
let a = _mm256_loadu_si256(...);
// ...
}
portable-simd (nightly): 이식 가능한 SIMD. 곧 stable 목표.
Part 13 — WebAssembly + Rust
최고의 조합
- Rust는 GC 없음 → WASM 바이너리 작다.
- 안전성 보장 → 엣지 환경에 신뢰.
- wasm-bindgen으로 JS 상호운용.
2024-2025 주요 예
- Figma의 멀티플레이어 엔진 — Rust → WASM.
- Linear — 동기화 엔진.
- Shopify Functions — 점주의 커스텀 로직.
- 1Password — 브라우저 확장 일부.
- SWC, Turbopack, Rolldown, Biome — 프론트엔드 도구.
Component Model
이전 WASM 글에서 다룬 개념. Rust가 가장 빠르게 지원.
Part 14 — 언제 Rust를 쓰지 말아야 하나
다른 언어가 나은 경우:
- 빠른 프로토타이핑 — Python/TypeScript가 훨씬 빠르다.
- GC로 충분한 서비스 — Go가 운영 간단.
- 팀이 초보자로만 — 학습 곡선이 스케줄을 먹는다.
- 단순 CRUD 앱 — 언어 장점 살릴 부분이 적다.
- 프론트엔드 — TypeScript가 여전히 정답 (Rust→WASM은 특수 케이스).
Rust가 빛나는 곳:
- 장기 운영 서비스 (메모리 릭 치명적).
- 지연 민감 (GC pause 못 견디는).
- 임베디드/시스템.
- 보안 필수.
- 성능 경쟁.
Part 15 — 학습 경로
순서
- The Rust Programming Language ("The Book") — 공식, 무료. 전체 읽기.
- Rust By Example — 짧은 예제로 확인.
- Rustlings — 100+ 작은 연습 문제.
- Programming Rust (O'Reilly) — 더 깊이.
- 도메인별:
- 웹: axum 공식 예제.
- CLI: clap 튜토리얼.
- 임베디드: The Embedded Rust Book.
- 게임: Bevy 튜토리얼.
커뮤니티
- r/rust — 활발한 서브레딧.
- This Week in Rust — 주간 뉴스레터.
- Rust Users Forum — 질문 답변.
- Discord — 실시간 도움.
Part 16 — 12개 체크리스트
- ownership·빌림 규칙 먼저 체화 — 나머지는 그 위에 쌓인다.
clippy를 항상 실행 — 입문자는 특히.- 에러는
anyhow + thiserror조합 — 90% 경우 충분. - async는 Tokio 선택 — 특수한 이유 없으면.
Arc<Mutex<>>보다 채널 선호 — 공유 메모리는 복잡.unsafe는 최소 — 필요하면 SAFETY 주석.cargo audit + cargo deny— 의존성 관리.- lifetime 에러는 경고가 아니라 힌트 — 정확한 설계가 필요.
- release 프로파일에 LTO — 성능 10%+ 쉽게 얻음.
- 테스트는
#[cfg(test)]모듈 + integration tests. - 트레이싱은
tracing+tokio-console— 프로덕션 디버깅. - 크레이트 선택은 다운로드 수·유지보수·라이선스 확인.
Part 17 — 10대 안티패턴
.unwrap()남용 — 프로덕션에서 panic 폭탄.Arc<Mutex<>>로 모든 걸 공유 — 성능·데드락 위험.- 모든 함수에 제네릭 — 컴파일 시간 폭발.
- 불필요한
.clone()— 힙 할당 남발. - lifetime 명시로 회피 — 실제론 설계 문제.
String과&str을 혼용 불명확 — API 품질 저하.- async 함수에서 블로킹 I/O — 런타임 교착.
std::sync::Mutex를 async 코드에 —tokio::sync::Mutex써야.- unsafe로 빠른 해결 시도 — UB의 입구.
- Rust를 C++처럼 작성 — idiomatic한 방식을 거부.
마치며 — Rust는 투자다
Rust 학습은 힘들다. 3-6개월을 "왜 이게 안 되지?"로 보낸다. 그러나 그 관문을 넘으면:
- 메모리 버그로 디버깅하지 않는 삶.
- C++에 근접한 성능.
- 아름다운 도구 생태계.
- 업계에서 높아지는 수요(2024년 Rust 개발자 평균 연봉 미국 $165K).
2025년, Rust를 모르는 시니어 엔지니어는 줄어들고 있다. 직접 쓰지 않더라도 코드베이스를 읽을 수 있어야 한다. 왜냐하면 당신이 쓰는 많은 도구(Biome, Turbopack, SWC, Polars, Ruff, uv, ripgrep, fd, bat, zoxide, lapce...)가 Rust로 쓰였기 때문.
Rust는 "한 번쯤은 넘어야 할 언어"다. 넘고 나면 이전과 다른 눈으로 모든 다른 언어를 보게 된다.
다음 글 예고 — "컴퓨터 아키텍처의 현대" — CPU 파이프라인, Out-of-Order 실행, 캐시 계층, 브랜치 예측, Meltdown/Spectre, Apple Silicon, GPU 아키텍처
Rust가 빠른 이유는 결국 CPU와 친해서. 다음 글은 CPU 내부를 파헤친다.
- CPU 파이프라인과 슈퍼스칼라 — 왜 한 사이클에 여러 명령
- Out-of-Order 실행 — CPU는 당신 코드를 재정렬한다
- 캐시 계층(L1/L2/L3)과 Cache Line — 왜 배열이 linked list를 이기는가
- 브랜치 예측 — 분기가 10배 느려지는 순간
- Meltdown, Spectre, Zenbleed — 2018년 이후 보안 지형 변화
- Apple Silicon 탐구 — M1이 인텔을 이긴 설계 비결
- ARM vs x86 vs RISC-V — 현재의 지형
- SIMD의 실전 — SSE/AVX/NEON
- GPU 아키텍처 — CUDA 코어/SM/Warp
- 메모리 대역폭의 법칙 — DRAM·HBM·CXL
"내 코드가 실제 실리콘 위에서 어떻게 실행되는가?" 다음 글에서.
현재 단락 (1/338)
2015년 1.0 출시 때만 해도 "실험적 언어"였다. 2020년경부터 변화: