Skip to content

✍️ 필사 모드: Rust 완전 가이드 — Ownership·Lifetime·async·Tokio·Axum·실전 프로젝트까지 (Season 2 Ep 2, 2025)

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

들어가며 — 왜 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크기 비교
HashHashMap 키
Default기본값
From/Into타입 변환
TryFrom/TryInto실패 가능한 변환
Iteratorfor loop 가능
IntoIteratorfor 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 fnFuture를 반환한다. 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년 선택 기준

프레임워크특징적합
AxumTokio 팀 제작, tower 미들웨어 생태계신규 프로젝트 기본값
Actix-Web오래된 성숙도, 성능 최상위레거시·극한 퍼포먼스
Rocket매크로 중심, 편리교육·프로토타입
PoemOpenAPI 우선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)
DBsqlx (컴파일 타임 쿼리 검증), diesel, sea-orm
직렬화serde, serde_json, bincode
에러anyhow, thiserror
로깅tracing, tracing-subscriber
테스트proptest (속성 기반), mockall
CLIclap, 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년 동안 "가능성 있는 언어"에서 "시스템 프로그래밍의 현대적 기본값"으로 이동했다.

작성 글자: 0원문 글자: 11,467작성 단락: 0/353