Skip to content
Published on

Rust와 시스템 프로그래밍의 귀환 완전 정복 — Ownership, Borrowing, Lifetime, Trait, Async, Tokio, Cargo, WebAssembly (2025)

Authors

"In Rust, we don't trust the programmer. We trust the compiler — and the programmer, trained by the compiler, becomes trustworthy." — Niko Matsakis (Rust core team)

2024년 2월, 백악관이 C/C++에서 메모리 안전한 언어로의 전환을 공식 권장했다. 2025년 2월, Linux 커널이 Rust 드라이버를 메인 트리에 병합했다. 9년 연속 Stack Overflow에서 "가장 사랑받는 언어" 1위. 이건 단순한 팬덤이 아니라, 소프트웨어 산업 전체의 구조적 전환이 진행 중임을 말한다.

Rust는 "어렵다"는 평판에도 불구하고, 왜 이 정도로 영향력이 큰가? 이 글은 Rust의 탄생부터 2025년의 생태계 지도까지, "러스트를 이해한다"는 것이 무엇인지 답한다.


1. Rust의 탄생 — 모질라의 선물

Graydon Hoare의 개인 프로젝트 (2006)

  • 캐나다 엔지니어, 자기 아파트 엘리베이터가 자꾸 고장나는 걸 보며 시작
  • "소프트웨어가 안 죽는 언어"를 만들고 싶었음
  • 처음엔 OCaml 영향 짙은 실험 언어

모질라 공식 프로젝트 (2010)

  • 모질라가 Graydon을 고용, Rust에 투자
  • Servo — Rust로 쓴 브라우저 엔진 실험
  • 2015년 1.0 출시

Servo → Firefox Quantum (2017)

  • Servo의 CSS/렌더링 엔진(Stylo, WebRender)을 Firefox에 이식
  • 실전 투입 첫 대규모 성공
  • 성능 2배, 버그 감소 경험

재단 독립 (2021)

  • 모질라 재정 위기로 Rust 팀 해체
  • AWS, Google, Microsoft, Huawei, Meta 주도로 Rust Foundation 설립
  • 거버넌스 분산

2025 — 편재

  • Linux 커널 (Rust for Linux)
  • Windows 커널 (Microsoft 이식 중)
  • 브라우저(Firefox, Servo 부활), JS 도구(swc, esbuild 경쟁자 oxc, Turbopack)
  • 런타임(Deno), 데이터베이스(TiKV, Qdrant, SurrealDB)
  • 블록체인(Solana, Polkadot)

2. Ownership — GC 없는 메모리 안전

문제 정의

  • C/C++: 수동 관리 → use-after-free, double-free, memory leak
  • Java/Go: GC → 예측 불가 지연, 메모리 오버헤드
  • 두 문제를 모두 피하는 제3의 길?

Rust의 세 가지 규칙

  1. 각 값은 정확히 하나의 owner를 가진다
  2. 값은 owner가 scope를 벗어나면 해제(drop)된다
  3. owner가 바뀌면 이전 owner는 사용 불가

기본 예

fn main() {
    let s = String::from("hello");
    let t = s;  // s의 소유권이 t로 이동
    // println!("{}", s);  // 에러! s는 더 이상 유효하지 않음
    println!("{}", t);  // OK
}  // t가 scope를 벗어남 → drop

Copy vs Move

  • 스택 값 (i32, bool, char): Copy — 복사
  • 힙 할당 (String, Vec): Move — 이동
let x = 5;
let y = x;  // Copy
println!("{} {}", x, y);  // OK

let s1 = String::from("hi");
let s2 = s1;  // Move
// println!("{}", s1);  // 에러!

Clone — 명시적 복사

let s1 = String::from("hi");
let s2 = s1.clone();  // 힙까지 복사
println!("{} {}", s1, s2);  // OK

.clone()을 써야 비용이 들었음이 코드에서 보인다. 암묵적 성능 비용 없음.


3. Borrowing — 빌림의 예술

왜 필요한가

fn print_len(s: String) { println!("{}", s.len()) }

let s = String::from("hi");
print_len(s);  // 소유권 이동
// s는 여기서 사용 불가

불편하다. 그래서 reference(&) 도입.

Immutable Borrow

fn print_len(s: &String) { println!("{}", s.len()) }

let s = String::from("hi");
print_len(&s);  // 빌림
println!("{}", s);  // OK, 소유권 유지

Mutable Borrow

fn add_world(s: &mut String) {
    s.push_str(" world")
}

