LabHub

블로그

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

왜 2025년에 Rust를 배워야 하는가

2015년 1.0 출시 때만 해도 "실험적 언어"였다. 2020년경부터 변화:

2024년 Stack Overflow 개발자 설문에서 9년 연속 "가장 사랑받는 언어".

그런데 왜 학습 곡선이 악명 높은 Rust가 이기는가? 답: 메모리 안전성 + 성능 + 현대적 도구를 동시에 제공하는 유일한 선택지라서.

Part 1 — 소유권(Ownership) — 모든 것의 중심

문제: 메모리 안전성 vs 성능

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

이 구분이 "처음 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

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

대부분의 타입이 자동으로 이 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(); }

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();
}

특징:

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

주요 도구

workspaces와 대규모 프로젝트

[workspace]
members = ["crates/*"]
resolver = "2"

모노레포 친화. 공통 의존성·캐시 공유.

크레이트 생태계 — 2025년 기준

Part 9 — Unsafe Rust — "탈출구"

Unsafe는 왜 필요한가

Unsafe로 가능한 것

  1. Raw pointer 역참조.
  2. unsafe 함수 호출.
  3. union 필드 접근.
  4. 가변 static 접근.
  5. unsafe trait 구현.

좋은 unsafe 코드의 규칙

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과 프레임워크

언제 임베디드 Rust가 빛나나

Part 11 — Rust in Linux Kernel

2022년 10월 — 역사적 순간

Linus Torvalds가 Linux 6.1에 Rust 지원 머지. 역사상 두 번째 언어(C 이후).

왜 커널에 Rust?

현재 상태 (2025)

논쟁

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% 개선 보고됨.

주의 포인트

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

최고의 조합

2024-2025 주요 예

Component Model

이전 WASM 글에서 다룬 개념. Rust가 가장 빠르게 지원.

Part 14 — 언제 Rust를 쓰지 말아야 하나

다른 언어가 나은 경우:

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

Rust가 빛나는 곳:

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 튜토리얼.

커뮤니티

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개월을 "왜 이게 안 되지?"로 보낸다. 그러나 그 관문을 넘으면:

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 내부를 파헤친다.

"내 코드가 실제 실리콘 위에서 어떻게 실행되는가?" 다음 글에서.

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다