Skip to content

✍️ 필사 모드: Rust 완전 정복 — 소유권, 빌림 검사기, Lifetime, Async/Tokio, 임베디드, Linux Kernel, Cargo, Unsafe 완벽 가이드 (2025)

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

왜 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가지 규칙

  1. 각 값은 정확히 하나의 소유자를 가진다.
  2. 한 시점에 값의 소유자는 하나.
  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()
}

빌림 규칙 (이게 핵심)

  1. 불변 참조는 여러 개 가능.
  2. 가변 참조는 동시에 하나.
  3. 불변과 가변이 동시에 존재할 수 없다.

이 규칙이 데이터 레이스를 컴파일 타임에 제거한다.

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 fnFuture를 반환한다. 호출만으로는 실행되지 않는다 — 런타임이 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 safetyselect! 사용 시 각 가지가 취소 안전해야.
  • runtime 혼재 금지tokio::fsasync-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로 가능한 것

  1. Raw pointer 역참조.
  2. unsafe 함수 호출.
  3. union 필드 접근.
  4. 가변 static 접근.
  5. 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]> vs Vec<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를 쓰지 말아야 하나

다른 언어가 나은 경우:

  1. 빠른 프로토타이핑 — Python/TypeScript가 훨씬 빠르다.
  2. GC로 충분한 서비스 — Go가 운영 간단.
  3. 팀이 초보자로만 — 학습 곡선이 스케줄을 먹는다.
  4. 단순 CRUD 앱 — 언어 장점 살릴 부분이 적다.
  5. 프론트엔드 — TypeScript가 여전히 정답 (Rust→WASM은 특수 케이스).

Rust가 빛나는 곳:

  • 장기 운영 서비스 (메모리 릭 치명적).
  • 지연 민감 (GC pause 못 견디는).
  • 임베디드/시스템.
  • 보안 필수.
  • 성능 경쟁.

Part 15 — 학습 경로

순서

  1. The Rust Programming Language ("The Book") — 공식, 무료. 전체 읽기.
  2. Rust By Example — 짧은 예제로 확인.
  3. Rustlings — 100+ 작은 연습 문제.
  4. Programming Rust (O'Reilly) — 더 깊이.
  5. 도메인별:
    • 웹: axum 공식 예제.
    • CLI: clap 튜토리얼.
    • 임베디드: The Embedded Rust Book.
    • 게임: Bevy 튜토리얼.

커뮤니티

  • r/rust — 활발한 서브레딧.
  • This Week in Rust — 주간 뉴스레터.
  • Rust Users Forum — 질문 답변.
  • Discord — 실시간 도움.

Part 16 — 12개 체크리스트

  1. ownership·빌림 규칙 먼저 체화 — 나머지는 그 위에 쌓인다.
  2. clippy를 항상 실행 — 입문자는 특히.
  3. 에러는 anyhow + thiserror 조합 — 90% 경우 충분.
  4. async는 Tokio 선택 — 특수한 이유 없으면.
  5. Arc<Mutex<>>보다 채널 선호 — 공유 메모리는 복잡.
  6. unsafe는 최소 — 필요하면 SAFETY 주석.
  7. cargo audit + cargo deny — 의존성 관리.
  8. lifetime 에러는 경고가 아니라 힌트 — 정확한 설계가 필요.
  9. release 프로파일에 LTO — 성능 10%+ 쉽게 얻음.
  10. 테스트는 #[cfg(test)] 모듈 + integration tests.
  11. 트레이싱은 tracing + tokio-console — 프로덕션 디버깅.
  12. 크레이트 선택은 다운로드 수·유지보수·라이선스 확인.

Part 17 — 10대 안티패턴

  1. .unwrap() 남용 — 프로덕션에서 panic 폭탄.
  2. Arc<Mutex<>>로 모든 걸 공유 — 성능·데드락 위험.
  3. 모든 함수에 제네릭 — 컴파일 시간 폭발.
  4. 불필요한 .clone() — 힙 할당 남발.
  5. lifetime 명시로 회피 — 실제론 설계 문제.
  6. String&str을 혼용 불명확 — API 품질 저하.
  7. async 함수에서 블로킹 I/O — 런타임 교착.
  8. std::sync::Mutex를 async 코드에tokio::sync::Mutex 써야.
  9. unsafe로 빠른 해결 시도 — UB의 입구.
  10. 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년경부터 변화:

작성 글자: 0원문 글자: 10,724작성 단락: 0/338