let mut s = String::from("hi");
add_world(&mut s);
println!("{}", s);  // "hi world"

핵심 규칙 — "공유 xor 가변"

어느 한 시점에:

  • N개의 immutable borrow OR
  • 1개의 mutable borrow
  • 둘 다는 안 됨
let mut s = String::from("hi");
let r1 = &s;
let r2 = &s;
let r3 = &mut s;  // 에러! 이미 immutable borrow 존재

왜 이 규칙인가

  • 데이터 레이스 불가능 — 공유되면 변경 안됨
  • 컴파일 타임에 증명 — 런타임 오버헤드 0
  • **Rust의 "fearless concurrency"**의 기반

4. Lifetime — 참조의 유효 기간

문제

fn dangling() -> &String {
    let s = String::from("hi");
    &s
}  // s가 drop되면 &s는 dangling!

C에서는 세그폴트, Rust에서는 컴파일 에러.

Lifetime 주석

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

'a는 "이 함수 안에서 공통된 lifetime". 리턴값은 x 또는 y 중 더 짧은 lifetime을 가짐.

Lifetime Elision — 생략 규칙

대부분의 경우 명시 안 해도 됨:

fn first_word(s: &str) -> &str { ... }
// 내부적으로: fn first_word<'a>(s: &'a str) -> &'a str

3가지 규칙:

  1. 각 참조 파라미터는 자기 lifetime 받음
  2. 파라미터가 하나면 리턴 lifetime = 그 파라미터
  3. &self가 있으면 리턴 lifetime = self의 lifetime

'static

let s: &'static str = "hello";  // 문자열 리터럴

프로그램 전체 기간 동안 유효. 종종 남용되는 개념.

Lifetime이 어려운 이유

  • 처음엔 "왜 이게 필요?" 느낌
  • 복잡한 상황에서 이해해야 함 (self-referential struct 등)
  • 해법: 처음부터 쓰지 말고, 컴파일러가 요구할 때 배우기

5. Trait System — Rust의 핵심 추상화

Trait = Interface + Mixin + Typeclass

trait Greet {
    fn hello(&self) -> String;
    fn goodbye(&self) -> String {  // 기본 구현
        String::from("bye")
    }
}

struct Kim { name: String }

impl Greet for Kim {
    fn hello(&self) -> String {
        format!("안녕, 나는 {}", self.name)
    }
}

Generic with Trait Bounds

fn greet_all<T: Greet>(people: &[T]) {
    for p in people {
        println!("{}", p.hello());
    }
}

T: Greet는 "T는 Greet를 구현한 모든 타입". Java generics의 상한(upper bound)과 유사하나 더 강력.

impl Trait (2018)

fn make_greeter() -> impl Greet {
    Kim { name: String::from("김") }
}

"어떤 Greet 구현체를 반환한다" — 구체 타입 감춤.

Trait Object — dyn Trait

fn greet_all(people: &[Box<dyn Greet>]) {
    for p in people {
        println!("{}", p.hello());
    }
}

vtable 기반 동적 디스패치. C++ 가상 함수와 유사. 단, dyn 키워드로 비용을 명시.

Orphan Rule

trait 또는 타입 둘 중 하나는 현재 crate 소유여야 impl 가능. 이유: 한 타입에 두 크레이트가 같은 trait 구현하면 충돌.

유명한 Trait

  • Debug{:?} 출력
  • Clone, Copy — 복사
  • Iterator — for 문
  • Drop — 소멸자
  • From/Into — 변환
  • Deref* 연산자 오버로드

6. Zero-cost Abstractions — Bjarne의 약속, Rust의 실현

철학

"What you don't use, you don't pay for. What you do use, you couldn't hand-code better." — Bjarne Stroustrup

예 1 — Iterator

let sum: i32 = (1..=100)
    .filter(|n| n % 2 == 0)
    .map(|n| n * 2)
    .sum();

고수준 함수형 체인이지만 컴파일 후 어셈블리는 수동 for 루프와 동일. LLVM 최적화가 추상화를 완전 제거.

예 2 — Option<T>

let x: Option<i32> = Some(5);
let y = x.unwrap_or(0);

C의 null check보다 안전하지만 런타임 비용 0. Option 자체가 tagged union으로 컴파일.

예 3 — Generic vs dyn Trait

// Generic — 컴파일 시 monomorphization
fn static_greet<T: Greet>(x: T) { ... }  // 인라인 가능

// Trait object — 런타임 vtable
fn dynamic_greet(x: &dyn Greet) { ... }  // 포인터 참조

개발자가 비용을 선택.


7. Error Handling — panic이 아닌 Result

Result<T, E>

fn divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        Err(String::from("zero division"))
    } else {
        Ok(a / b)
    }
}

match divide(10, 2) {
    Ok(v) => println!("{}", v),
    Err(e) => eprintln!("{}", e),
}

? 연산자 — 에러 전파

fn do_stuff() -> Result<(), MyError> {
    let data = read_file("x.txt")?;  // 에러면 즉시 return Err
    let parsed = parse(&data)?;
    save(parsed)?;
    Ok(())
}

try/catch 없이도 에러 전파가 깔끔. 호출자가 Result를 반환해야.

panic! — 비정상 상황만

  • 배열 out of bounds, unwrap 실패 등
  • 복구 불가능 로직 오류
  • 실무에서는 거의 안 발생해야 함

thiserror & anyhow

  • thiserror — 라이브러리용, 명시적 에러 enum
  • anyhow — 애플리케이션용, 편한 에러 합성
use anyhow::{Result, Context};

fn load_config() -> Result<Config> {
    let s = std::fs::read_to_string("config.yml")
        .context("config.yml 읽기 실패")?;
    Ok(serde_yaml::from_str(&s)?)
}

8. Async/Await와 Tokio

async/await의 비동기

async fn fetch_user(id: u64) -> Result<User> {
    let resp = reqwest::get(format!("/users/{}", id)).await?;
    Ok(resp.json().await?)
}

#[tokio::main]
async fn main() {
    let user = fetch_user(1).await.unwrap();
    println!("{:?}", user);
}

async는 Future를 만든다

async fn 호출만으로는 아무것도 안 일어남Future만 반환. runtime이 poll해야 실행.

런타임

  • Tokio — 사실상 표준, 멀티 스레드, I/O 최적화
  • async-std — 표준 라이브러리 스타일
  • smol — 경량
  • monoio / glommio — io_uring 기반, thread-per-core

Tokio의 강점

  • 수백만 동시 연결
  • tokio::spawn으로 비동기 태스크
  • tokio::select!, join!로 복잡한 병렬 제어
  • 풍부한 생태계 (hyper, tonic, axum, tower)

JavaScript의 async/await와 차이

측면JSRust
기본 실행즉시 시작 (Promise)await되기 전에는 안 함
런타임빌트인라이브러리
타입Promise<T>impl Future<Output=T>
취소암시적 어려움drop으로 자동
성능V8 튜닝 한계네이티브

Async Trait — 2024년 드디어

오랫동안 async fn in trait는 workaround 필요했음. **Rust 1.75(2023년 12월)**에서 안정화 시작. 2024-2025에 완성.


9. Cargo — 패키지 매니저의 이상향

한 도구로 전부

cargo new myapp          # 새 프로젝트
cargo build              # 빌드
cargo run                # 실행
cargo test               # 테스트
cargo bench              # 벤치마크
cargo doc                # 문서 생성
cargo fmt                # 포맷팅 (rustfmt)
cargo clippy             # 린트
cargo publish            # crates.io 업로드

의존성 관리, 빌드, 테스트, 문서, 벤치마크가 표준 명령 하나씩. Python/Node 생태의 혼란(pip/poetry, npm/yarn/pnpm)과 극명한 대비.

Cargo.toml

[package]
name = "myapp"
version = "0.1.0"
edition = "2024"

[dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
anyhow = "1"

[dev-dependencies]
criterion = "0.5"

Features — 선택적 기능

[features]
default = ["tokio"]
tokio = ["dep:tokio"]
async-std = ["dep:async-std"]

같은 crate에서 여러 런타임 지원 등. 조건부 컴파일과 결합.

Workspace — 모노레포 기본 지원

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

여러 crate를 한 리포에서. 의존성 버전 통일.

crates.io

  • 중앙 저장소
  • 17만+ crate
  • cargo publish 한 줄
  • 버전 yank 가능 (삭제는 불가)

10. 유명한 Rust 프로젝트 2025

인프라

  • ripgrepgrep 대체, 훨씬 빠름
  • fdfind 대체
  • batcat with syntax highlight
  • exa/ezals 개선
  • starship — 셸 프롬프트
  • helix — 에디터

컨테이너/클라우드

  • containerd plugin
  • firecracker (AWS Lambda) — 부분 Rust
  • Linkerd2-proxy — service mesh 프록시
  • kube-rs — Rust K8s 클라이언트

프런트엔드 도구

  • swc — Babel 대체
  • Turbopack — Webpack 후속
  • Rspack — Webpack 호환
  • Biome (fka Rome) — 린트+포맷
  • oxc — 새로운 JS 툴체인

런타임/DB

  • Deno — Node 대안
  • Qdrant — 벡터 DB
  • SurrealDB — 멀티모델
  • TiKV — 분산 KV
  • InfluxDB 3 — 시계열 (Rust 재작성)
  • Neon — 서버리스 Postgres (부분 Rust)

웹/서버

  • axum — Tokio 팀 웹 프레임워크
  • actix-web — 고성능 웹
  • rocket — 개발 친화
  • tower — 미들웨어 생태

11. Rust로 쓰는 프론트엔드 — WebAssembly

wasm-bindgen

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn greet(name: &str) -> String {
    format!("안녕, {}!", name)
}

→ JS에서 greet("Kim") 호출 가능.

Leptos — Solid.js 스타일

#[component]
fn Counter() -> impl IntoView {
    let (n, set_n) = create_signal(0);
    view! {
        <button on:click=move |_| set_n.update(|v| *v += 1)>
            "카운트: " {n}
        </button>
    }
}
  • Fine-grained reactivity
  • SSR + Hydration 지원
  • 번들 매우 작음

Dioxus — React 스타일

fn Counter(cx: Scope) -> Element {
    let n = use_state(cx, || 0);
    render! {
        button { onclick: move |_| n.set(n + 1), "{n}" }
    }
}
  • React-like JSX 매크로
  • Web, Desktop, Mobile, TUI 크로스
  • 2024-2025 급성장

Yew

  • 가장 오래된 Rust 프론트엔드 프레임워크
  • Virtual DOM 기반
  • 보수적 안정성

현실

  • 프로덕션 채택은 아직 작음
  • React/Vue 대체 목적보다 특정 성능 병목 대체
  • Figma, Photopea 같은 복잡 클라이언트에서 Wasm 모듈로

12. Rust for Linux — 2025 승리

긴 여정

  • 2020: Linus Torvalds "고려할 가치 있음" 발언
  • 2021: 메일링 리스트 공식 제안
  • 2022: 6.1 커널에 Rust 초기 지원
  • 2024: 일부 드라이버 (Apple GPU, Nvidia 오픈 드라이버 일부)
  • 2025: 본격 드라이버 병합

영향

  • 커널 CVE의 70%가 메모리 안전 버그
  • Rust가 이를 원천 차단
  • Microsoft도 Windows 커널 부분 Rust 이식 중

갈등

  • 일부 C 메인테이너의 저항 ("코드가 아닌 부족주의")
  • 2024년 잘 알려진 메인테이너 사임 사건
  • 언어 전환은 기술만이 아닌 문화 문제

Asahi Linux 팀

  • Apple Silicon Linux — 거의 모든 신규 드라이버 Rust
  • Rust의 실전 투입에 가장 급진적

13. 정부와 규제 — Rust의 강제

백악관 ONCD 보고서 (2024년 2월)

"Memory-safe languages, including Rust, Go, C#, Java, Swift, Python, and JavaScript, offer...the most comprehensive approach to eliminate an entire class of vulnerabilities."

국방부, NASA, 항공

  • 신규 안전 필수 시스템에 Rust 권고
  • C/C++ 감사 부담 대비 Rust 지지 증가

Ferrocene — 인증된 Rust 컴파일러

  • ISO 26262 (자동차 기능 안전)
  • DO-178C (항공 소프트웨어)
  • Ferrous Systems가 상용 지원

2025 현재

  • Infineon, Volvo, Boeing 파일럿
  • Rust가 안전 필수 임베디드의 주류 후보로 부상

14. Rust 배우는 가장 빠른 길

1단계 (1-2주)

  1. The Rust Book (무료, 공식) 완독
  2. rustlings 과제 100개
  3. 작은 CLI 만들기 (argh/clap 사용)

2단계 (2-4주)

  1. Ownership/Borrowing에 익숙해지기
  2. Result? 체화
  3. Iterator 체인 숙달
  4. Trait 설계 연습

3단계 (1-3개월)

  1. Async/Tokio로 네트워크 앱
  2. serde로 JSON/YAML
  3. 에러 타입 설계 (thiserror/anyhow)
  4. 소규모 실전 프로젝트 배포

4단계 (3개월+)

  1. unsafe 이해
  2. 매크로 (선언적/절차적)
  3. Lifetime 고급
  4. FFI (C와 연동)
  5. 오픈소스 crate 기여

무료 자원

  • The Rust Book (doc.rust-lang.org/book)
  • Rust by Example
  • Rustlings (github.com/rust-lang/rustlings)
  • Rust for JavaScript Developers
  • Zero to Production in Rust (Luca Palmieri, 유료)

15. 안티패턴 TOP 10

  1. clone() 남발 — 컴파일은 되지만 성능 낭비
  2. unwrap() 전역 — 프로덕션에서 패닉
  3. Rc<RefCell<T>>로 빌림 규칙 회피 — Rust의 장점 포기
  4. 모든 걸 async — CPU 바인딩 코드는 필요 없음
  5. Lifetime 과도 명시 — elision이 대부분 처리
  6. dyn Trait 성능 고려 없이 — generic이 더 빠를 때 많음
  7. 매크로 남발 — 컴파일 시간 폭발, 디버그 지옥
  8. Box<dyn Error> 남발 — 세밀한 에러 타입 포기
  9. unsafe 없이도 되는데 unsafe — 안전성 포기
  10. Rust 스타일 아닌 다른 언어 패러다임 — snake_case, trait 중심

16. Rust 현명하게 쓰기 체크리스트

  • cargo clippy CI에서 실행 — 코딩 스타일 통일
  • cargo fmt pre-commit — 자동 포맷
  • #![deny(unsafe_code)] 라이브러리 기본
  • 에러 타입 설계 — thiserror + anyhow 조합
  • 공개 API 문서/// 주석 + doctest
  • 벤치마크 — criterion으로 성능 회귀 방지
  • Miri 테스트 — unsafe 검증
  • cargo-deny — 라이선스/취약점 감사
  • MSRV 명시 — Minimum Supported Rust Version
  • CI에서 여러 플랫폼 테스트 — Linux/Mac/Windows
  • Feature flag 설계 — 선택적 의존성
  • Async vs Sync 결정 — 정말 async 필요한가

마치며 — "시스템 프로그래밍의 르네상스"

30년간 시스템 프로그래밍은 C/C++의 독점이었다. 누구도 이 독점을 깰 수 있다고 믿지 않았다. 2015년 Rust 1.0이 나왔을 때도, 대부분의 전문가는 "호기심 거리"로 치부했다.

10년이 지난 지금, Linux 커널이, 백악관이, Azure와 AWS가 Rust로 이동하고 있다. 이는 혁명이라기보다 세대 교체에 가깝다. C는 사라지지 않지만, 새로운 시스템 코드는 점점 Rust로 태어난다.

Rust의 진짜 선물은 속도가 아니다. 그것은 **"컴파일러와 협력해 올바른 프로그램을 작성하는 경험"**이다. borrow checker와 싸우다 보면, 당신은 메모리, 동시성, 생명주기에 대한 직관을 얻는다. 그 직관은 다른 언어로 돌아가도 영원히 당신과 함께 간다.

"Writing Rust is sometimes like being shouted at by an expert compiler. When you finally agree with it, your code is bulletproof." — Amos Wenger (fasterthanli.me)


다음 글 예고 — Go와 클라우드 네이티브의 언어

Rust가 "컴파일러와 싸우며 배우는" 언어라면, Go는 "1주일이면 쓸 수 있는" 언어다. 그 단순함으로 Docker, Kubernetes, Prometheus, Terraform, 그리고 현대 클라우드 네이티브 인프라 전체를 썼다. 다음 글에서는:

  • Go의 탄생 철학 — Google의 빌드 지옥에서 태어남
  • 고루틴과 채널 — CSP(Communicating Sequential Processes) 모델
  • GC의 혁신 — Pauseless에 가까운 ms 단위
  • 단순함의 대가 — generics 논쟁 10년
  • 모듈 시스템 — GOPATH의 악몽에서 go.mod까지
  • 표준 라이브러리 — 배터리 포함 철학
  • 인터페이스의 구조적 타이핑 — Go가 다시 보여준 우아함
  • Generics(1.18) 이후 — 2년간의 변화
  • Go 1.24 PGO와 최신 성능 기능
  • 왜 클라우드 네이티브 전부가 Go로 쓰였나 — k8s, docker, prometheus, grafana, terraform...
  • TinyGo로 임베디드, WASM
  • Go와 Rust의 공존 — 선택 기준

"빠른 컴파일 + 간단한 문법"이 어떻게 산업을 바꿨는지 정리하는 여정.


"Rust is for when you need to be fast. Go is for when you need to be done. Python is for when you need both. TypeScript is for when you had a Python and want to sleep at night." — Anonymous HN comment (2024)