Rust 완전 가이드: 기초부터 커널 적용까지
Rust 언어의 탄생 배경과 설계 철학, 기초 문법(변수, 타입, 소유권, 빌림, 수명, 패턴 매칭, 에러 처리)부터 중급(트레이트, 제네릭, 컬렉션, 반복자, 스마트 포인터, 동시성, 매크로, 메모리 레이아웃), 고급(unsafe, FFI, async/await, Pin, no_std)까지 모든 레벨을 다루고, Linux 커널에서의 Rust 적용(아키텍처, Kbuild 통합, bindgen, 추상화 계층, 드라이버, 테스팅, 디버깅, C→Rust 마이그레이션 전략)을 SVG 다이어그램과 함께 심층 해설합니다.
핵심 요약
- 소유권(Ownership) — 모든 값에는 정확히 하나의 소유자가 있으며, 소유자가 스코프를 벗어나면 값이 자동으로 해제됩니다. GC 없이도 메모리 안전성을 보장하는 Rust의 근간입니다.
- 빌림(Borrowing) — 공유 참조(
&T)는 여러 개 가능하지만, 가변 참조(&mut T)는 동시에 하나만 존재할 수 있습니다. 이 규칙이 데이터 레이스를 컴파일 타임에 차단합니다. - 수명(Lifetime) — 참조는 참조 대상보다 오래 살 수 없으며, 컴파일러가 이를 정적으로 검증합니다. 댕글링 포인터를 원천 차단합니다.
- 제로 코스트 추상화 — 트레이트, 제네릭, 반복자 등의 추상화가 런타임 오버헤드 없이 동작합니다. 컴파일러가 단형화(monomorphization)와 인라인 최적화를 수행하여 C와 동등한 성능을 냅니다.
- unsafe — 하드웨어 접근, FFI, 원시 포인터 등 안전성 검증이 불가능한 영역을 위한 명시적 탈출구입니다. 안전한 추상화(safe wrapper)로 감싸는 것이 관례입니다.
- 커널 적용 — Linux 6.1부터 Rust가 커널 개발 언어로 공식 지원되며, 안전한 추상화 계층을 통해 C API와 상호운용됩니다.
unsafe경계를 최소화하고 safe Rust로 드라이버를 작성하는 것이 목표입니다.
| 개념 | 해결하는 문제 | C 대비 이점 | 관련 섹션 |
|---|---|---|---|
| 소유권 | Use-After-Free, Double Free | 수동 free() 불필요, RAII 보장 | 소유권 |
| 빌림 | 데이터 레이스, 동시 수정 | 스레드 안전성 컴파일 검증 | 빌림 |
| 수명 | 댕글링 포인터 | 참조 유효성 정적 검증 | 수명 |
| 트레이트 | 코드 중복, 다형성 | 제로 코스트 추상화 + 타입 안전 | 트레이트 |
| Result/Option | NULL 포인터, 에러 무시 | 에러 처리 강제, 런타임 체크 최소 | 에러 처리 |
| unsafe/FFI | 하드웨어 접근, C 연동 | 위험 코드 범위를 명시적으로 제한 | unsafe |
단계별 이해
- Part 1: 탄생과 철학 (3개 섹션)
Rust가 왜 만들어졌고, 어떤 문제를 해결하는지 이해합니다. C/C++의 메모리 안전 문제가 어떻게 Rust의 소유권 시스템으로 해결되는지, Rust 생태계(rustup, cargo, crates.io)의 전체 구조를 파악합니다. - Part 2: 기초 문법 (8개 섹션)
변수와 타입 시스템부터 시작하여 소유권, 빌림, 수명의 3대 핵심 개념을 학습합니다. 구조체, 열거형, 패턴 매칭, 에러 처리까지 Rust 프로그래밍의 기본기를 다집니다. 각 개념에 SVG 다이어그램과 코드 예제가 포함됩니다. - Part 3: 중급 (8개 섹션)
트레이트와 제네릭으로 코드 재사용 패턴을 익히고, 스마트 포인터(Box, Rc, Arc, RefCell), 동시성(Send/Sync, Mutex, Channel), 매크로 시스템, 메모리 레이아웃까지 실무에 필요한 중급 패턴을 다룹니다. - Part 4: 고급 (6개 섹션)
unsafe Rust의 5가지 초능력, C↔Rust FFI 호출 패턴, async/await 상태 머신, 고급 타입 시스템(HRTB, GAT, PhantomData), Pin/Unpin, no_std 환경까지 시스템 프로그래밍의 핵심을 다룹니다. 커널 진입을 위한 필수 지식입니다. - Part 5: 커널 적용 (18+ 섹션)
커널 Rust 아키텍처, Kbuild/bindgen 빌드 파이프라인, 추상화 계층, 디바이스 드라이버(misc, platform, PCI, PHY), workqueue/타이머, 테스팅(KUnit), 디버깅, C vs Rust 비교, 점진적 마이그레이션 전략까지 커널 개발의 모든 것을 다룹니다.
| 단계 | 선수 지식 | 학습 시간 (추정) | 핵심 역량 |
|---|---|---|---|
| Part 1 | 프로그래밍 기초 | 1-2시간 | Rust의 탄생 배경과 설계 철학 이해 |
| Part 2 | C/C++ 기초 경험 | 10-20시간 | 소유권/빌림/수명 체득, 기본 코드 작성 |
| Part 3 | Part 2 완료 | 15-25시간 | 트레이트 기반 설계, 동시성 패턴 활용 |
| Part 4 | Part 3 + C 연동 경험 | 10-20시간 | unsafe/FFI 안전하게 다루기, no_std |
| Part 5 | Part 4 + 커널 기초 | 20-40시간 | 커널 Rust 드라이버 작성, 빌드, 테스트 |
C vs Rust 핵심 개념 대조표
C 개발자가 Rust를 배울 때 가장 혼란스러운 부분을 한눈에 비교할 수 있는 대조표입니다.
| 개념 | C | Rust | Rust의 이점 |
|---|---|---|---|
| 변수 기본값 | 가변 (기본) | 불변 (기본, mut으로 가변) | 의도하지 않은 변경 방지 |
| 메모리 관리 | malloc/free 수동 | 소유권 + 자동 drop | 메모리 누수·이중 해제 방지 |
| null 포인터 | NULL (역참조 시 segfault) | Option<T> (컴파일 타임 검사) | null 참조 버그 원천 차단 |
| 에러 처리 | 반환 코드 + errno | Result<T, E> + ? 연산자 | 에러 무시 불가, 타입 안전 |
| 문자열 | char* + \0 종단 | String (소유) / &str (빌림) | 버퍼 오버플로 방지, UTF-8 보장 |
| 배열 접근 | 범위 검사 없음 (오버플로 가능) | 런타임 범위 검사 (panic) | 버퍼 오버리드/오버라이트 방지 |
| 동시성 안전 | 프로그래머 책임 | Send/Sync 트레이트 | 데이터 레이스 컴파일 타임 차단 |
| enum | 정수 상수 | Tagged Union (데이터 포함 가능) | 타입 안전한 상태 머신 |
| switch/match | fall-through 기본 | 완전성 검사 + 구조 분해 | case 누락·fall-through 버그 방지 |
| 매크로 | 텍스트 치환 (#define) | 위생적(hygienic) + 타입 인지 | 매크로 관련 버그 감소 |
| 인터페이스 | 함수 포인터 테이블 | 트레이트 (Trait) | 타입 안전, 제네릭 활용 |
| 타입 변환 | 암묵적 변환 허용 | as 명시적 변환만 | 정수 오버플로·부호 버그 방지 |
Rust 탄생 배경과 역사
Rust는 2006년 Mozilla 직원 Graydon Hoare의 개인 프로젝트로 시작되었습니다. C/C++의 메모리 안전성 문제를 근본적으로 해결하면서도 시스템 프로그래밍 수준의 성능을 유지하겠다는 목표로 설계되었습니다.
| 시기 | 핵심 사건 | 의미 |
|---|---|---|
| 2006 | Graydon Hoare 개인 프로젝트 | C++의 메모리 안전성 문제를 타입 시스템으로 해결하려는 첫 시도 |
| 2009 | Mozilla 공식 지원 | Servo 브라우저 엔진 프로젝트와 함께 본격적인 언어 개발 |
| 2011 | 셀프 호스팅 달성 | Rust 컴파일러가 Rust로 자신을 컴파일 — 언어 성숙도의 증거 |
| 2015.05 | Rust 1.0 안정 릴리스 | 하위 호환성 보장 시작, 6주 주기 릴리스 |
| 2018 | Rust 2018 Edition | 모듈 시스템 개선, NLL(Non-Lexical Lifetimes), async/await 기반 확립 |
| 2021.02 | Rust Foundation 설립 | AWS, Google, Huawei, Microsoft, Mozilla가 후원하는 독립 재단 |
| 2022.12 | Linux 6.1 머지 | 역사상 가장 큰 오픈소스 프로젝트에 Rust가 제2 언어로 진입 |
| 2024 | Rust 2024 Edition | RPIT 캡처 규칙, unsafe 내 unsafe 호출 필수화, gen 키워드 예약 |
Rust 릴리스 모델: Rust는 6주마다 새 안정(stable) 버전을 출시합니다. Edition(2015, 2018, 2021, 2024)은 하위 호환을 깨는 변경을 묶어 출시하며, 서로 다른 Edition의 크레이트가 같은 프로젝트에서 공존할 수 있습니다. 이는 커널처럼 장기 유지보수가 필요한 프로젝트에 유리합니다.
Rust 설계 철학: 안전성·동시성·성능
Rust의 설계 철학은 "안전성과 성능은 트레이드오프가 아니다"라는 명제에 기반합니다. 전통적으로 C/C++은 성능을 위해 안전성을 희생하고, Java/Python은 안전성을 위해 성능을 희생했습니다. Rust는 컴파일 타임 검증을 통해 런타임 비용 없이 안전성을 보장합니다.
핵심 설계 원칙
| 원칙 | 의미 | 구현 방법 |
|---|---|---|
| 제로 코스트 추상화 | 추상화를 사용해도 직접 작성한 코드와 동일한 성능 | 제네릭의 단형화(monomorphization), 인라인 최적화 |
| 소유권 기반 메모리 관리 | GC 없이 컴파일 타임에 메모리 해제 시점 결정 | 소유권, 빌림, 수명 시스템 |
| Fearless Concurrency | 데이터 레이스를 타입 시스템으로 원천 차단 | Send/Sync 트레이트, 소유권 이전 |
| 표현력 있는 타입 시스템 | 런타임 에러를 컴파일 타임으로 이동 | Option, Result, 패턴 매칭, 트레이트 |
| 실용주의 | 안전성이 불가능한 영역에서도 작동 | unsafe 블록으로 명시적 탈출구 제공 |
// 제로 코스트 추상화 예시: 반복자 체인은 수동 루프와 동일한 기계어 생성
let sum: i64 = (1..1000)
.filter(|n| n % 3 == 0 || n % 5 == 0)
.sum(); // 컴파일러가 최적의 루프 하나로 변환
// Fearless Concurrency: 소유권 이전으로 데이터 레이스 원천 방지
let data = vec![1, 2, 3];
std::thread::spawn(move || {
// data의 소유권이 이 스레드로 이동 — 원래 스레드에서 접근 불가
println!("{:?}", data);
});
// println!("{:?}", data); ← 컴파일 에러! 소유권이 이미 이동함
시스템 프로그래밍 언어 비교
| 특성 | C | C++ | Rust | Go | Zig |
|---|---|---|---|---|---|
| 메모리 관리 | 수동 (malloc/free) | 수동 + RAII + 스마트 포인터 | 소유권 시스템 (컴파일 타임) | GC (가비지 컬렉터) | 수동 + 할당자 인터페이스 |
| 메모리 안전 | 보장 안 됨 | 부분적 (UB 가능) | safe Rust에서 보장 | 보장 (런타임 비용) | 보장 안 됨 (선택적 검사) |
| 데이터 레이스 | 방지 안 됨 | 방지 안 됨 | 컴파일 타임 방지 | 런타임 탐지 (race detector) | 방지 안 됨 |
| 제로 코스트 추상화 | 없음 (매크로만) | 있음 (템플릿) | 있음 (제네릭/트레이트) | 없음 (인터페이스 박싱) | 있음 (comptime) |
| 커널 사용 | 주력 (100%) | 미사용 | 공식 지원 (6.1+) | 미사용 | 미사용 |
| 에러 처리 | 반환값 + errno | 예외 + 반환값 | Result<T, E> + ? | 다중 반환값 | 에러 유니온 |
| 학습 곡선 | 낮음-중간 | 높음 | 높음 (빌림 검사기) | 낮음 | 중간 |
왜 커널에 C++이 아닌 Rust인가? Linus Torvalds는 C++을 "끔찍한 언어"로 평가하며 커널에서의 사용을 거부해 왔습니다. C++의 예외, RTTI, 복잡한 상속 계층이 커널 환경에 부적합한 반면, Rust는 no_std 지원, 예외 없는 에러 처리(Result), 명시적 메모리 관리가 커널 요구사항에 부합합니다. 또한 Rust는 C++과 달리 메모리 안전성을 언어 수준에서 보장합니다.
Rust 생태계와 도구
Rust의 강력한 생태계는 언어 자체만큼이나 중요합니다. rustup으로 툴체인을 관리하고, cargo로 빌드·의존성·테스트를 통합 관리하며, crates.io에서 수십만 개의 라이브러리를 활용할 수 있습니다.
| 도구 | 명령어 예시 | 설명 |
|---|---|---|
| rustup | rustup install stable | Rust 툴체인 설치·관리. stable/beta/nightly 채널 전환 |
| cargo | cargo new myproject | 프로젝트 생성, 빌드, 테스트, 의존성 관리를 통합 |
| rustc | rustc main.rs | Rust 컴파일러. 보통 cargo를 통해 간접 호출 |
| clippy | cargo clippy | 550+ 린트 규칙으로 코드 품질 경고 |
| rustfmt | cargo fmt | 공식 스타일 가이드에 맞춰 자동 포맷팅 |
| rust-analyzer | IDE 플러그인 | 자동완성, 타입 추론 표시, 리팩토링 지원 (LSP) |
| miri | cargo +nightly miri test | Undefined Behavior 동적 검출 인터프리터 |
| bindgen | bindgen input.h -o bindings.rs | C 헤더에서 Rust FFI 바인딩 자동 생성 (커널 필수) |
# Rust 설치 (공식 방법)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# 프로젝트 생성과 빌드
cargo new hello_rust && cd hello_rust
cargo build # 디버그 빌드
cargo build --release # 최적화 빌드 (릴리스용)
cargo test # 모든 테스트 실행
cargo doc --open # API 문서 생성 후 브라우저 열기
# Cargo.toml 의존성 추가
cargo add serde --features derive # serde + derive 피처 추가
cargo add tokio --features full # tokio 비동기 런타임
# Cargo.toml 예시
[package]
name = "hello_rust"
version = "0.1.0"
edition = "2024" # Rust Edition (2015, 2018, 2021, 2024)
[dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
[dev-dependencies]
criterion = "0.5" # 벤치마크 라이브러리
[profile.release]
opt-level = 3 # 최대 최적화
lto = true # 링크 타임 최적화
커널 개발과 일반 Rust의 차이: 커널 Rust는 no_std 환경이므로 crates.io의 대부분의 크레이트를 직접 사용할 수 없습니다. 대신 커널 트리의 rust/ 디렉터리에 포함된 커널 전용 크레이트(kernel, macros, alloc)를 사용합니다. 빌드도 cargo 대신 Kbuild(make)를 통해 수행됩니다.
변수, 타입 시스템, 섀도잉
Rust에서 변수는 기본적으로 불변(immutable)입니다. 변경이 필요하면 mut 키워드로 명시해야 합니다. 이는 실수로 인한 값 변경을 방지하고, 의도적인 변경을 코드에서 명확히 드러냅니다.
일상 비유 — 계약서와 화이트보드: 불변 변수(let x = 5)는 서명된 계약서와 같습니다 — 한번 작성하면 수정할 수 없습니다. 가변 변수(let mut x = 5)는 화이트보드처럼 언제든 지우고 다시 쓸 수 있습니다. Rust는 기본적으로 모든 것을 계약서로 만들고, 명시적으로 "이건 화이트보드입니다"라고 표시해야 합니다. 이렇게 하면 코드를 읽을 때 mut가 보이는 변수만 "이 값이 변할 수 있구나"라고 주의하면 됩니다.
C/C++ 개발자를 위한 비교: C에서는 int x = 5;로 선언하면 기본적으로 가변이고, 불변으로 만들려면 const int x = 5;라고 써야 합니다. Rust는 이 관계가 정반대입니다. 대부분의 변수는 한번 초기화된 후 변경될 필요가 없으므로, 기본 불변이 더 안전합니다. 커널 코드에서도 실제로 수정이 필요한 변수는 전체의 일부에 불과하기 때문에 이 설계는 매우 합리적입니다.
// 변수 선언과 불변성
let x = 5; // 불변 — 기본값
// x = 6; // ← 컴파일 에러! 불변 변수에 재대입 불가
let mut y = 10; // 가변 — mut 명시 필요
y = 20; // OK
// 상수 — 컴파일 타임에 결정, 타입 명시 필수
const MAX_POINTS: u32 = 100_000;
// 섀도잉(Shadowing) — 같은 이름으로 새 변수를 선언하여 타입 변환도 가능
let spaces = " "; // &str 타입
let spaces = spaces.len(); // usize 타입으로 변환 — 같은 이름의 새 변수
// mut와 달리 타입이 변경 가능하고, 새 변수는 불변
let, const, static 비교
Rust에서 값을 바인딩하는 세 가지 키워드의 차이를 이해하는 것이 중요합니다.
| 키워드 | 평가 시점 | 메모리 위치 | 수명 | 타입 추론 | C 대응 |
|---|---|---|---|---|---|
let | 런타임 | 스택 | 스코프 한정 | 가능 | int x = func(); |
const | 컴파일 타임 | 인라인 치환 | 해당 없음 (값 치환) | 불가 (타입 필수) | #define 또는 enum |
static | 컴파일 타임 | 고정 주소 (.data/.bss) | 프로그램 전체 ('static) | 불가 (타입 필수) | static int x = 5; |
// let — 런타임 값 바인딩 (가장 일반적)
let start = std::time::Instant::now(); // 런타임에 결정되는 값 OK
// const — 컴파일 타임 상수 (사용 시마다 값이 인라인 삽입)
const MAX_RETRIES: u32 = 3; // 타입 명시 필수, 주소 없음
const HEADER: &str = "X-Custom"; // 문자열 리터럴도 가능
// static — 고정 메모리 주소를 가지는 전역 변수
static COUNTER: AtomicU64 = AtomicU64::new(0); // 주소가 있어 참조 가능
static mut BUFFER: [u8; 1024] = [0; 1024]; // static mut → unsafe 필수!
// 커널에서: const로 ioctl 번호, static으로 전역 구조체 정의
// const IOCTL_GET_INFO: u32 = 0x8004_0001;
// static DEVICE: Mutex<Option<Device>> = Mutex::new(None);
스칼라 타입
| 분류 | 타입 | 크기 | 설명 |
|---|---|---|---|
| 정수 | i8 / u8 | 1 byte | 부호 있는/없는 8비트 정수 |
i16 / u16 | 2 bytes | 부호 있는/없는 16비트 정수 | |
i32 / u32 | 4 bytes | 부호 있는/없는 32비트 정수 (i32가 기본) | |
i64 / u64 | 8 bytes | 부호 있는/없는 64비트 정수 | |
isize / usize | 포인터 크기 | 플랫폼 의존 (32 또는 64비트). 인덱싱에 사용 | |
| 부동소수점 | f32 | 4 bytes | 단정밀도 (IEEE 754) |
f64 | 8 bytes | 배정밀도 (기본값) | |
| 불리언 | bool | 1 byte | true / false |
| 문자 | char | 4 bytes | 유니코드 스칼라 값 (U+0000 ~ U+D7FF, U+E000 ~ U+10FFFF) |
복합 타입
// 튜플 — 서로 다른 타입의 값을 묶음
let tup: (i32, f64, u8) = (500, 6.4, 1);
let (x, y, z) = tup; // 구조 분해
let first = tup.0; // 인덱스 접근
// 배열 — 같은 타입, 고정 길이 (스택 할당)
let arr: [i32; 5] = [1, 2, 3, 4, 5];
let zeros = [0; 10]; // [0, 0, 0, ..., 0] — 10개 0으로 초기화
let first = arr[0]; // 인덱스 접근 (범위 검사 포함!)
// 슬라이스 — 배열/벡터의 연속된 부분 참조
let slice: &[i32] = &arr[1..3]; // [2, 3] — 소유권 없이 참조
// 문자열 타입
let s1: &str = "hello"; // 문자열 슬라이스 (불변, 스택/정적 영역)
let s2: String = String::from("hello"); // 소유된 문자열 (힙, 가변 가능)
let s3 = s2.as_str(); // String → &str 변환
// 타입 변환(casting)
let x: i32 = 42;
let y: f64 = x as f64; // 명시적 변환 필수 (암묵적 변환 없음!)
let z: u8 = 256 as u8; // 오버플로: 0 (값이 잘림)
// 정수 리터럴 접미사
let a = 42u32; // u32 타입
let b = 3.14f32; // f32 타입
let c = 0xFF_u8; // 16진수 u8
let d = 0b1010_0110; // 2진수
let e = 1_000_000; // 밑줄로 자릿수 구분 (가독성)
정수 오버플로 동작
| 빌드 모드 | 오버플로 발생 시 | 예시: 255u8 + 1 |
|---|---|---|
| debug | panic! (프로그램 중단) | thread panicked: overflow |
| release | 2의 보수 래핑 (C와 동일) | 0 (래핑) |
// 명시적 오버플로 제어 메서드 (빌드 모드와 무관하게 동일 동작)
let a: u8 = 250;
a.wrapping_add(10) // 4 — 항상 래핑
a.checked_add(10) // None — 오버플로 시 None 반환
a.saturating_add(10) // 255 — 최대값에서 포화
a.overflowing_add(10) // (4, true) — 값 + 오버플로 플래그
// 커널에서는 wrapping_* 또는 checked_*를 사용하여
// 정수 오버플로를 명시적으로 처리해야 합니다.
Rust의 타입 안전성: C와 달리 Rust는 암묵적 타입 변환을 허용하지 않습니다. i32를 u64에 대입하려면 명시적으로 as를 사용해야 합니다. 이는 정수 오버플로나 부호 변환으로 인한 버그를 방지합니다. 커널에서 특히 중요한데, C 커널 코드의 상당수 버그가 암묵적 정수 변환에서 비롯되기 때문입니다. 안전한 변환이 필요하면 TryFrom / TryInto 트레이트를 사용하세요: u8::try_from(256i32)는 Err를 반환합니다.
타입 별칭과 const fn
Rust의 type 키워드는 기존 타입에 대한 별칭(alias)을 만듭니다. 새로운 타입을 정의하는 것이 아니라, 긴 타입 표기를 짧게 줄여 가독성을 높이는 용도입니다. const fn은 컴파일 타임에 평가 가능한 함수를 정의하며, 커널처럼 런타임 비용을 최소화해야 하는 환경에서 특히 유용합니다.
// ── 타입 별칭(Type Alias) ──
type Kilometers = i32; // i32와 완전히 동일한 타입
let distance: Kilometers = 42;
let raw: i32 = 10;
let total = distance + raw; // OK — 같은 타입이므로 연산 가능
// 복잡한 타입을 줄일 때 유용
type Thunk = Box<dyn Fn() + Send + 'static>;
type Result<T> = std::result::Result<T, std::io::Error>; // std::io에서 실제 사용
fn spawn_task(f: Thunk) { /* Box<dyn Fn() + Send + 'static> 대신 간결 */ }
fn read_file() -> Result<String> { /* std::result::Result<String, io::Error> */ }
// 커널에서의 타입 별칭
// kernel::Result<T> = core::result::Result<T, kernel::error::Error>
// 커널 코드 전체에서 간결하게 Result<()>, Result<i32> 등으로 사용
// ── const fn: 컴파일 타임 함수 ──
// const: 컴파일 타임 상수 (값만 가능)
const MAX_SIZE: usize = 1024;
// const fn: 컴파일 타임에 평가 가능한 함수
const fn kilobytes(kb: usize) -> usize {
kb * 1024
}
const BUFFER_SIZE: usize = kilobytes(4); // 4096 — 컴파일 타임에 계산
let runtime_size = kilobytes(8); // 런타임에도 호출 가능
// const fn으로 비트 마스크 생성
const fn bit_mask(bits: u32) -> u32 {
(1u32 << bits) - 1
}
const LOW_4_BITS: u32 = bit_mask(4); // 0b1111 = 15
const LOW_8_BITS: u32 = bit_mask(8); // 0xFF = 255
// const fn의 제약 (Rust 안정 채널 기준)
// - 힙 할당 불가 (Box, Vec, String 등)
// - 트레이트 객체 호출 불가
// - 부동소수점 연산 가능 (Rust 1.82+)
// - if/match/loop 사용 가능 (Rust 1.46+)
const fn clamp(val: i32, min: i32, max: i32) -> i32 {
if val < min { min }
else if val > max { max }
else { val }
}
const CLAMPED: i32 = clamp(150, 0, 100); // 100 — 컴파일 타임에 결정
커널에서의 const fn: 리눅스 커널 Rust 코드에서 const fn은 레지스터 오프셋 계산, 비트 플래그 조합, 버퍼 크기 결정 등에 광범위하게 사용됩니다. 런타임 오버헤드 없이 안전한 계산을 보장하므로, C 매크로(#define)를 대체하는 타입 안전한 대안입니다.
| 특성 | 타입 별칭 (type) | 뉴타입 패턴 (struct) |
|---|---|---|
| 정의 | type Km = i32; | struct Km(i32); |
| 타입 검사 | 원본과 동일 타입 (혼용 가능) | 별도 타입 (혼용 불가) |
| 연산자 | 원본 타입 연산자 그대로 사용 | 직접 구현해야 함 (impl Add) |
| 용도 | 긴 타입 축약, 가독성 | 타입 안전성, 의미 구분 |
| 런타임 비용 | 없음 | 없음 (ZST 최적화) |
| 커널 예시 | kernel::Result<T> | struct Opaque<T>(MaybeUninit<UnsafeCell<T>>) |
문자열 타입 심층 (&str vs String)
Rust에는 두 가지 핵심 문자열 타입이 있습니다. String은 힙에 할당된 가변 소유 문자열이고, &str은 UTF-8 바이트 시퀀스에 대한 불변 참조(슬라이스)입니다. C의 char*와 달리 Rust 문자열은 항상 유효한 UTF-8이며, null 종단이 아닌 길이 정보를 함께 저장합니다.
// ── 문자열 생성과 변환 ──
let s1: &str = "hello"; // 문자열 리터럴: 정적 메모리, 타입은 &'static str
let s2: String = String::from("hello"); // 힙에 복사하여 String 생성
let s3: String = "hello".to_string(); // 동일한 결과
let s4: &str = &s2; // Deref 강제: &String → &str 자동 변환
let s5: &str = s2.as_str(); // 명시적 변환
let s6: &str = &s2[0..3]; // 슬라이싱 (바이트 인덱스!)
// ── UTF-8 인코딩과 인덱싱 ──
let korean = "한글";
korean.len() // 6 — 바이트 길이 (한 = 3바이트, 글 = 3바이트)
korean.chars().count() // 2 — 문자(char) 수
// korean[0] // 컴파일 에러! 인덱싱 불가
// 이유: UTF-8은 가변 길이 인코딩이므로 O(1) 인덱싱이 불가능
// 올바른 문자 순회 방법
for ch in "한글rust".chars() {
print!("{} ", ch); // 한 글 r u s t
}
for b in "한글".bytes() {
print!("{:02x} ", b); // ed 95 9c ea b8 80
}
// 그래핌 클러스터: 사용자가 인식하는 "문자" 단위
// "é" = 'e' + '\u{0301}' (결합 분음 부호)
// chars()는 2개, 그래핌은 1개 → unicode-segmentation 크레이트 필요
// ── String 조작 메서드 ──
let mut s = String::from("Hello");
// 추가
s.push(' '); // char 하나 추가
s.push_str("World"); // &str 추가 → "Hello World"
// 연결
let s1 = String::from("Hello");
let s2 = String::from(" World");
let s3 = s1 + &s2; // + 연산자: s1 소유권 이동! (s1은 더 이상 사용 불가)
// fn add(self, s: &str) → self를 소비
let s4 = format!("{} {}", "Hello", "World"); // 소유권 이동 없이 연결
// 검색과 변환
let text = "Rust is fast and safe";
text.contains("fast") // true
text.starts_with("Rust") // true
text.replace("fast", "efficient") // "Rust is efficient and safe" (새 String 반환)
text.to_uppercase() // "RUST IS FAST AND SAFE"
// 분할과 트림
let parts: Vec<&str> = "a,b,c".split(',').collect(); // ["a", "b", "c"]
" hello ".trim() // "hello" — 양쪽 공백 제거
// 숫자 변환
let n: i32 = "42".parse().unwrap(); // &str → i32
let s: String = 42.to_string(); // i32 → String
// ── C 문자열과의 비교 및 FFI ──
// C: null 종단 문자열 → strlen()으로 길이 계산 O(n)
// char *s = "hello\0"; // 실제 6바이트 (null 포함)
// Rust String: 길이 저장 → len() 은 O(1)
// String { ptr, len: 5, cap: 8 } // null 없음, 길이 즉시 반환
// FFI에서 C 문자열과 상호운용
use std::ffi::{CStr, CString};
// Rust → C: CString (소유, null 종단 보장)
let c_string = CString::new("hello").unwrap(); // 내부에 null이 있으면 Err
let ptr: *const c_char = c_string.as_ptr(); // C 함수에 전달
// C → Rust: CStr (빌림, null 종단 가정)
unsafe {
let c_str = CStr::from_ptr(ptr); // *const c_char → &CStr
let rust_str: &str = c_str.to_str().unwrap(); // &CStr → &str (UTF-8 검증)
}
// 커널에서: kernel::str::CStr — 커널 전용 CStr 래퍼
// c_str!("device_name") 매크로로 커널 C 문자열 생성
// kernel::fmt 모듈로 커널 로깅에 문자열 포맷팅
문자열 슬라이싱 주의: &s[0..3]은 바이트 인덱스입니다. UTF-8 멀티바이트 문자의 중간에서 자르면 panic!이 발생합니다. 예를 들어 &"한글"[0..2]는 '한'이 3바이트이므로 panic합니다. 안전하게 자르려면 char_indices()를 사용하세요.
함수, 제어 흐름, 표현식
Rust에서 함수는 fn 키워드로 선언합니다. 모든 매개변수의 타입을 명시해야 하며, 함수 본문의 마지막 표현식이 반환값이 됩니다(세미콜론 없이). Rust는 표현식 기반 언어로, if, match, 블록 {} 모두 값을 반환합니다.
C 개발자를 위한 핵심 차이 — 표현식 vs 문(Statement): C에서 if, for, { }는 모두 문(statement)이라 값을 반환하지 않습니다. 삼항 연산자 x ? a : b만 값을 반환합니다. Rust에서는 if/else, match, { } 블록이 모두 표현식이라 값을 반환합니다. 이 덕분에 임시 변수 선언이 줄고, const 초기화 같은 복잡한 패턴도 깔끔하게 작성됩니다.
세미콜론의 의미: Rust에서 세미콜론(;)은 표현식을 문으로 변환하는 역할입니다. x + 1은 값을 반환하는 표현식이지만, x + 1;은 값을 버리는 문입니다. 함수 마지막 줄에서 세미콜론을 빠뜨리면 그 값이 반환됩니다. C에서 return을 빠뜨리면 undefined behavior이지만, Rust에서는 세미콜론 유무로 반환 여부가 결정되는 명확한 규칙입니다.
// 함수 선언 — 반환 타입은 -> 뒤에 명시
fn add(a: i32, b: i32) -> i32 {
a + b // 세미콜론 없음 = 표현식으로 반환
}
// 표현식 기반: if는 값을 반환함
let number = 7;
let category = if number > 5 { "big" } else { "small" };
// 블록도 표현식 — 마지막 값이 블록의 값
let result = {
let x = 3;
let y = 4;
x * x + y * y // 25가 result에 대입됨
};
// 반복문
for i in 0..5 { // 0, 1, 2, 3, 4
println!("{}", i);
}
for (idx, val) in arr.iter().enumerate() {
println!("[{}] = {}", idx, val);
}
// loop — 무한 루프 + break로 값 반환
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2; // 20을 반환
}
};
// while let — Option/Result 패턴 소진
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
println!("{}", top); // 3, 2, 1 순서로 출력
}
함수 포인터와 클로저 인자
// 함수 포인터 — C의 함수 포인터와 유사
fn apply(f: fn(i32) -> i32, x: i32) -> i32 {
f(x)
}
fn double(n: i32) -> i32 { n * 2 }
let result = apply(double, 5); // 10
// 클로저를 인자로 — 제네릭 바운드 사용
fn apply_closure<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
f(x)
}
let multiplier = 3;
let result = apply_closure(|n| n * multiplier, 5); // 15
// 클로저 트레이트 3종류:
// Fn(&self) — 환경을 불변 빌림 (여러 번 호출 가능)
// FnMut(&mut self) — 환경을 가변 빌림
// FnOnce(self) — 환경을 소유 (한 번만 호출)
// 발산 함수 — 절대 반환하지 않음 (! 타입)
fn forever() -> ! {
loop { /* 무한 반복 */ }
}
// 조기 반환 (early return)
fn find_first_positive(nums: &[i32]) -> Option<i32> {
for &n in nums {
if n > 0 { return Some(n); } // 조기 반환
}
None
}
| 제어 흐름 | 특징 | 값 반환 |
|---|---|---|
if/else | 조건 분기, 표현식으로 사용 가능 | 가능 |
match | 패턴 매칭, 모든 경우 처리 강제 | 가능 |
loop | 무한 루프, break 값으로 반환 | 가능 |
while | 조건 루프 | 불가 (()) |
for | 반복자 기반 루프 | 불가 (()) |
while let | 패턴이 매치되는 동안 반복 | 불가 (()) |
클로저 캡처 모드 비교
| 트레이트 | 캡처 방식 | 호출 횟수 | 시그니처 | 예시 |
|---|---|---|---|---|
Fn | 불변 빌림 (&T) | 무제한 | &self | |x| x + captured |
FnMut | 가변 빌림 (&mut T) | 무제한 | &mut self | |x| { count += 1; x } |
FnOnce | 소유권 이동 (move) | 1번 | self | move |x| drop(owned_val) |
// move 클로저 — 환경의 소유권을 클로저로 이동
let name = String::from("Rust");
let greet = move || println!("Hello, {}", name);
// name은 이제 사용 불가 (소유권이 클로저로 이동)
greet(); // "Hello, Rust"
// 커널에서의 클로저: 콜백 등록 시 move 클로저 사용 빈번
// 예: workqueue에 작업 등록 시 필요한 데이터를 move로 전달
소유권(Ownership) 심층 해설
소유권은 Rust의 가장 독창적인 기능이자 핵심 개념입니다. GC(Garbage Collector) 없이 메모리를 안전하게 관리하는 컴파일 타임 규칙 체계입니다.
일상 비유 — 자동차 키: 소유권을 자동차 키로 생각하면 쉽습니다. 자동차(힙 메모리)에는 키(소유자)가 정확히 하나만 존재합니다. 키를 친구에게 건네면(let s2 = s1, move) 나는 더 이상 차를 운전할 수 없고, 친구가 키를 잃으면(스코프 종료) 차는 폐차됩니다(메모리 해제). 키를 복사하고 싶으면 비용을 들여 스페어 키를 만들어야 합니다(.clone()). 잠깐 빌려주는 것(&T)은 조수석에 태우는 것과 같습니다 — 보기는 하지만 운전은 못합니다.
C에서 소유권이 없으면 일어나는 일: C에는 소유권 개념이 없어서, 같은 메모리를 여러 포인터가 가리킬 수 있고 누가 free()할지 규칙이 없습니다. 결과적으로 double-free(두 번 해제), use-after-free(해제 후 사용), 메모리 누수(아무도 해제 안 함) 같은 버그가 런타임에 발생합니다. 이런 버그는 커널 취약점(CVE)의 상당 부분을 차지합니다. Rust의 소유권 시스템은 이 세 가지를 컴파일 타임에 모두 차단합니다.
/* C에서의 소유권 없는 메모리 관리 — 런타임 버그 발생 */
char *s1 = malloc(6);
strcpy(s1, "hello");
char *s2 = s1; /* 두 포인터가 같은 메모리를 가리킴 — 소유자가 불명확! */
free(s1); /* s1으로 해제 */
printf("%s\n", s2); /* ← Use-After-Free! 해제된 메모리 접근 (정의되지 않은 동작) */
free(s2); /* ← Double-Free! 같은 메모리를 두 번 해제 (보안 취약점) */
/* Rust라면: let s2 = s1; 에서 s1이 무효화되어
위 두 줄 모두 컴파일 에러 → 버그 자체가 불가능 */
소유권 3대 규칙
- Rust의 각 값에는 소유자(owner)라는 변수가 하나만 존재합니다.
- 소유자가 스코프를 벗어나면 값이 자동으로 해제(drop)됩니다.
- 소유권은 이동(move)됩니다 — 대입이나 함수 호출 시 원래 소유자는 무효화됩니다.
// 이동(Move) 의미론 — 힙 할당 타입
let s1 = String::from("hello");
let s2 = s1; // s1의 소유권이 s2로 이동
// println!("{}", s1); // ← 컴파일 에러! s1은 더 이상 유효하지 않음
println!("{}", s2); // OK — s2가 소유자
// 복사(Copy) 의미론 — 스택 타입 (i32, f64, bool, char, 고정 크기 배열 등)
let a: i32 = 42;
let b = a; // Copy! a도 여전히 유효
println!("a={}, b={}", a, b); // 둘 다 사용 가능
// 소유권 이동과 함수
fn take_ownership(s: String) {
println!("{}", s);
} // s가 스코프를 벗어남 → 자동 drop
let my_string = String::from("owned");
take_ownership(my_string); // 소유권 이동
// println!("{}", my_string); // ← 컴파일 에러!
// 소유권 반환
fn give_back(s: String) -> String {
s // 소유권을 호출자에게 반환
}
let s = String::from("return me");
let s = give_back(s); // 소유권 이동 → 반환 → 재소유
// Clone — 힙 데이터의 깊은 복사 (명시적, 비용 있음)
let s1 = String::from("deep copy");
let s2 = s1.clone(); // 힙 데이터 전체 복사
println!("s1={}, s2={}", s1, s2); // 둘 다 유효
Copy vs Clone: Copy 트레이트는 스택의 비트 복사로 구현되며, 정수·부동소수점·bool·char 같은 고정 크기 타입에 자동 적용됩니다. Clone은 명시적 깊은 복사로, 힙 할당 타입(String, Vec)에서 사용합니다. Copy를 구현하려면 Clone도 함께 구현해야 합니다.
| 소유권 동작 | 구문 | 원본 유효? | 비용 | 예시 타입 |
|---|---|---|---|---|
| Move | let b = a; | 무효 | 스택 복사만 (O(1)) | String, Vec, Box |
| Copy | let b = a; | 유효 | 비트 복사 (O(1)) | i32, f64, bool, char |
| Clone | let b = a.clone(); | 유효 | 깊은 복사 (O(n)) | String, Vec, HashMap |
| 빌림 | let b = &a; | 유효 | 포인터만 (O(1)) | 모든 타입 |
| Rc/Arc | let b = Rc::clone(&a); | 유효 | 참조 카운트 +1 | 공유 소유권 필요 시 |
초보자가 자주 하는 실수: 소유권을 처음 접하면 "이 값을 여기서도 쓰고 저기서도 쓰고 싶은데, 왜 컴파일 에러가 나지?"라는 상황을 자주 만납니다. 해결 방법을 순서대로 시도하세요: 1) 소유권 이전 대신 참조(&T)로 빌려주기 — 대부분의 경우 이것으로 충분합니다. 2) 값이 정말 필요하면 .clone()으로 복사 — 성능 비용이 있지만, 컴파일 에러를 먼저 해결하고 나중에 최적화하는 전략이 초보자에게는 더 나은 접근입니다. 3) 공유 소유가 필요하면 Rc/Arc를 사용하세요.
// Drop 트레이트 — 소유자가 스코프를 벗어날 때 자동 호출
struct Resource { name: String }
impl Drop for Resource {
fn drop(&mut self) {
println!("자원 해제: {}", self.name);
}
}
{
let r1 = Resource { name: String::from("first") };
let r2 = Resource { name: String::from("second") };
// 스코프 끝: r2 먼저 해제, 그 다음 r1 해제 (역순!)
// 출력: "자원 해제: second" → "자원 해제: first"
}
// 커널의 RAII: miscdev::Registration, MutexGuard, irq::Registration 등
// 모두 Drop을 구현하여 자동으로 자원을 해제합니다
부분 이동(Partial Move)과 소유권 분할
구조체의 필드 중 일부만 이동(move)하면, 해당 필드는 사용 불가하지만 나머지 필드는 여전히 접근 가능합니다. 이를 부분 이동(Partial Move)이라 하며, 소유권 시스템의 세밀한 추적 능력을 보여줍니다.
// ── 부분 이동 기본 예제 ──
struct Person {
name: String, // 소유 타입 — 이동 가능
age: u32, // Copy 타입 — 항상 복사
}
let person = Person {
name: String::from("Alice"),
age: 30,
};
// name 필드만 이동
let name = person.name; // name 소유권 이동
println!("나이: {}", person.age); // OK — age는 Copy이므로 여전히 사용 가능
// println!("{}", person.name); // 컴파일 에러! name은 이미 이동됨
// println!("{:?}", person); // 컴파일 에러! 구조체 전체 사용 불가
// ── 부분 이동 방지: ref 패턴 ──
let person = Person {
name: String::from("Bob"),
age: 25,
};
// ref로 빌림 — 소유권을 이동하지 않음
let Person { ref name, age } = person;
println!("이름: {}, 나이: {}", name, age); // name은 &String
println!("전체: {} {}", person.name, person.age); // OK — person 아직 유효
// ── 필요한 필드만 clone ──
let person = Person {
name: String::from("Charlie"),
age: 35,
};
let name_copy = person.name.clone(); // 복제 — 원본 유지
println!("복사: {}, 원본: {}", name_copy, person.name); // 둘 다 OK
// ── match에서의 부분 이동 ──
enum Message {
Text(String),
Data(Vec<u8>),
}
let msg = Message::Text(String::from("hello"));
match &msg { // &로 빌림 — msg 소유권 유지
Message::Text(s) => println!("텍스트: {}", s), // s는 &String
Message::Data(d) => println!("데이터: {} 바이트", d.len()),
}
// msg는 여전히 사용 가능 (빌림만 했으므로)
커널에서의 부분 이동: 커널 드라이버 구조체는 여러 자원(registration, mutex, buffer 등)을 포함합니다. 초기화 실패 시 일부 필드만 해제해야 할 때, 부분 이동 패턴과 Option::take()를 조합하여 안전하게 처리합니다. ManuallyDrop과 함께 사용하면 drop 순서를 세밀하게 제어할 수 있습니다.
String 내부 구조와 메모리 모델
String은 내부적으로 Vec<u8>의 래퍼이며, 스택에 포인터(ptr), 길이(len), 용량(capacity) 3개 필드를 저장하고 실제 UTF-8 데이터는 힙에 위치합니다. push_str 등으로 문자열이 커질 때 용량이 부족하면 재할당(reallocation)이 발생합니다.
// ── with_capacity로 재할당 최소화 ──
let mut s = String::with_capacity(100); // 100바이트 미리 할당
println!("len: {}, cap: {}", s.len(), s.capacity()); // len: 0, cap: 100
for _ in 0..10 {
s.push_str("hello!"); // 60바이트까지는 재할당 없음
}
println!("len: {}, cap: {}", s.len(), s.capacity()); // len: 60, cap: 100
// 실제 사용량에 맞게 용량 축소
s.shrink_to_fit();
println!("축소 후 cap: {}", s.capacity()); // cap: 60
// ── String과 Vec<u8>의 관계 ──
let s = String::from("hello");
let bytes: Vec<u8> = s.into_bytes(); // String → Vec<u8> (이동, 할당 없음)
let s2 = String::from_utf8(bytes).unwrap(); // Vec<u8> → String (UTF-8 검증)
// 안전하지 않은 변환 (UTF-8 검증 생략)
let bytes = vec![72, 101, 108, 108, 111];
let s3 = unsafe { String::from_utf8_unchecked(bytes) }; // "Hello"
// 주의: 잘못된 UTF-8 바이트를 넣으면 정의되지 않은 동작(UB)!
상각 분석(Amortized Analysis): push_str이 재할당을 유발하면 O(n) 비용이 들지만, 용량을 2배로 늘리기 때문에 n번의 push 전체를 평균하면 push 당 O(1)입니다. 이는 Vec, HashMap 등 동적 자료구조에서 공통적으로 사용되는 전략입니다. 성능이 중요한 커널 코드에서는 with_capacity()로 초기 용량을 충분히 잡아 재할당을 아예 방지하는 것이 좋습니다.
빌림(Borrowing)과 참조
소유권을 이전하지 않고 데이터에 접근하는 방법이 빌림(Borrowing)입니다. 참조(&)를 통해 소유권 없이 데이터를 읽거나 수정할 수 있습니다.
일상 비유 — 도서관 규칙: 빌림 규칙을 도서관 열람실로 이해하면 쉽습니다. 공유 참조(&T)는 여러 사람이 같은 책을 동시에 열람하는 것입니다 — 읽기만 하므로 아무 문제 없습니다. 가변 참조(&mut T)는 누군가가 책에 밑줄을 긋거나 수정하는 것입니다 — 이때는 그 사람만 책을 가져야 합니다. 누군가 읽고 있는 동안 다른 사람이 수정하면 읽는 사람이 혼란스러워지니, 도서관은 이를 동시에 허용하지 않습니다. Rust 컴파일러가 바로 이 도서관 사서 역할을 합니다.
C 포인터와의 차이: C에서 int *p = &x;는 p를 통해 아무 제약 없이 읽고 쓸 수 있고, 같은 메모리를 가리키는 다른 포인터와 동시에 수정해도 컴파일러가 경고하지 않습니다. 이것이 멀티스레드 환경에서 데이터 레이스(data race)를 일으킵니다. Rust의 빌림 규칙은 "동시에 여러 읽기 또는 단독 쓰기" 원칙을 컴파일 타임에 강제하여, 데이터 레이스를 원천 차단합니다. 커널처럼 동시성이 핵심인 소프트웨어에서 이 보장은 매우 강력합니다.
// 공유 참조 (immutable borrow)
fn calculate_length(s: &String) -> usize {
s.len() // 읽기만 가능, 수정 불가
}
let s = String::from("hello");
let len = calculate_length(&s); // 참조 전달 — 소유권 유지
println!("'{}' length = {}", s, len); // s 여전히 유효
// 가변 참조 (mutable borrow)
fn append_world(s: &mut String) {
s.push_str(", world");
}
let mut s = String::from("hello");
append_world(&mut s); // 가변 참조 전달
println!("{}", s); // "hello, world"
// 빌림 규칙 위반 예시
let mut data = vec![1, 2, 3];
let r1 = &data; // 공유 참조 1
let r2 = &data; // 공유 참조 2 — OK
println!("{:?} {:?}", r1, r2); // 여기서 r1, r2 마지막 사용
let w = &mut data; // 가변 참조 — NLL 덕분에 OK (r1, r2 이미 사용 끝)
w.push(4);
// 댕글링 참조 방지
// fn dangle() -> &String {
// let s = String::from("hello");
// &s // ← 컴파일 에러! s가 함수 끝에서 해제되므로 댕글링 참조
// }
NLL (Non-Lexical Lifetimes): Rust 2018 Edition부터 참조의 수명이 렉시컬 스코프가 아닌 마지막 사용 지점까지로 단축되었습니다. 이 덕분에 이전에는 불가능했던 많은 빌림 패턴이 허용됩니다.
// NLL 이전에는 불가능했던 패턴 — 이제 가능
let mut map = HashMap::new();
map.insert("key", 1);
match map.get("key") { // &map 빌림 시작
Some(val) => println!("found: {}", val), // &map 빌림 끝 (NLL)
None => {
map.insert("key", 2); // &mut map — NLL 덕분에 OK
}
}
// 빌림 vs 소유권 이전 — 언제 무엇을 쓰나?
// 읽기만 필요: fn foo(data: &Vec<i32>) — 공유 참조
// 수정 필요: fn foo(data: &mut Vec<i32>) — 가변 참조
// 소유권 넘기기: fn foo(data: Vec<i32>) — 소유권 이전 (move)
// 복제: fn foo(data: Vec<i32>) — clone() 후 전달
// 슬라이스 빌림 — 컬렉션의 일부를 안전하게 참조
let v = vec![1, 2, 3, 4, 5];
let slice: &[i32] = &v[1..4]; // [2, 3, 4] — 복사 없이 참조
// 커널에서의 빌림: ArcBorrow — Arc 복제 없이 참조
// fn open(shared: &Arc<DeviceState>, file: &File) → Arc를 빌려서 사용
// fn read(shared: &Arc<DeviceState>, ...) → 참조 카운트 증가 없이 접근
재빌림(Reborrowing)
가변 참조(&mut T)는 동시에 하나만 존재할 수 있다는 규칙이 있지만, 재빌림(reborrowing) 덕분에 &mut T를 받는 함수에 기존 가변 참조를 전달할 수 있습니다. 컴파일러가 암묵적으로 원본 참조를 일시 정지하고 새로운 빌림을 생성하기 때문입니다.
// ── 재빌림(Reborrowing) 기본 ──
fn add_one(val: &mut i32) {
*val += 1;
}
let mut x = 10;
let r = &mut x;
// 재빌림: r을 "일시적으로" 다시 빌려서 add_one에 전달
add_one(r); // 컴파일러가 암묵적으로 &mut *r 수행
add_one(r); // 또 다시 재빌림 — OK!
println!("{}", r); // 12 — r은 여전히 유효
// 명시적으로 쓰면 이렇습니다:
add_one(&mut *r); // 이것이 컴파일러가 내부적으로 하는 것
// ── 왜 이동이 아닌 재빌림인가? ──
fn process(data: &mut Vec<i32>) {
data.push(42);
}
let mut v = vec![1, 2, 3];
let r = &mut v;
// 만약 재빌림이 없다면, r의 소유권이 process로 이동하여
// 아래 두 번째 호출은 불가능했을 것입니다.
process(r); // r → 재빌림 → process 완료 → r 다시 활성
process(r); // OK — r은 소비되지 않았음
r.push(100); // r 여전히 사용 가능
// 주의: &mut를 소유권처럼 이동시키면 재빌림 불가
fn take_ref(data: &mut Vec<i32>) -> &mut Vec<i32> {
data // 반환 시 수명이 함수 시그니처에 묶임
}
// 재빌림은 공유 참조(&T)에서도 동작합니다.
// &T는 Copy이므로 항상 재빌림 없이도 복사 가능하지만,
// &mut T는 Copy가 아니므로 재빌림이 필수적입니다.
재빌림의 원리: 컴파일러는 &mut T를 함수에 전달할 때 원본 참조를 일시 중지(suspend)하고, 함수가 반환되면 다시 활성화(resume)합니다. 이는 "동시에 하나의 가변 참조만 존재" 규칙을 위반하지 않으면서도, 참조를 여러 함수에 순차적으로 전달할 수 있게 합니다. NLL(Non-Lexical Lifetimes) 덕분에 이 분석이 더욱 정밀해졌습니다.
빌림 검사기 에러 패턴과 해결책
Rust의 빌림 검사기(borrow checker)는 메모리 안전성을 보장하지만, 처음 접하면 에러 메시지가 난해할 수 있습니다. 아래는 가장 흔한 에러 패턴과 그 해결 방법입니다.
| 에러 메시지 | 원인 | 해결 방법 |
|---|---|---|
cannot borrow as mutable because it is also borrowed as immutable |
공유 참조와 가변 참조가 동시 존재 | 공유 참조 사용 완료 후 가변 빌림, 또는 스코프 분리 |
cannot move out of borrowed content |
빌린 데이터에서 소유권 이동 시도 | clone(), ref 패턴, 또는 std::mem::replace |
returns a reference to data owned by the current function |
지역 변수의 참조를 반환 | 소유 타입(String, Vec 등)을 반환 |
value used after being moved |
이동된 값을 다시 사용 | clone() 또는 참조로 전달 |
temporary value dropped while borrowed |
임시값의 참조가 임시값보다 오래 살아남 | 임시값을 변수에 바인딩하여 수명 연장 |
// ── 에러 1: 공유 + 가변 참조 충돌 ──
// 문제 코드
let mut v = vec![1, 2, 3];
let first = &v[0]; // 공유 참조 생성
v.push(4); // 에러! v를 가변으로 빌리려 하지만 first가 아직 살아있음
// println!("{}", first); // first가 여기서 사용되므로 push 시점에 공존
// 해결: 공유 참조 사용을 먼저 완료
let mut v = vec![1, 2, 3];
let first = v[0]; // i32는 Copy — 값 복사, 빌림 아님
v.push(4); // OK!
println!("{}", first); // 1 — 복사된 값
// 또는 스코프 분리
let mut v = vec![1, 2, 3];
{
let first = &v[0];
println!("{}", first); // 여기서 first 사용 완료
} // first의 빌림이 끝남
v.push(4); // OK — 더 이상 공유 참조 없음
// ── 에러 2: 빌린 데이터에서 이동 시도 ──
struct Container {
data: String,
}
// 문제 코드
fn extract(c: &Container) -> String {
c.data // 에러! cannot move out of `c.data` which is behind a shared reference
}
// 해결 1: clone
fn extract_v1(c: &Container) -> String {
c.data.clone() // 복제하여 새 String 반환
}
// 해결 2: 참조 반환
fn extract_v2(c: &Container) -> &str {
&c.data // 빌림으로 반환 — 복사 비용 없음
}
// 해결 3: 소유권을 받아서 이동 (함수가 소유권 소비)
fn extract_v3(c: Container) -> String {
c.data // OK — c의 소유권을 받았으므로 필드 이동 가능
}
// ── 에러 3: 지역 변수 참조 반환 ──
// 문제 코드
fn make_greeting() -> &str {
let s = String::from("hello");
&s // 에러! s는 함수 끝에서 해제되므로 참조가 댕글링 포인터가 됨
}
// 해결: 소유 타입 반환
fn make_greeting_v1() -> String {
String::from("hello") // 소유권을 호출자에게 이전
}
// 또는 정적 문자열 반환 (리터럴의 수명은 'static)
fn make_greeting_v2() -> &'static str {
"hello" // 문자열 리터럴은 프로그램 전체 수명
}
// ── 에러 4: 임시값 참조 ──
// 문제 코드
let r: &str = &String::from("temp");
// 에러! 임시 String이 이 줄 끝에서 해제되지만 r이 참조 중
// 해결: 임시값을 변수에 바인딩
let owned = String::from("temp");
let r: &str = &owned; // OK — owned가 r보다 오래 살아남
// 커널에서 흔한 패턴: Guard를 변수에 바인딩
// let guard = mutex.lock(); // guard를 변수에 저장해야 락이 유지됨
// let _ = mutex.lock(); // 즉시 해제! 보호가 되지 않음
NLL(Non-Lexical Lifetimes): Rust 2021 에디션부터 빌림의 수명은 마지막 사용 지점에서 끝납니다(렉시컬 스코프가 아님). 즉, let r = &v[0]; println!("{}", r); v.push(4);는 OK입니다 — r의 수명이 println! 이후 끝나므로 push 시점에는 충돌하지 않습니다. 이전 Rust에서는 r이 스코프 끝까지 살아있다고 판단하여 에러였습니다.
수명(Lifetime)
수명(Lifetime)은 참조가 유효한 범위를 나타내는 컴파일 타임 표기입니다. 대부분의 경우 컴파일러가 자동으로 추론(elision)하지만, 여러 참조 간의 관계가 모호할 때 명시적으로 지정해야 합니다.
초보자 안심 메시지: 수명은 Rust에서 가장 어렵다고 느끼는 개념이지만, 실제로 코드를 작성할 때 90% 이상의 경우 수명을 명시할 필요가 없습니다. 컴파일러의 수명 생략 규칙(Elision Rules)이 자동으로 처리합니다. 수명 표기('a)가 필요한 상황은 주로: (1) 함수가 여러 참조를 받아 참조를 반환할 때, (2) 구조체가 참조를 필드로 가질 때입니다. 처음에는 "컴파일러가 요구하면 그때 추가하면 된다"는 실용적 접근이 좋습니다.
C에서의 댕글링 포인터 vs Rust 수명: C에서 가장 위험한 버그 중 하나는 댕글링 포인터(dangling pointer)입니다 — 이미 해제된 메모리를 가리키는 포인터를 사용하는 것입니다. C 컴파일러는 이를 전혀 감지하지 못하고, 런타임에 정의되지 않은 동작(segfault, 데이터 손상, 보안 취약점)으로 나타납니다. Rust의 수명 시스템은 "이 참조가 가리키는 데이터가 참조보다 먼저 사라지는가?"를 컴파일 타임에 검사하여, 댕글링 참조를 원천적으로 불가능하게 만듭니다.
// 수명 매개변수 — 두 참조 중 짧은 수명을 반환 타입에 적용
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
// 'a는 x와 y의 수명 중 짧은 쪽으로 결정됨
// 수명 생략 규칙 (Lifetime Elision Rules)
// 규칙 1: 각 참조 매개변수에 고유 수명 부여
// 규칙 2: 입력 수명이 하나면 출력에 동일 수명 적용
// 규칙 3: &self/&mut self가 있으면 self의 수명 적용
// 생략 전
fn first_word<'a>(s: &'a str) -> &'a str { /* ... */ }
// 생략 후 (규칙 1+2 적용 → 동일한 의미)
fn first_word(s: &str) -> &str { /* ... */ }
// 구조체에서의 수명 — 구조체가 참조를 보유할 때 필수
struct Important<'a> {
part: &'a str, // 이 구조체는 참조의 수명보다 오래 살 수 없음
}
impl<'a> Important<'a> {
fn level(&self) -> i32 {
3 // 규칙 3: &self의 수명이 반환 참조에 적용
}
}
// 'static 수명 — 프로그램 전체 기간 유효
let s: &'static str = "I live forever"; // 문자열 리터럴
수명 생략 규칙 (Lifetime Elision Rules)
Rust 컴파일러는 다음 3가지 규칙을 순서대로 적용하여 수명을 자동 추론합니다. 규칙 적용 후에도 모호하면 명시적 수명이 필요합니다.
| 규칙 | 적용 조건 | 동작 | 예시 |
|---|---|---|---|
| 규칙 1 | 항상 | 각 참조 매개변수에 고유 수명 부여 | fn f(a: &i32, b: &i32) → fn f<'a,'b>(a: &'a i32, b: &'b i32) |
| 규칙 2 | 입력 수명이 정확히 1개 | 그 수명을 모든 출력 참조에 적용 | fn f(s: &str) -> &str → fn f<'a>(s: &'a str) -> &'a str |
| 규칙 3 | 메서드(&self/&mut self) | self의 수명을 출력에 적용 | fn name(&self) -> &str → fn name<'a>(&'a self) -> &'a str |
// === 복잡한 수명 시나리오 ===
// 여러 수명 매개변수 — 입력이 2개 이상이면 규칙2 적용 불가 → 명시 필요
fn select<'a, 'b>(first: &'a str, second: &'b str, use_first: bool) -> &'a str {
if use_first { first } else {
// second를 반환하려면 'b: 'a (second가 first보다 오래 살아야 함)
// 여기서는 first만 반환 → 'a만 출력에 사용
first
}
}
// 수명 바운드 — 'b는 최소 'a만큼 살아야 함
fn longest_with_bound<'a, 'b: 'a>(x: &'a str, y: &'b str) -> &'a str {
if x.len() > y.len() { x } else { y } // 'b: 'a이므로 y를 'a로 반환 가능
}
// 트레이트 객체의 수명
trait Processor {
fn process(&self, data: &[u8]) -> Result<(), Error>;
}
// Box<dyn Processor + 'a> → 트레이트 객체도 수명을 가짐
// Box<dyn Processor>는 Box<dyn Processor + 'static>과 동일
fn create_processor<'a>(config: &'a Config) -> Box<dyn Processor + 'a> {
// 반환된 트레이트 객체는 config의 수명 내에서만 유효
Box::new(ConfigProcessor { config })
}
// 'static 수명의 세 가지 의미
// 1. 문자열 리터럴: 바이너리에 포함 → 프로그램 전체 수명
let s: &'static str = "hello";
// 2. 소유된 타입: String, Vec 등은 T: 'static을 만족 (참조를 포함하지 않으므로)
fn spawn_thread<F: FnOnce() + Send + 'static>(f: F) { /* ... */ }
// 3. const 프로모션: 컴파일 타임 상수는 암묵적 'static
커널에서의 수명: 커널 Rust에서 수명은 특히 중요합니다. ArcBorrow<'_, T>는 수명으로 Arc의 참조가 원본보다 오래 살지 않음을 보장하고, 콜백 함수에 전달하는 참조의 수명은 콜백의 실행 기간과 맞아야 합니다. C에서 프로그래머가 수동으로 관리하던 "이 포인터는 언제까지 유효한가?"를 컴파일러가 검증합니다.
구조체(Struct)와 열거형(Enum)
구조체와 열거형은 Rust의 기본 데이터 정의 도구입니다. 구조체는 관련 데이터를 묶고, 열거형은 가능한 변형(variant)을 나열합니다.
C 개발자를 위한 비교: Rust의 struct는 C의 struct와 매우 유사하지만, 메서드를 impl 블록으로 직접 정의할 수 있습니다 (C에서는 함수 포인터를 구조체에 넣는 패턴을 사용). Rust의 enum은 C의 enum과 완전히 다릅니다 — C의 enum은 단순한 정수 상수이지만, Rust의 enum은 각 변형(variant)이 서로 다른 타입의 데이터를 가질 수 있는 Tagged Union입니다. C에서 union + enum 판별자를 수동으로 조합하는 패턴을, Rust는 언어 차원에서 안전하게 지원합니다.
Rust에는 null이 없습니다: C에서 NULL 포인터는 "값이 없음"을 표현하지만, 역참조하면 segfault가 발생합니다. Rust는 null 대신 Option<T> 열거형을 사용합니다. Some(값)은 값이 있음을, None은 없음을 나타내며, match나 if let으로 반드시 두 경우를 모두 처리해야 컴파일이 됩니다. 이로써 null 참조 버그가 원천 차단됩니다.
// 일반 구조체
struct Process {
pid: u32,
name: String,
state: ProcessState,
priority: i8,
}
// 튜플 구조체 — 필드명 없이 순서로 접근
struct Color(u8, u8, u8);
struct Pid(u32); // 뉴타입 패턴 — 타입 안전성 강화
// 유닛 구조체 — 필드 없음 (트레이트 구현용)
struct AlwaysEqual;
// 열거형(Enum) — 각 변형이 다른 데이터를 가질 수 있음
enum ProcessState {
Running, // 데이터 없음
Sleeping { duration_ms: u64 }, // 명명된 필드
Stopped(i32), // 튜플 변형
Zombie,
}
// impl 블록 — 메서드와 연관 함수 정의
impl Process {
// 연관 함수 (생성자 패턴) — Self::new()
fn new(pid: u32, name: String) -> Self {
Process {
pid,
name,
state: ProcessState::Running,
priority: 0,
}
}
// 메서드 — &self로 불변 접근
fn is_running(&self) -> bool {
matches!(self.state, ProcessState::Running)
}
// 가변 메서드 — &mut self
fn set_priority(&mut self, prio: i8) {
self.priority = prio;
}
}
// 사용
let mut proc = Process::new(1, String::from("init"));
proc.set_priority(-20);
// Option<T> — Rust의 null 대체
let some_val: Option<i32> = Some(42);
let no_val: Option<i32> = None;
// unwrap, expect, match, if let, map 등으로 안전하게 접근
// Option 메서드 체이닝
let result = some_val
.map(|x| x * 2) // Some(84)
.filter(|&x| x > 50) // Some(84)
.unwrap_or(0); // 84
// Result<T, E> vs Option<T>
// Option: 값이 있거나(Some) 없음(None)
// Result: 성공(Ok) 또는 에러 정보 포함(Err)
| 구조체 종류 | 문법 | 용도 | 예시 |
|---|---|---|---|
| 일반 구조체 | struct Foo { a: T, b: U } | 이름 있는 필드 | Process { pid, name } |
| 튜플 구조체 | struct Bar(T, U) | 순서 기반 접근 | Color(255, 0, 0) |
| 뉴타입 | struct Pid(u32) | 타입 구분 강제 | Pid ≠ u32 |
| 유닛 구조체 | struct Unit; | 트레이트 마커 | struct Locked; |
자주 사용하는 derive 매크로
| derive | 생성하는 트레이트 | 용도 | 조건 |
|---|---|---|---|
#[derive(Debug)] | fmt::Debug | {:?} 포맷 출력 | 모든 필드가 Debug 구현 |
#[derive(Clone)] | Clone | .clone()으로 깊은 복사 | 모든 필드가 Clone 구현 |
#[derive(Copy, Clone)] | Copy | 대입 시 자동 복사 (이동 대신) | 모든 필드가 Copy (스택만) |
#[derive(PartialEq, Eq)] | PartialEq, Eq | ==, != 비교 | 모든 필드가 PartialEq |
#[derive(Hash)] | Hash | HashMap/HashSet 키로 사용 | 모든 필드가 Hash |
#[derive(Default)] | Default | T::default() 기본값 생성 | 모든 필드가 Default |
#[derive(PartialOrd, Ord)] | PartialOrd, Ord | <, > 비교, 정렬 | 모든 필드가 PartialOrd |
// 열거형 크기 최적화 예시
use std::mem;
// 각 변형의 크기가 다르면, 가장 큰 변형 + 판별자 크기
enum Message {
Quit, // 0 바이트
Move { x: i32, y: i32 }, // 8 바이트
Write(String), // 24 바이트 (가장 큼)
Color(u8, u8, u8), // 3 바이트
}
// Message 크기 = max(0, 8, 24, 3) + 판별자(8) = 32 바이트
// → 큰 변형은 Box로 감싸면 전체 enum 크기 감소
enum OptimizedMessage {
Quit,
Move { x: i32, y: i32 },
Write(Box<String>), // 8 바이트 (포인터만)
Color(u8, u8, u8),
}
// OptimizedMessage = max(0, 8, 8, 3) + 판별자 = 16 바이트!
Rust의 enum vs C의 enum: C의 enum은 단순한 정수 상수지만, Rust의 enum은 각 변형마다 다른 타입의 데이터를 가질 수 있습니다(tagged union). Option<T>과 Result<T, E>도 enum입니다. 이 차이가 Rust에서 null pointer와 에러 처리 누락을 컴파일 타임에 방지하는 핵심 메커니즘입니다. 커널에서는 #[repr(C)] enum으로 C 측 상수와 호환되는 열거형을 정의합니다.
구조체 갱신 문법 (Struct Update Syntax)
Rust에서는 .. 구문(struct update syntax)을 사용하여 기존 구조체의 나머지 필드를 복사할 수 있습니다. 이 문법은 일부 필드만 변경하고 나머지는 그대로 유지할 때 매우 유용합니다.
#[derive(Debug, Clone)]
struct Config {
width: u32,
height: u32,
title: String,
fullscreen: bool,
vsync: bool,
fps_limit: u32,
}
impl Config {
fn default() -> Self {
Config {
width: 1920,
height: 1080,
title: String::from("My App"),
fullscreen: false,
vsync: true,
fps_limit: 60,
}
}
}
fn main() {
let default_config = Config::default();
// 일부 필드만 변경하고 나머지는 default_config에서 복사
let custom = Config {
width: 2560,
height: 1440,
fullscreen: true,
..default_config.clone() // 나머지 필드 복사
};
// ⚠️ 주의: String 필드가 있으면 move가 발생!
let base = Config::default();
let moved = Config {
width: 3840,
..base // base.title이 move됨 → base 더 이상 사용 불가
};
// println!("{:?}", base); // ❌ 컴파일 에러: base가 부분적으로 move됨
// 하지만 Copy 타입 필드는 개별 접근 가능:
// println!("{}", base.width); // ✅ u32는 Copy이므로 OK
}
부분 이동(partial move)에 주의: ..other 갱신 문법에서 String, Vec, Box 등 Copy 트레이트를 구현하지 않는 필드가 포함되면, 해당 필드가 move됩니다. move 후에는 원본 구조체 전체를 사용할 수 없지만, Copy 타입인 개별 필드(u32, bool 등)는 여전히 접근 가능합니다. 이를 피하려면 .clone()을 사용하세요.
빌더 패턴(Builder Pattern)은 복잡한 구조체를 단계별로 구성할 때 사용하는 Rust의 관용적 설계 패턴입니다. 선택적 필드가 많거나 유효성 검증이 필요할 때 특히 유용합니다.
struct Request {
url: String,
method: String,
headers: Vec<(String, String)>,
body: Option<Vec<u8>>,
timeout_ms: u64,
}
struct RequestBuilder {
url: String,
method: String,
headers: Vec<(String, String)>,
body: Option<Vec<u8>>,
timeout_ms: u64,
}
impl RequestBuilder {
// 필수 필드만 받는 생성자
fn new(url: &str) -> Self {
RequestBuilder {
url: url.to_string(),
method: String::from("GET"),
headers: Vec::new(),
body: None,
timeout_ms: 5000,
}
}
// 각 설정 메서드는 self를 소비하고 반환 → 체이닝 가능
fn method(mut self, method: &str) -> Self {
self.method = method.to_string();
self
}
fn header(mut self, key: &str, value: &str) -> Self {
self.headers.push((key.to_string(), value.to_string()));
self
}
fn body(mut self, data: Vec<u8>) -> Self {
self.body = Some(data);
self
}
fn timeout(mut self, ms: u64) -> Self {
self.timeout_ms = ms;
self
}
// 최종 빌드 - 유효성 검증 후 실제 타입 반환
fn build(self) -> Result<Request, String> {
if self.url.is_empty() {
return Err("URL은 비어있을 수 없습니다".to_string());
}
Ok(Request {
url: self.url,
method: self.method,
headers: self.headers,
body: self.body,
timeout_ms: self.timeout_ms,
})
}
}
// 사용 예시 - 메서드 체이닝으로 가독성 있게 구성
let request = RequestBuilder::new("https://api.example.com/data")
.method("POST")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer token123")
.body(b"{\"key\": \"value\"}".to_vec())
.timeout(10000)
.build()
.expect("유효한 요청");
빌더 패턴의 두 가지 스타일: 위 예시처럼 mut self를 받아 자신을 반환하는 consuming builder와, &mut self를 받아 참조를 반환하는 borrowing builder가 있습니다. consuming builder는 타입 상태 패턴(typestate)과 결합하여 컴파일 타임에 필수 필드 누락을 방지할 수 있습니다. 커널에서는 kernel::device::Builder가 이 패턴을 사용합니다.
Display와 Debug 트레이트 구현
Rust에서 타입을 출력하려면 fmt::Display(사용자 친화적 출력)와 fmt::Debug(개발자 디버깅용 출력) 트레이트를 구현해야 합니다. 두 트레이트의 용도와 구현 방식을 비교합니다.
use std::fmt;
struct IpAddr {
octets: [u8; 4],
}
// Display: 사용자에게 보여줄 형태 (println!("{}", x))
impl fmt::Display for IpAddr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// 점으로 구분된 사람이 읽기 쉬운 형태
write!(f, "{}.{}.{}.{}",
self.octets[0], self.octets[1],
self.octets[2], self.octets[3])
}
}
// Debug: 디버깅용 상세 형태 (println!("{:?}", x))
impl fmt::Debug for IpAddr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// 구조체 이름과 필드를 포함한 상세 형태
f.debug_struct("IpAddr")
.field("octets", &self.octets)
.finish()
}
}
fn main() {
let addr = IpAddr { octets: [192, 168, 1, 100] };
// Display: 사용자 친화적
println!("서버 주소: {}", addr);
// 출력: 서버 주소: 192.168.1.100
// Debug: 개발자용
println!("디버그: {:?}", addr);
// 출력: 디버그: IpAddr { octets: [192, 168, 1, 100] }
// Pretty Debug: 들여쓰기된 디버그 출력
println!("상세:\n{:#?}", addr);
// 출력:
// 상세:
// IpAddr {
// octets: [
// 192,
// 168,
// 1,
// 100,
// ],
// }
}
#[derive(Debug)]를 사용하면 자동으로 Debug 구현을 생성할 수 있지만, Display는 항상 수동 구현이 필요합니다.
// #[derive(Debug)]로 자동 구현 vs 수동 구현 비교
#[derive(Debug)] // 자동 생성: 필드를 그대로 출력
struct AutoDebug {
name: String,
values: Vec<i32>,
active: bool,
}
// {:?} → AutoDebug { name: "test", values: [1, 2, 3], active: true }
// 수동 구현: 민감 정보 숨기기, 커스텀 포맷
struct User {
name: String,
email: String,
password_hash: String, // 보안상 숨겨야 함
}
impl fmt::Debug for User {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("User")
.field("name", &self.name)
.field("email", &self.email)
.field("password_hash", &"[REDACTED]") // 비밀번호 숨김
.finish()
}
}
impl fmt::Display for User {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} <{}>", self.name, self.email)
}
}
Display vs Debug 사용 지침:
{}(Display): 최종 사용자에게 보여줄 메시지, 로그,.to_string()변환에 사용. Display를 구현하면ToString트레이트도 자동 구현됩니다.{:?}(Debug): 개발 중 디버깅,assert_eq!실패 메시지, 테스트 출력에 사용.#[derive(Debug)]로 쉽게 생성 가능.{:#?}(Pretty Debug): 복잡한 중첩 구조체를 들여쓰기하여 읽기 쉽게 출력.- 커널에서는
pr_info!매크로가 Display를,pr_debug!가 Debug를 사용합니다.
열거형 메서드와 impl
구조체처럼 열거형에도 impl 블록을 사용하여 메서드와 연관 함수를 정의할 수 있습니다. 이를 통해 각 변형(variant)에 대한 유틸리티 메서드를 제공하고, 타입 변환을 구현할 수 있습니다.
#[derive(Debug, Clone, PartialEq)]
enum HttpStatus {
Ok, // 200
NotFound, // 404
InternalError(String), // 500 + 메시지
Redirect { url: String, permanent: bool },
}
impl HttpStatus {
// 상태 코드 반환
fn code(&self) -> u16 {
match self {
HttpStatus::Ok => 200,
HttpStatus::NotFound => 404,
HttpStatus::InternalError(_) => 500,
HttpStatus::Redirect { permanent: true, .. } => 301,
HttpStatus::Redirect { permanent: false, .. } => 302,
}
}
// is_* 헬퍼 메서드 패턴 — 변형 판별에 자주 사용
fn is_success(&self) -> bool {
matches!(self, HttpStatus::Ok)
}
fn is_error(&self) -> bool {
matches!(self, HttpStatus::NotFound | HttpStatus::InternalError(_))
}
fn is_redirect(&self) -> bool {
matches!(self, HttpStatus::Redirect { .. })
}
// 에러 메시지 추출 (해당 변형일 때만 Some 반환)
fn error_message(&self) -> Option<&str> {
match self {
HttpStatus::InternalError(msg) => Some(msg),
_ => None,
}
}
// 연관 함수: 상태 코드 숫자에서 생성
fn from_code(code: u16) -> Option<Self> {
match code {
200 => Some(HttpStatus::Ok),
404 => Some(HttpStatus::NotFound),
500 => Some(HttpStatus::InternalError(String::from("Internal Server Error"))),
_ => None,
}
}
}
// From/Into 트레이트로 타입 변환 구현
impl From<u16> for HttpStatus {
fn from(code: u16) -> Self {
match code {
200 => HttpStatus::Ok,
404 => HttpStatus::NotFound,
301 => HttpStatus::Redirect {
url: String::new(),
permanent: true,
},
_ => HttpStatus::InternalError(
format!("알 수 없는 상태 코드: {}", code)
),
}
}
}
// Display 구현으로 사용자 친화적 출력
impl std::fmt::Display for HttpStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HttpStatus::Ok => write!(f, "200 OK"),
HttpStatus::NotFound => write!(f, "404 Not Found"),
HttpStatus::InternalError(msg) => write!(f, "500 {}", msg),
HttpStatus::Redirect { url, permanent } => {
let code = if *permanent { 301 } else { 302 };
write!(f, "{} Redirect → {}", code, url)
}
}
}
}
fn main() {
let status = HttpStatus::InternalError("DB 연결 실패".to_string());
// is_* 헬퍼로 간결한 조건 분기
if status.is_error() {
println!("에러 발생: {} (코드: {})", status, status.code());
if let Some(msg) = status.error_message() {
println!("상세: {}", msg);
}
}
// From 트레이트를 통한 변환
let from_code: HttpStatus = 404.into(); // Into는 From 구현 시 자동 제공
assert!(from_code.is_error());
}
matches! 매크로의 활용: is_* 헬퍼 메서드에서 matches! 매크로를 사용하면, match 전체를 작성하지 않고도 패턴 매칭 결과를 bool로 간결하게 얻을 수 있습니다. matches!(self, Pattern)은 match self { Pattern => true, _ => false }와 동일합니다. 가드 절(if)도 사용 가능합니다: matches!(x, Some(v) if v > 10).
패턴 매칭 (match, if let, while let)
Rust의 패턴 매칭은 완전성(exhaustiveness)을 보장합니다. match에서 모든 가능한 경우를 처리하지 않으면 컴파일 에러가 발생합니다. C의 switch문과 달리 fall-through가 없고, 구조 분해(destructuring)를 통해 복합 데이터를 우아하게 분석할 수 있습니다.
C의 switch와 결정적 차이 3가지:
1) Fall-through 없음: C의 switch에서 break를 빠뜨리면 다음 case로 넘어가는 버그가 흔하지만, Rust의 match는 매칭된 arm만 실행합니다.
2) 완전성 검사: C의 switch에서 default를 빠뜨려도 경고뿐이지만, Rust의 match는 모든 가능한 값을 처리하지 않으면 컴파일 에러가 발생합니다. enum에 새 변형을 추가하면 모든 match 문에서 처리를 강제하므로, 누락이 불가능합니다.
3) 구조 분해: C의 switch는 정수 비교만 가능하지만, Rust의 match는 튜플, 구조체, 열거형 내부 데이터를 분해하여 추출할 수 있습니다.
| 패턴 종류 | 문법 | 설명 | 예시 |
|---|---|---|---|
| 리터럴 | 1, "hello", true | 정확한 값 매칭 | match x { 1 => ... } |
| 변수 바인딩 | n, name | 모든 값을 변수에 바인딩 | match x { n => use(n) } |
| 와일드카드 | _ | 모든 값 무시 | match x { _ => default() } |
| 범위 | 1..=5 | 범위 내 값 매칭 | match x { 1..=5 => ... } |
| OR | a | b | 여러 패턴 중 하나 | match x { 1 | 2 => ... } |
| 튜플 분해 | (a, b) | 튜플 요소 추출 | let (x, y) = point; |
| 구조체 분해 | Struct { field, .. } | 필드 추출 (나머지 무시) | let Point { x, .. } = p; |
| 열거형 분해 | Enum::Variant(v) | 열거형 variant 내부값 추출 | Some(val) => use(val) |
| 참조 | &val | 참조 내부값 매칭 | match &x { &0 => ... } |
| @ 바인딩 | name @ pattern | 매칭하면서 전체를 바인딩 | n @ 1..=5 => use(n) |
| 매치 가드 | pat if cond | 추가 조건 검사 | n if n > 0 => ... |
// match — 완전한 패턴 매칭
fn describe_state(state: &ProcessState) -> &str {
match state {
ProcessState::Running => "실행 중",
ProcessState::Sleeping { duration_ms } => {
if *duration_ms > 10000 { "깊은 수면" } else { "수면 중" }
}
ProcessState::Stopped(code) => "정지됨",
ProcessState::Zombie => "좀비",
}
}
// 매치 가드 (match guard)
let num = 4;
match num {
n if n < 0 => println!("음수"),
0 => println!("영"),
n if n % 2 == 0 => println!("양의 짝수: {}", n),
_ => println!("양의 홀수"),
}
// if let — 하나의 패턴만 관심 있을 때
let config_max: Option<u8> = Some(3);
if let Some(max) = config_max {
println!("max = {}", max);
}
// let-else (Rust 1.65+) — 매칭 실패 시 조기 반환
fn process_value(opt: Option<i32>) -> i32 {
let Some(val) = opt else {
return -1; // None인 경우 조기 반환
};
val * 2
}
// 구조 분해 패턴
let point = (3, 5);
match point {
(0, 0) => println!("원점"),
(x, 0) => println!("x축 위: {}", x),
(0, y) => println!("y축 위: {}", y),
(x, y) => println!("({}, {})", x, y),
}
// @ 바인딩 — 패턴 매칭하면서 값을 변수에 바인딩
match num {
n @ 1..=12 => println!("1~12 범위: {}", n),
n @ 13..=19 => println!("13~19 범위: {}", n),
_ => println!("범위 밖"),
}
// while let — 패턴이 매칭하는 동안 반복
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
println!("꺼냄: {}", top); // 3, 2, 1 순서로 출력
}
// 중첩 구조체 분해
struct Rect { origin: (i32, i32), size: (u32, u32) }
let rect = Rect { origin: (10, 20), size: (100, 50) };
let Rect { origin: (x, y), size: (w, h) } = rect;
println!("x={}, y={}, w={}, h={}", x, y, w, h);
// 슬라이스 패턴 (Rust 1.26+)
let nums = [1, 2, 3, 4, 5];
match &nums[..] {
[] => println!("비어 있음"),
[single] => println!("하나: {}", single),
[first, .., last] => println!("첫={}, 끝={}", first, last),
}
// matches! 매크로 — 패턴 매칭 결과를 bool로
let is_letter = matches!('a', 'a'..='z' | 'A'..='Z'); // true
// 커널 코드에서의 패턴 매칭 활용
fn handle_ioctl(cmd: u32, arg: usize) -> Result {
match cmd {
IOCTL_GET_INFO => get_device_info(arg),
IOCTL_SET_CONFIG => set_device_config(arg),
IOCTL_RESET => reset_device(),
_ => Err(ENOTTY), // 지원하지 않는 명령
}
}
반박 가능성(refutability): let은 반박 불가능(irrefutable) 패턴만 허용합니다 — 항상 매칭하는 패턴(변수 바인딩, 튜플 분해 등). if let과 match는 반박 가능(refutable) 패턴도 허용합니다 — 매칭이 실패할 수 있는 패턴(Some(x), 리터럴 등). let Some(x) = expr은 let-else로만 가능합니다.
고급 패턴 문법 총정리
Rust의 패턴 문법은 단순한 값 비교를 넘어, 복잡한 중첩 구조를 분해하고, 참조를 다루며, 범위를 지정하는 등 매우 강력한 표현력을 제공합니다. 여기서는 실전에서 자주 사용하는 고급 패턴 문법을 총정리합니다.
// ═══════════════════════════════════════════════════════════════
// 1. 중첩 구조 분해 (Nested Destructuring)
// ═══════════════════════════════════════════════════════════════
struct Point { x: f64, y: f64 }
struct Circle { center: Point, radius: f64 }
enum Shape {
Rect { top_left: Point, bottom_right: Point },
Circ(Circle),
}
let shape = Shape::Rect {
top_left: Point { x: 0.0, y: 10.0 },
bottom_right: Point { x: 20.0, y: 0.0 },
};
match shape {
// 중첩된 구조체를 한 번에 분해
Shape::Rect {
top_left: Point { x: x1, y: y1 },
bottom_right: Point { x: x2, y: y2 },
} => {
let area = (x2 - x1) * (y1 - y2);
println!("직사각형 면적: {}", area);
}
Shape::Circ(Circle { center: Point { x, y }, radius }) => {
println!("원: 중심({}, {}), 반지름 {}", x, y, radius);
}
}
// ═══════════════════════════════════════════════════════════════
// 2. ref와 ref mut — 패턴에서 참조 바인딩
// ═══════════════════════════════════════════════════════════════
let data = Some(String::from("hello"));
// ref를 사용하여 소유권을 가져가지 않고 참조만 바인딩
match &data {
Some(s) => println!("길이: {}", s.len()), // s: &String (자동 참조)
None => {}
}
// data는 여전히 사용 가능 (move되지 않음)
let mut value = Some(42);
match &mut value {
Some(v) => *v += 1, // v: &mut i32
None => {}
}
assert_eq!(value, Some(43));
// 레거시 문법 (Rust 2015): ref 키워드 사용
match data {
Some(ref s) => println!("{}", s), // ref로 명시적 참조 바인딩
None => {}
}
// ═══════════════════════════════════════════════════════════════
// 3. 다중 참조 레이어 해소
// ═══════════════════════════════════════════════════════════════
let x = 5;
let r1 = &x;
let r2 = &r1;
let r3 = &r2;
// match ergonomics: 자동으로 참조를 벗겨냄
match r3 {
5 => println!("5입니다"), // &&&i32와 i32를 자동 비교
_ => {}
}
// ═══════════════════════════════════════════════════════════════
// 4. 범위 패턴 (Range Patterns)
// ═══════════════════════════════════════════════════════════════
let code: u16 = 404;
let category = match code {
100..=199 => "정보", // 100 이상 199 이하 (inclusive)
200..=299 => "성공", // ..= 는 양 끝 포함
300..=399 => "리다이렉트",
400..=499 => "클라이언트 에러",
500..=599 => "서버 에러",
_ => "알 수 없음",
};
// 문자 범위도 가능
let ch = '가';
match ch {
'a'..='z' => println!("소문자 영문"),
'A'..='Z' => println!("대문자 영문"),
'가'..='힣' => println!("한글"),
_ => println!("기타 문자"),
}
바인딩 모드(Binding Modes)와 match ergonomics는 Rust 2018부터 도입된 기능으로, 패턴에서 참조를 더 편리하게 다룰 수 있게 합니다.
// ═══════════════════════════════════════════════════════════════
// 5. Match Ergonomics (Rust 2018+)
// ═══════════════════════════════════════════════════════════════
// Rust 2015 스타일: 명시적 참조 해제 필요
let values = vec![1, 2, 3];
for val in &values {
// val: &i32
match *val { // 명시적 역참조 필요했음
1 => println!("하나"),
_ => {}
}
}
// Rust 2018 스타일: 자동으로 바인딩 모드 결정
for val in &values {
match val { // 역참조 불필요!
1 => println!("하나"), // 컴파일러가 자동으로 &1과 비교
_ => {}
}
}
// Option<&T>에서의 ergonomics
let names: Vec<String> = vec!["Alice".into(), "Bob".into()];
if let Some(first) = names.first() {
// first: &String (자동으로 참조 바인딩)
println!("첫 번째: {}", first);
}
// ═══════════════════════════════════════════════════════════════
// 6. let-else 패턴 (Rust 1.65+)
// ═══════════════════════════════════════════════════════════════
use std::collections::HashMap;
fn process_config(config: &HashMap<String, String>) -> Result<u16, String> {
// let-else: 패턴 매칭 실패 시 반드시 분기(return, break, continue, panic)
let Some(port_str) = config.get("port") else {
return Err("port 설정이 없습니다".to_string());
};
let Ok(port) = port_str.parse::<u16>() else {
return Err(format!("유효하지 않은 포트: {}", port_str));
};
// port_str과 port는 이후 코드에서 바로 사용 가능
println!("포트: {} (원본: {})", port, port_str);
Ok(port)
}
// let-else vs if-let 비교
fn example(input: Option<&str>) {
// ❌ if-let: 깊은 들여쓰기 (pyramid of doom)
if let Some(s) = input {
if let Ok(n) = s.parse::<i32>() {
if n > 0 {
println!("양수: {}", n);
}
}
}
// ✅ let-else: 일직선 흐름 (early return)
let Some(s) = input else { return };
let Ok(n) = s.parse::<i32>() else { return };
if n <= 0 { return; }
println!("양수: {}", n);
}
let-else의 핵심 규칙: else 블록은 반드시 발산(diverge)해야 합니다 — return, break, continue, panic! 중 하나로 끝나야 합니다. 이를 통해 성공 경로에서 바인딩된 변수가 항상 유효함을 컴파일러가 보장합니다. 커널 코드에서는 return Err(EINVAL)과 함께 사용하면 에러 처리가 매우 깔끔해집니다.
패턴이 사용되는 모든 위치
Rust에서 패턴은 match뿐만 아니라 다양한 위치에서 사용됩니다. 각 위치마다 반박 가능(refutable) 패턴과 반박 불가능(irrefutable) 패턴의 허용 여부가 다릅니다.
// ═══════════════════════════════════════════════════════════════
// 패턴이 사용되는 6가지 위치
// ═══════════════════════════════════════════════════════════════
// 1. let 문 — 반박 불가능(irrefutable) 패턴만
let (a, b, c) = (1, 2, 3); // ✅ 튜플 분해
let Point { x, y } = point; // ✅ 구조체 분해
let [first, .., last] = [1,2,3,4,5]; // ✅ 슬라이스 분해
// let Some(x) = option; // ❌ 컴파일 에러: refutable 패턴
// 2. 함수 매개변수 — 반박 불가능 패턴만
fn print_point(&(x, y): &(i32, i32)) {
println!("({}, {})", x, y);
}
fn first_element([first, ..]: [i32; 3]) -> i32 { first }
// 3. for 루프 — 반박 불가능 패턴만
let pairs = vec![("key1", 1), ("key2", 2)];
for (key, value) in &pairs {
println!("{}: {}", key, value);
}
for (idx, (key, val)) in pairs.iter().enumerate() {
println!("[{}] {}: {}", idx, key, val);
}
// 4. while let — 반박 가능(refutable) 패턴 허용
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
println!("꺼낸 값: {}", top);
}
// 5. if let — 반박 가능 패턴 허용
let config_value: Option<i32> = Some(42);
if let Some(val) = config_value {
println!("설정값: {}", val);
}
// 6. match — 반박 가능 패턴 허용 (모든 경우 포함 필수)
match config_value {
Some(v) if v > 0 => println!("양수: {}", v),
Some(v) => println!("음수 또는 0: {}", v),
None => println!("값 없음"),
}
| 위치 | 반박 불가능 (irrefutable) | 반박 가능 (refutable) | 설명 |
|---|---|---|---|
let 문 |
허용 | 불가 (let-else만 가능) |
항상 매칭이 보장되어야 함 |
| 함수 매개변수 | 허용 | 불가 | 함수 호출 시 항상 매칭되어야 함 |
for 루프 |
허용 | 불가 | 각 반복에서 항상 매칭되어야 함 |
while let |
허용 (무한 루프) | 허용 | 매칭 실패 시 루프 종료 |
if let |
허용 (경고) | 허용 | 매칭 실패 시 else 분기 |
match |
허용 | 허용 | 모든 가능한 패턴을 다뤄야 함 |
let-else |
허용 (불필요) | 허용 | 실패 시 반드시 발산 (return 등) |
반박 가능성이 중요한 이유: 반박 불가능 위치(let, 함수 매개변수, for)에 반박 가능 패턴을 사용하면 컴파일 에러가 발생합니다. 이는 매칭 실패 시 어떻게 처리할지 코드에 명시되지 않았기 때문입니다. 반대로 if let에 반박 불가능 패턴을 사용하면 경고가 발생합니다 — 항상 참인 조건문은 의미가 없기 때문입니다. 이 구분을 이해하면 컴파일러 에러 메시지를 빠르게 해석할 수 있습니다.
에러 처리 기초 (Result, Option, ? 연산자)
Rust는 예외(exception) 대신 Result<T, E>와 Option<T> 열거형으로 에러를 처리합니다. ? 연산자는 에러 전파를 간결하게 만듭니다.
C의 에러 처리와 비교: C에서는 에러를 반환 코드(-1, NULL, errno)로 처리합니다. 문제는 호출자가 반환값을 무시해도 컴파일러가 경고하지 않는다는 것입니다. fd = open("file", O_RDONLY); 후 반환값 검사를 잊으면 잘못된 fd(-1)로 작업하게 됩니다. Rust의 Result<T, E>는 이 문제를 근본적으로 해결합니다: (1) Result를 무시하면 컴파일러가 경고(#[must_use])를 줍니다. (2) 값을 꺼내려면 match, ?, unwrap() 중 하나를 선택해야 하므로 에러 처리를 의식적으로 결정하게 됩니다.
왜 예외(exception)가 아닌가: Java/Python은 예외(exception)로 에러를 처리하지만, 예외는 보이지 않는 제어 흐름입니다 — 함수 시그니처만 봐서는 어떤 예외가 발생할 수 있는지 알 수 없습니다. 또한 예외는 스택 되감기(stack unwinding) 비용이 있어 커널 같은 성능 민감한 환경에 부적합합니다. Rust의 Result는 반환 타입에 에러 가능성이 명시되고, 런타임 비용이 없으며, 컴파일러가 처리를 강제합니다.
// Result<T, E> — 성공(Ok) 또는 실패(Err)
use std::fs;
use std::io;
fn read_username(path: &str) -> Result<String, io::Error> {
let mut file = fs::File::open(path)?; // ? = Err이면 즉시 반환
let mut name = String::new();
file.read_to_string(&mut name)?; // ?로 에러 전파
Ok(name.trim().to_string())
}
// 더 간결한 방법
fn read_username_short(path: &str) -> Result<String, io::Error> {
fs::read_to_string(path) // 한 줄로!
}
// Option<T> — 값이 있거나(Some) 없음(None)
fn find_process(pid: u32) -> Option<Process> {
if pid == 0 { None } else { Some(Process::new(pid, "found".into())) }
}
// Option 체이닝
let name = find_process(1)
.map(|p| p.name.to_uppercase())
.unwrap_or_else(|| "UNKNOWN".to_string());
// match로 Result/Option 처리
match read_username("/etc/hostname") {
Ok(name) => println!("호스트: {}", name),
Err(e) => eprintln!("에러: {}", e),
}
// unwrap/expect — 실패 시 panic (프로토타입/테스트 전용)
let val = Some(42).unwrap(); // 42, None이면 panic
let val = Some(42).expect("있어야 함"); // 42, None이면 메시지와 함께 panic
// 커스텀 에러 타입
#[derive(Debug)]
enum AppError {
Io(io::Error),
Parse(std::num::ParseIntError),
Custom(String),
}
impl From<io::Error> for AppError {
fn from(e: io::Error) -> Self { AppError::Io(e) }
}
// From 구현으로 ? 연산자가 자동 변환
Option / Result 주요 메서드 비교
| 메서드 | Option<T> | Result<T, E> | 동작 |
|---|---|---|---|
unwrap() | Some→T, None→panic | Ok→T, Err→panic | 값 추출 (실패 시 panic) |
expect("msg") | Some→T, None→panic+msg | Ok→T, Err→panic+msg | 에러 메시지와 함께 panic |
unwrap_or(default) | Some→T, None→default | Ok→T, Err→default | 기본값 반환 |
unwrap_or_else(f) | Some→T, None→f() | Ok→T, Err→f(e) | 지연 기본값 (클로저) |
map(f) | Some(f(v)), None | Ok(f(v)), Err(e) | 값 변환 |
and_then(f) | Some→f(v), None | Ok→f(v), Err(e) | 체이닝 (flatmap) |
or_else(f) | Some, None→f() | Ok, Err→f(e) | 대체 경로 |
is_some()/is_ok() | bool | bool | 상태 확인 |
ok() | — | Ok→Some(v), Err→None | Result→Option 변환 |
transpose() | Option<Result>↔Result<Option> | Result<Option>↔Option<Result> | 중첩 타입 교환 |
커널 에러 처리 규칙: 커널 Rust에서 unwrap()이나 expect()는 사용 금지입니다. panic이 발생하면 커널 전체가 멈추거나 oops가 발생합니다. 항상 ? 연산자로 에러를 전파하거나, unwrap_or() / unwrap_or_else()로 기본값을 제공하세요. 커널의 Result<T>는 Result<T, kernel::error::Error>의 별칭이며, C의 -EINVAL, -ENOMEM 등에 대응합니다.
panic!과 백트레이스
panic!은 복구 불가능한 에러 상황에서 프로그램을 즉시 종료하는 메커니즘입니다. Result와의 사용 구분, 백트레이스 활용, panic 전략(unwinding vs abort)을 이해하는 것이 중요합니다.
// ═══════════════════════════════════════════════════════════════
// 1. panic!의 기본 사용과 발생 상황
// ═══════════════════════════════════════════════════════════════
// 명시적 panic
panic!("치명적 에러 발생");
panic!("인덱스 {} 가 범위 {} 를 초과", idx, len);
// 암시적 panic이 발생하는 경우
let v = vec![1, 2, 3];
let _ = v[99]; // 인덱스 범위 초과 → panic
let _ = 0_i32 / 0; // 0으로 나누기 → panic (디버그 모드)
let x: Option<i32> = None;
x.unwrap(); // None에 unwrap → panic
// ═══════════════════════════════════════════════════════════════
// 2. RUST_BACKTRACE로 백트레이스 확인
// ═══════════════════════════════════════════════════════════════
// 터미널에서 실행:
// $ RUST_BACKTRACE=1 cargo run → 간략한 백트레이스
// $ RUST_BACKTRACE=full cargo run → 전체 백트레이스 (라이브러리 프레임 포함)
// 출력 예시:
// thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99'
// stack backtrace:
// 0: std::panicking::begin_panic
// 1: my_app::process_data ← 에러 발생 위치
// at src/main.rs:42:5
// 2: my_app::main
// at src/main.rs:10:3
// ═══════════════════════════════════════════════════════════════
// 3. panic hook 커스터마이징
// ═══════════════════════════════════════════════════════════════
use std::panic;
// 커스텀 panic handler 설치
panic::set_hook(Box::new(|info| {
// 에러 로깅, 알림 전송 등
if let Some(msg) = info.payload().downcast_ref::<&str>() {
eprintln!("[FATAL] Panic: {}", msg);
}
if let Some(loc) = info.location() {
eprintln!(" 위치: {}:{}", loc.file(), loc.line());
}
}));
// panic 캐치 (테스트/서버 등에서 사용)
let result = panic::catch_unwind(|| {
panic!("테스트 panic");
});
match result {
Ok(_) => println!("정상 완료"),
Err(_) => println!("panic이 발생했지만 복구됨"),
}
Unwinding vs Abort: Rust의 panic은 두 가지 전략을 지원합니다. Unwinding(기본값)은 스택을 되감으며 각 프레임의 Drop을 호출하여 리소스를 정리합니다. Abort는 즉시 프로세스를 종료하며 Drop을 호출하지 않습니다. Cargo.toml에서 [profile.release] panic = "abort"로 설정할 수 있으며, 바이너리 크기가 줄고 컴파일이 빨라집니다. 커널에서는 항상 abort 전략을 사용합니다 — unwinding은 커널 스택에서 안전하지 않기 때문입니다.
커스텀 에러 타입 설계 패턴
실전 프로젝트에서는 String이나 Box<dyn Error> 대신 구조화된 커스텀 에러 타입을 정의하여, 에러 분류, 원인 추적, 사용자 친화적 메시지를 제공합니다.
use std::fmt;
use std::io;
use std::num::ParseIntError;
// ═══════════════════════════════════════════════════════════════
// 1. 커스텀 에러 타입 정의
// ═══════════════════════════════════════════════════════════════
#[derive(Debug)]
enum AppError {
Io(io::Error),
Parse(ParseIntError),
Config { key: String, message: String },
NotFound(String),
Auth(String),
}
// Display 구현: 사용자에게 보여줄 에러 메시지
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::Io(e) => write!(f, "I/O 에러: {}", e),
AppError::Parse(e) => write!(f, "파싱 에러: {}", e),
AppError::Config { key, message } =>
write!(f, "설정 에러 [{}]: {}", key, message),
AppError::NotFound(name) => write!(f, "'{}' 을(를) 찾을 수 없음", name),
AppError::Auth(msg) => write!(f, "인증 실패: {}", msg),
}
}
}
// std::error::Error 구현: 에러 체인 지원
impl std::error::Error for AppError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
AppError::Io(e) => Some(e), // 원인 에러 추적 가능
AppError::Parse(e) => Some(e), // 원인 에러 추적 가능
_ => None, // 원인 에러 없음
}
}
}
// ═══════════════════════════════════════════════════════════════
// 2. From 변환으로 ? 연산자 지원
// ═══════════════════════════════════════════════════════════════
impl From<io::Error> for AppError {
fn from(e: io::Error) -> Self {
AppError::Io(e)
}
}
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self {
AppError::Parse(e)
}
}
// From 구현 덕분에 ? 연산자가 자동으로 에러를 변환
fn read_config_port(path: &str) -> Result<u16, AppError> {
let content = std::fs::read_to_string(path)?; // io::Error → AppError::Io
let port: u16 = content.trim().parse()?; // ParseIntError → AppError::Parse
Ok(port)
}
// ═══════════════════════════════════════════════════════════════
// 3. 에러 체인 출력
// ═══════════════════════════════════════════════════════════════
fn print_error_chain(err: &dyn std::error::Error) {
eprintln!("에러: {}", err);
let mut source = err.source();
while let Some(cause) = source {
eprintln!(" 원인: {}", cause);
source = cause.source();
}
}
// 출력:
// 에러: I/O 에러: No such file or directory (os error 2)
// 원인: No such file or directory (os error 2)
thiserror와 anyhow는 에러 처리의 보일러플레이트를 줄여주는 대표적인 크레이트입니다. 라이브러리에는 thiserror를, 애플리케이션에는 anyhow를 사용하는 것이 일반적입니다.
// ═══════════════════════════════════════════════════════════════
// thiserror: 라이브러리용 — 구조화된 에러 타입 자동 생성
// ═══════════════════════════════════════════════════════════════
// Cargo.toml: thiserror = "1"
use thiserror::Error;
#[derive(Debug, Error)]
enum DbError {
#[error("연결 실패: {0}")] // Display 자동 생성
Connection(String),
#[error("쿼리 에러: {query}")]
Query { query: String },
#[error("I/O 에러")]
Io(#[from] io::Error), // From 자동 구현 + source()
#[error(transparent)] // Display와 source를 내부 에러에 위임
Other(#[from] Box<dyn std::error::Error + Send + Sync>),
}
// ═══════════════════════════════════════════════════════════════
// anyhow: 애플리케이션용 — 빠른 프로토타이핑과 에러 컨텍스트
// ═══════════════════════════════════════════════════════════════
// Cargo.toml: anyhow = "1"
use anyhow::{Context, Result, bail};
fn load_user_config() -> Result<Config> { // anyhow::Result
let path = "~/.config/app.toml";
let content = std::fs::read_to_string(path)
.context("설정 파일을 읽을 수 없습니다")?; // 컨텍스트 추가
let config: Config = parse_toml(&content)
.with_context(|| format!("{} 파싱 실패", path))?;
if config.port == 0 {
bail!("포트 번호가 0입니다"); // return Err(anyhow!(...))의 단축
}
Ok(config)
}
// ═══════════════════════════════════════════════════════════════
// Box<dyn Error>: 빠른 프로토타이핑용
// ═══════════════════════════════════════════════════════════════
type GenericResult<T> = Result<T, Box<dyn std::error::Error>>;
fn quick_prototype() -> GenericResult<()> {
let file = std::fs::read_to_string("data.txt")?; // io::Error 자동 변환
let num: i32 = file.trim().parse()?; // ParseIntError 자동 변환
println!("값: {}", num);
Ok(())
}
에러 타입 선택 가이드:
- 라이브러리:
thiserror로 구조화된 enum 에러 → 호출자가 패턴 매칭으로 에러별 처리 가능 - 애플리케이션:
anyhow로 에러 컨텍스트 추가 → 상세한 에러 메시지 제공 - 프로토타입:
Box<dyn Error>→ 외부 크레이트 없이 빠른 개발 - 커널:
kernel::error::Error— C의 에러 코드(-EINVAL등)를 래핑하며,thiserror/anyhow는 사용 불가 (alloc 미지원)
실전 에러 처리 전략
프로젝트 규모와 코드 위치(라이브러리 vs 애플리케이션)에 따라 적절한 에러 처리 전략이 다릅니다. unwrap을 언제 사용해도 되는지, Result 컴비네이터를 어떻게 체이닝하는지, 그리고 이터레이터에서 Result를 수집하는 방법을 다룹니다.
// ═══════════════════════════════════════════════════════════════
// 1. unwrap()을 사용해도 되는 경우
// ═══════════════════════════════════════════════════════════════
// ✅ 테스트 코드에서
#[test]
fn test_parse() {
let result = "42".parse::<i32>().unwrap();
assert_eq!(result, 42);
}
// ✅ 논리적으로 실패가 불가능한 경우
let home = std::env::var("HOME")
.expect("HOME 환경변수가 설정되지 않음"); // Unix에서 거의 항상 존재
// ✅ 정적으로 유효성을 증명 가능한 경우
let re = Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap(); // 정규식 리터럴
// ✅ 프로토타이핑 / 빠른 실험
fn main() {
let data = std::fs::read_to_string("input.txt").unwrap(); // TODO: 나중에 수정
}
// ═══════════════════════════════════════════════════════════════
// 2. Result 컴비네이터 체이닝
// ═══════════════════════════════════════════════════════════════
fn get_user_age(db: &Database, id: u64) -> Result<String, AppError> {
db.find_user(id)
// map: Ok 값을 변환
.map(|user| user.age)
// and_then: Ok 값을 Result를 반환하는 함수에 전달
.and_then(|age| {
if age > 0 && age < 150 {
Ok(format!("나이: {}세", age))
} else {
Err(AppError::Config {
key: "age".into(),
message: format!("유효하지 않은 나이: {}", age),
})
}
})
// map_err: Err 값을 변환
.map_err(|e| {
eprintln!("사용자 {} 조회 실패: {}", id, e);
e
})
}
// or_else: Err일 때 대체 시도
fn get_config_value(key: &str) -> Result<String, AppError> {
read_from_env(key)
.or_else(|_| read_from_file(key)) // 환경변수 실패 → 파일에서 시도
.or_else(|_| read_from_defaults(key)) // 파일 실패 → 기본값에서 시도
}
// ═══════════════════════════════════════════════════════════════
// 3. Iterator에서 Result 수집
// ═══════════════════════════════════════════════════════════════
let strings = vec!["1", "2", "3", "4"];
// 방법 1: collect로 Vec<Result> → Result<Vec> 변환
// 하나라도 Err이면 전체가 Err (첫 번째 에러 반환)
let numbers: Result<Vec<i32>, _> = strings.iter()
.map(|s| s.parse::<i32>())
.collect();
assert_eq!(numbers, Ok(vec![1, 2, 3, 4]));
// 방법 2: 에러 무시하고 성공한 것만 수집
let mixed = vec!["1", "abc", "3", "xyz"];
let valid: Vec<i32> = mixed.iter()
.filter_map(|s| s.parse().ok()) // ok(): Result → Option, Err는 None
.collect();
assert_eq!(valid, vec![1, 3]);
// 방법 3: 성공/실패 분리
let (successes, failures): (Vec<_>, Vec<_>) = mixed.iter()
.map(|s| s.parse::<i32>())
.partition(Result::is_ok);
let values: Vec<i32> = successes.into_iter().map(|r| r.unwrap()).collect();
println!("성공: {:?}, 실패 수: {}", values, failures.len());
라이브러리 vs 애플리케이션 에러 처리: 라이브러리는 에러를 구조화하여 호출자가 match로 분기할 수 있게 해야 합니다(thiserror). 애플리케이션은 에러를 사용자에게 설명하는 데 집중해야 합니다(anyhow). 두 접근법을 혼합하면 안 됩니다: 라이브러리에서 anyhow::Error를 반환하면 호출자가 에러 종류를 판별할 수 없고, 애플리케이션에서 지나치게 세분화된 에러 타입을 만들면 불필요한 복잡성만 늘어납니다.
트레이트(Trait)와 제네릭(Generics)
트레이트는 공유 동작을 정의하는 Rust의 인터페이스 시스템입니다. 제네릭과 결합하여 타입 안전하면서도 유연한 코드를 작성할 수 있으며, 단형화(monomorphization)를 통해 런타임 오버헤드가 없습니다.
// 트레이트 정의
trait Summary {
fn summarize(&self) -> String;
// 기본 구현 — 구현체에서 오버라이드 가능
fn preview(&self) -> String {
format!("{}...", &self.summarize()[..20])
}
}
// 트레이트 구현
struct Article { title: String, content: String }
impl Summary for Article {
fn summarize(&self) -> String {
format!("{}: {}", self.title, &self.content[..50])
}
}
// 제네릭 함수 + 트레이트 바운드
fn notify<T: Summary>(item: &T) {
println!("속보: {}", item.summarize());
}
// where 절 — 복잡한 바운드를 읽기 쉽게
fn complex<T, U>(t: &T, u: &U) -> String
where
T: Summary + Clone,
U: Display + Debug,
{
format!("{} — {}", t.summarize(), u)
}
// 제네릭 구조체
struct Pair<T> { first: T, second: T }
impl<T: PartialOrd> Pair<T> {
fn larger(&self) -> &T {
if self.first >= self.second { &self.first } else { &self.second }
}
}
// 트레이트 객체 (동적 디스패치) — 런타임 다형성
fn print_summary(items: &[&dyn Summary]) {
for item in items {
println!("{}", item.summarize());
}
}
// derive 매크로 — 자동 트레이트 구현
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Config {
name: String,
value: i32,
}
정적 디스패치 vs 동적 디스패치
| 특성 | 정적 디스패치 (impl Trait / 제네릭) | 동적 디스패치 (dyn Trait) |
|---|---|---|
| 해결 시점 | 컴파일 타임 | 런타임 |
| 메커니즘 | 단형화 (각 타입별 코드 복제) | vtable (가상 함수 테이블) |
| 성능 | 인라인 최적화 가능, 오버헤드 0 | 간접 호출 (vtable lookup), 인라인 불가 |
| 바이너리 크기 | 커질 수 있음 (타입마다 코드 복제) | 작음 (코드 하나 + vtable) |
| 이종 컬렉션 | 불가 (모든 요소 같은 타입) | 가능 (Vec<Box<dyn Trait>>) |
| 구문 | fn f(x: impl Trait) | fn f(x: &dyn Trait) |
| 커널 사용 | 대부분의 커널 트레이트 | #[vtable]로 C vtable 래핑 |
// 정적 디스패치 — 컴파일러가 타입별 코드 생성 (제로 코스트)
fn process_static(item: impl Summary) {
println!("{}", item.summarize());
}
// process_static(article) → process_static_Article() 생성
// process_static(tweet) → process_static_Tweet() 생성
// 동적 디스패치 — 런타임에 vtable로 메서드 호출
fn process_dynamic(item: &dyn Summary) {
println!("{}", item.summarize()); // vtable을 통한 간접 호출
}
// 이종 컬렉션 — dyn Trait만 가능
let items: Vec<Box<dyn Summary>> = vec![
Box::new(article),
Box::new(tweet), // 서로 다른 타입을 하나의 컬렉션에!
];
수퍼트레이트, 블랭킷 구현, 연관 타입
Rust의 트레이트 시스템은 수퍼트레이트(Supertrait)로 트레이트 간 의존성을 표현하고, 블랭킷 구현(Blanket Implementation)으로 제네릭한 범위에 대해 일괄 구현하며, 연관 타입(Associated Type)으로 트레이트의 출력 타입을 지정합니다. 이 세 가지를 이해하면 표준 라이브러리와 커널 트레이트 설계를 깊이 파악할 수 있습니다.
| 개념 | 구문 | 목적 | 예시 |
|---|---|---|---|
| 수퍼트레이트 | trait A: B + C | A를 구현하려면 B, C도 구현 필수 | trait Error: Display + Debug |
| 블랭킷 구현 | impl<T: X> Y for T | 조건 만족하는 모든 타입에 일괄 구현 | impl<T: Display> ToString for T |
| Orphan Rule | — | 외부 크레이트 타입+트레이트 동시 외부이면 구현 불가 | impl Display for Vec<T> 불가 |
| 연관 타입 | type Item; | 트레이트 내 출력 타입 고정 (구현 시 1개만 지정) | Iterator::Item |
| 제네릭 매개변수 | trait Foo<T> | 같은 타입에 여러 구현 가능 | From<u32>, From<String> |
// ═══════════════════════════════════════════════════════════════
// 1. 수퍼트레이트 — trait A: B + C
// ═══════════════════════════════════════════════════════════════
use std::fmt;
// Error 트레이트는 Display + Debug를 수퍼트레이트로 요구
// → Error를 구현하려면 Display와 Debug도 반드시 구현해야 함
trait Printable: fmt::Display + fmt::Debug {
fn print_both(&self) {
println!("Display: {}, Debug: {:?}", self, self);
}
}
#[derive(Debug)]
struct Point { x: f64, y: f64 }
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
// Display + Debug 모두 구현했으므로 Printable 구현 가능
impl Printable for Point {}
// 수퍼트레이트를 활용한 트레이트 바운드
fn log_item(item: &dyn Printable) {
// Printable을 받으면 Display와 Debug 메서드도 사용 가능
println!("[LOG] {}", item); // Display
println!("[DBG] {:?}", item); // Debug
item.print_both(); // Printable 자체 메서드
}
// ═══════════════════════════════════════════════════════════════
// 2. 블랭킷 구현 (Blanket Implementation)
// ═══════════════════════════════════════════════════════════════
// 표준 라이브러리의 대표적 블랭킷 구현:
// impl<T: Display> ToString for T { ... }
// → Display를 구현한 모든 타입은 자동으로 ToString도 갖게 됨!
trait Greet {
fn greet(&self) -> String;
}
// Display를 구현한 모든 타입에 Greet을 일괄 구현
impl<T: fmt::Display> Greet for T {
fn greet(&self) -> String {
format!("안녕하세요, {}님!", self)
}
}
// String, &str, i32 등 Display 구현 타입은 모두 greet() 사용 가능
assert_eq!("Rust".greet(), "안녕하세요, Rust님!");
assert_eq!(42.greet(), "안녕하세요, 42님!");
// ═══════════════════════════════════════════════════════════════
// 3. Orphan Rule (고아 규칙 / 일관성)
// ═══════════════════════════════════════════════════════════════
// ✅ 허용: 내 트레이트 + 외부 타입
trait MyTrait {}
impl MyTrait for Vec<i32> {} // OK — MyTrait은 내 크레이트 소속
// ✅ 허용: 외부 트레이트 + 내 타입
impl fmt::Display for Point {} // OK — Point는 내 크레이트 소속
// ❌ 금지: 외부 트레이트 + 외부 타입
// impl fmt::Display for Vec<i32> {} // 컴파일 에러!
// → 두 크레이트가 동시에 같은 구현을 만들면 충돌 발생
// 우회 방법: 뉴타입 패턴 (Newtype Pattern)
struct Wrapper(Vec<i32>); // 내 타입으로 감싸기
impl fmt::Display for Wrapper {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.iter()
.map(|n| n.to_string())
.collect::<Vec<_>>()
.join(", "))
}
}
// ═══════════════════════════════════════════════════════════════
// 4. 연관 타입 vs 제네릭 매개변수
// ═══════════════════════════════════════════════════════════════
// 연관 타입: 구현당 하나의 타입만 지정 (1:1 관계)
trait Iterator {
type Item; // 연관 타입 — 구현 시 고정
fn next(&mut self) -> Option<Self::Item>;
}
// Counter는 항상 u32를 반환 — Item이 고정됨
impl Iterator for Counter {
type Item = u32; // 구현 시 결정, 변경 불가
fn next(&mut self) -> Option<u32> { /* ... */ }
}
// 제네릭 매개변수: 같은 타입에 여러 구현 가능 (1:N 관계)
trait From<T> {
fn from(val: T) -> Self;
}
// 같은 타입이 From<u32>와 From<String> 모두 구현 가능
impl From<u32> for Point {
fn from(v: u32) -> Self { Point { x: v as f64, y: 0.0 } }
}
impl From<(f64, f64)> for Point {
fn from((x, y): (f64, f64)) -> Self { Point { x, y } }
}
연관 타입 vs 제네릭 선택 기준: 타입당 구현이 하나뿐이면 연관 타입(Iterator::Item), 같은 타입에 여러 변환이 필요하면 제네릭 매개변수(From<T>)를 사용합니다. 연관 타입은 호출부에서 타입을 명시할 필요가 없어 API가 깔끔해집니다.
커널에서의 Orphan Rule: 커널 Rust 모듈은 독립 크레이트로 취급되므로, 커널 트레이트를 자신의 타입에 구현하는 것은 허용되지만, 표준 트레이트를 커널 타입에 구현하는 것은 커널 크레이트 내부에서만 가능합니다. 모듈 개발자는 뉴타입 패턴으로 우회해야 합니다.
컬렉션 (Vec, HashMap, BTreeMap, HashSet)
Rust의 표준 컬렉션은 소유권 시스템과 통합되어 메모리 안전합니다. 모든 힙 할당 컬렉션은 스코프 종료 시 자동 해제됩니다.
// Vec<T> — 가변 길이 배열 (가장 많이 사용)
let mut v: Vec<i32> = Vec::new();
v.push(1); v.push(2); v.push(3);
let v2 = vec![1, 2, 3]; // 매크로로 초기화
// 안전한 접근
let third: Option<&i32> = v.get(2); // None if out of bounds
let third: &i32 = &v[2]; // panic if out of bounds
// HashMap<K, V> — 키-값 저장소
use std::collections::HashMap;
let mut scores: HashMap<String, i32> = HashMap::new();
scores.insert("Alice".to_string(), 100);
scores.insert("Bob".to_string(), 85);
// entry API — 키가 없을 때만 삽입
scores.entry("Alice".to_string()).or_insert(0);
*scores.entry("Charlie".to_string()).or_insert(0) += 50;
// BTreeMap — 키 기준 정렬 (O(log n) 탐색)
use std::collections::BTreeMap;
let mut sorted: BTreeMap<i32, &str> = BTreeMap::new();
sorted.insert(3, "c"); sorted.insert(1, "a"); sorted.insert(2, "b");
// 순회 시 키 순서 보장: 1→2→3
// HashSet — 중복 없는 값 집합
use std::collections::HashSet;
let a: HashSet<i32> = [1, 2, 3].into();
let b: HashSet<i32> = [2, 3, 4].into();
let union: HashSet<&i32> = a.union(&b).collect(); // {1, 2, 3, 4}
let inter: HashSet<&i32> = a.intersection(&b).collect(); // {2, 3}
// VecDeque — 양방향 큐 (O(1) 앞뒤 삽입/삭제)
use std::collections::VecDeque;
let mut deque = VecDeque::new();
deque.push_back(1); deque.push_front(0);
// [0, 1]
| 컬렉션 | 삽입 | 탐색 | 순서 | 용도 |
|---|---|---|---|---|
Vec<T> | O(1)* | O(n) | 삽입 순 | 범용 동적 배열 |
HashMap<K,V> | O(1)* | O(1)* | 무순서 | 키-값 빠른 조회 |
BTreeMap<K,V> | O(log n) | O(log n) | 키 정렬 | 정렬 필요 시 |
HashSet<T> | O(1)* | O(1)* | 무순서 | 중복 제거, 집합 연산 |
VecDeque<T> | O(1) | O(n) | 삽입 순 | 양방향 큐/버퍼 |
BinaryHeap<T> | O(log n) | O(1) max | 힙 순 | 우선순위 큐 |
LinkedList<T> | O(1) | O(n) | 삽입 순 | 양단 삽입/삭제 (거의 사용 안함) |
std vs kernel 컬렉션 API 비교
| 작업 | std (사용자 공간) | kernel (커널 공간) | 차이 이유 |
|---|---|---|---|
| Vec 생성 | Vec::new() | Vec::new() (동일) | — |
| 원소 추가 | v.push(x) | v.try_push(x)? | 커널에서 할당 실패 가능 (OOM) |
| 용량 확보 | v.reserve(n) | v.try_reserve(n)? | 실패 시 Err 반환 |
| Box 생성 | Box::new(v) | Box::try_new(v)? | OOM 시 panic 대신 에러 |
| HashMap | HashMap::new() | 미제공 (C rhashtable 래핑) | 커널 해시 테이블 재사용 |
| String | String::from("s") | CString::try_from_fmt(fmt!("s"))? | NUL 종단 + 실패 가능 할당 |
// === 커널 Vec 패턴: 모든 할당이 실패 가능 ===
use kernel::prelude::*;
fn collect_data(count: usize) -> Result<Vec<u32>> {
let mut data = Vec::new();
data.try_reserve(count)?; // 미리 용량 확보 시도
for i in 0..count {
data.try_push(i as u32)?; // push 대신 try_push
}
Ok(data)
}
// === 반복 패턴 — 컬렉션과 반복자 조합 ===
let v = vec![10, 20, 30, 40, 50];
// 소유권 이동 반복 (원본 소멸)
for val in v { /* val: i32, v 사용 불가 */ }
// 불변 참조 반복 (원본 유지)
for val in &v { /* val: &i32 */ }
// 가변 참조 반복 (원소 수정)
for val in &mut v { *val *= 2; }
커널에서의 컬렉션: 커널 Rust에서는 std::collections 대신 kernel::alloc의 Vec을 사용합니다. 모든 할당 가능 연산은 try_* 변형을 사용해야 하며, push()나 Box::new()처럼 실패 시 panic하는 API는 커널에서 사용 금지입니다. HashMap은 현재 커널 Rust에서 직접 제공되지 않으며, 커널의 기존 해시 테이블 C API(rhashtable)를 래핑하여 사용합니다.
Vec 내부 구조와 용량 관리
Vec<T>는 Rust에서 가장 많이 사용하는 컬렉션입니다. 내부적으로 포인터(ptr), 길이(len), 용량(capacity) 세 필드로 구성되며, 힙에 연속된 메모리 블록을 할당합니다. 용량 초과 시 2배 전략(doubling strategy)으로 재할당하며, with_capacity()로 미리 예약하면 불필요한 재할당을 방지할 수 있습니다.
| 메서드 | 동작 | 시간 복잡도 | 용도 |
|---|---|---|---|
with_capacity(n) | 최소 n개 용량으로 미리 할당 | O(n) | 크기를 미리 알 때 재할당 방지 |
reserve(n) | 추가로 n개 이상 수용 가능하도록 확장 | O(n) 최악 | 동적 확장 전 예약 |
shrink_to_fit() | 용량을 len에 맞춤 (메모리 절약) | O(n) | 더 이상 추가 없을 때 |
drain(range) | 범위 내 요소를 제거하며 반환 (Iterator) | O(n) | 일부 요소 추출/제거 |
retain(|x| pred) | 조건을 만족하는 요소만 유지 | O(n) | 필터링 (in-place) |
dedup() | 연속 중복 제거 (정렬 후 사용) | O(n) | 정렬된 Vec에서 중복 제거 |
split_off(at) | at 위치에서 분할, 뒷부분 새 Vec으로 | O(n-at) | Vec 분할 |
extend_from_slice() | 슬라이스의 모든 요소를 복사 추가 | O(k) | 효율적 일괄 추가 |
// ═══════════════════════════════════════════════════════════════
// 1. with_capacity()로 재할당 최소화
// ═══════════════════════════════════════════════════════════════
// 나쁜 예: 반복적 재할당 발생 (0→4→8→16→32→64→128→...)
let mut slow = Vec::new();
for i in 0..1000 {
slow.push(i); // 약 10번의 재할당 + 데이터 복사 발생
}
// 좋은 예: 한 번만 할당
let mut fast = Vec::with_capacity(1000);
for i in 0..1000 {
fast.push(i); // 재할당 없음!
}
assert_eq!(fast.len(), 1000);
assert!(fast.capacity() >= 1000);
// ═══════════════════════════════════════════════════════════════
// 2. drain(), retain(), dedup() 활용
// ═══════════════════════════════════════════════════════════════
let mut v = vec![1, 2, 3, 4, 5, 6, 7, 8];
// drain: 범위 요소를 제거하며 Iterator로 반환
let middle: Vec<i32> = v.drain(2..5).collect(); // [3, 4, 5]
assert_eq!(v, vec![1, 2, 6, 7, 8]); // 원본에서 제거됨
// retain: 조건에 맞는 요소만 남김 (in-place filter)
let mut nums = vec![1, 2, 3, 4, 5, 6];
nums.retain(|&x| x % 2 == 0); // 짝수만 유지
assert_eq!(nums, vec![2, 4, 6]);
// dedup: 연속 중복 제거 (정렬 후 사용해야 효과적)
let mut data = vec![3, 1, 4, 1, 5, 3, 3];
data.sort(); // [1, 1, 3, 3, 3, 4, 5]
data.dedup(); // [1, 3, 4, 5] — 연속 중복만 제거
// dedup_by_key: 키 함수 기반 중복 제거
let mut words = vec!["apple", "APPLE", "banana", "Banana"];
words.dedup_by_key(|s| s.to_lowercase());
assert_eq!(words, vec!["apple", "banana"]);
// ═══════════════════════════════════════════════════════════════
// 3. Vec<u8> — 바이트 버퍼로 활용
// ═══════════════════════════════════════════════════════════════
// 네트워크/파일 I/O에서 흔히 사용하는 패턴
let mut buf: Vec<u8> = Vec::with_capacity(4096);
// extend_from_slice로 효율적 데이터 추가
buf.extend_from_slice(b"HTTP/1.1 200 OK\r\n");
buf.extend_from_slice(b"Content-Type: text/plain\r\n\r\n");
buf.extend_from_slice(b"Hello, World!");
// Vec<u8> ↔ String 변환
let text = String::from_utf8(buf.clone()).unwrap();
let bytes: Vec<u8> = text.into_bytes();
// 커널에서: kmalloc 대신 Vec<u8> 사용
// let mut kernel_buf: Vec<u8> = Vec::new();
// kernel_buf.try_reserve(PAGE_SIZE)?;
// ═══════════════════════════════════════════════════════════════
// 4. 내부 포인터와 안전한 슬라이스 변환
// ═══════════════════════════════════════════════════════════════
let v = vec![10, 20, 30];
let ptr: *const i32 = v.as_ptr(); // 내부 포인터 획득
let slice: &[i32] = v.as_slice(); // &[i32]로 변환
let (left, right) = v.split_at(2); // [10,20] / [30]
assert_eq!(left, &[10, 20]);
assert_eq!(right, &[30]);
성능 팁: Vec::with_capacity()는 요소 개수를 미리 알 때 필수입니다. 예를 들어 collect()는 내부적으로 size_hint()를 사용해 with_capacity()를 호출하므로, ExactSizeIterator를 구현한 반복자는 collect() 시 재할당이 발생하지 않습니다. 커널에서는 try_with_capacity()를 사용하여 OOM 상황을 안전하게 처리합니다.
반복자(Iterator)와 클로저(Closure)
반복자와 클로저는 Rust의 함수형 프로그래밍 도구입니다. 지연 평가(lazy evaluation)와 제로 코스트 추상화 덕분에 가독성을 유지하면서 최적의 성능을 달성합니다.
// 클로저(Closure) — 환경을 캡처하는 익명 함수
let multiplier = 3;
let multiply = |x: i32| x * multiplier; // multiplier를 참조로 캡처
println!("{}", multiply(5)); // 15
// 클로저의 세 가지 트레이트 (캡처 방식에 따라 자동 결정)
// Fn: &self — 환경을 불변 참조로 캡처
// FnMut: &mut self — 환경을 가변 참조로 캡처
// FnOnce: self — 환경의 소유권을 가져감 (한 번만 호출 가능)
// move 클로저 — 환경 변수의 소유권을 강제 이동
let name = String::from("thread-1");
let handle = std::thread::spawn(move || {
println!("I'm {}", name); // name의 소유권이 클로저로 이동
});
// 반복자 어댑터 체인 — 가독성과 성능을 모두 확보
let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// 짝수만 필터 → 제곱 → 합계
let sum: i32 = data.iter()
.filter(|&&n| n % 2 == 0)
.map(|&n| n * n)
.sum(); // 4 + 16 + 36 + 64 + 100 = 220
// enumerate — 인덱스와 값을 동시에
for (i, val) in data.iter().enumerate() {
println!("[{}] = {}", i, val);
}
// collect — Iterator를 다양한 컬렉션으로 변환
let doubled: Vec<i32> = data.iter().map(|&x| x * 2).collect();
let set: HashSet<i32> = data.into_iter().collect();
// zip — 두 반복자를 병렬 결합
let keys = vec!["a", "b", "c"];
let vals = vec![1, 2, 3];
let map: HashMap<&&str, &i32> = keys.iter().zip(vals.iter()).collect();
// fold — 초기값 + 누적 함수
let product = (1..=5).fold(1, |acc, x| acc * x); // 120 (5!)
주요 반복자 어댑터/소비자 정리
| 분류 | 메서드 | 설명 | 예시 |
|---|---|---|---|
| 어댑터 (지연) | filter() | 조건에 맞는 요소만 | .filter(|x| x > &0) |
map() | 각 요소 변환 | .map(|x| x * 2) | |
take(n) | 처음 n개만 | .take(5) | |
skip(n) | 처음 n개 건너뛰기 | .skip(2) | |
zip() | 두 반복자 병합 | .zip(other.iter()) | |
chain() | 두 반복자 연결 | .chain(other.iter()) | |
enumerate() | 인덱스 추가 | .enumerate() | |
flat_map() | map + flatten | .flat_map(|x| x.chars()) | |
| 소비자 (즉시) | collect() | 컬렉션으로 변환 | .collect::<Vec<_>>() |
sum() | 합계 | .sum::<i32>() | |
count() | 요소 개수 | .count() | |
find() | 조건에 맞는 첫 요소 | .find(|x| x > &5) | |
any()/all() | 조건 검사 | .any(|x| x > &10) | |
fold() | 누적 연산 | .fold(0, |a, b| a + b) |
// 커스텀 Iterator 구현
struct Counter {
count: u32,
max: u32,
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<u32> {
if self.count < self.max {
self.count += 1;
Some(self.count)
} else {
None // 반복 종료
}
}
}
// 커스텀 반복자도 모든 어댑터/소비자 자동 사용 가능!
let sum: u32 = Counter { count: 0, max: 5 }
.filter(|&n| n % 2 == 0)
.sum(); // 2 + 4 = 6
클로저 트레이트(Fn/FnMut/FnOnce)와 고급 반복자 패턴
클로저는 환경을 캡처하는 방식에 따라 Fn, FnMut, FnOnce 중 하나의 트레이트를 자동으로 구현합니다. 이 세 트레이트는 계층 관계를 형성하며, 반복자와 결합하면 제로 코스트 추상화(zero-cost abstraction)를 통해 C 수준의 성능을 유지하면서도 고수준의 가독성을 확보할 수 있습니다.
| 트레이트 | 시그니처 | 캡처 방식 | 호출 횟수 | 자동 구현 조건 |
|---|---|---|---|---|
FnOnce | self | 소유권 이동 (값) | 최대 1회 | 모든 클로저 |
FnMut | &mut self | 가변 참조 | 여러 번 | 환경을 이동하지 않는 클로저 |
Fn | &self | 불변 참조 | 여러 번, 동시 가능 | 환경을 변경하지 않는 클로저 |
트레이트 계층: Fn ⊂ FnMut ⊂ FnOnce 관계입니다. 즉, Fn을 구현하면 자동으로 FnMut과 FnOnce도 구현됩니다. FnOnce만 요구하는 곳에 Fn 클로저를 전달할 수 있지만, 그 반대는 불가합니다.
// ═══════════════════════════════════════════════════════════════
// 1. Fn, FnMut, FnOnce 각각의 예시
// ═══════════════════════════════════════════════════════════════
// Fn — 환경을 불변 참조로 캡처 (여러 번 호출 가능, 동시 호출 가능)
let name = String::from("커널");
let greet = || println!("안녕, {}!", name); // &name 캡처
greet(); // 여러 번 호출 가능
greet(); // name은 불변 참조이므로 반복 사용 OK
// FnMut — 환경을 가변 참조로 캡처
let mut count = 0;
let mut counter = || {
count += 1; // &mut count 캡처 — 값을 변경
count
};
assert_eq!(counter(), 1);
assert_eq!(counter(), 2); // 여러 번 호출 가능하지만 &mut이므로 동시 호출 불가
// FnOnce — 환경의 소유권을 가져감 (한 번만 호출 가능)
let data = String::from("소유권 이동");
let consume = || {
let _moved = data; // data의 소유권을 클로저 내부로 이동
println!("소비됨: {}", _moved);
};
consume(); // OK — 첫 호출
// consume(); // 컴파일 에러! 이미 소유권이 이동됨
// ═══════════════════════════════════════════════════════════════
// 2. 함수 매개변수로 클로저 트레이트 사용
// ═══════════════════════════════════════════════════════════════
// Fn: 반복 호출 필요 (이벤트 핸들러, 콜백 등)
fn repeat_action(action: impl Fn(), times: usize) {
for _ in 0..times { action(); }
}
// FnMut: 상태 누적 필요 (sort_by, for_each 등)
fn apply_mut<F: FnMut(i32) -> i32>(mut f: F, val: i32) -> i32 {
f(val)
}
// FnOnce: 한 번만 호출 (thread::spawn, unwrap_or_else 등)
fn execute_once<F: FnOnce() -> String>(f: F) -> String {
f() // 소유권을 소비하므로 한 번만 호출
}
// ═══════════════════════════════════════════════════════════════
// 3. move 키워드의 효과
// ═══════════════════════════════════════════════════════════════
let data = vec![1, 2, 3];
// move 없이: 참조로 캡처 → 스레드에 전달 불가 (수명 문제)
// let handle = std::thread::spawn(|| println!("{:?}", data)); // 에러
// move 사용: 소유권을 클로저로 강제 이동
let handle = std::thread::spawn(move || {
println!("{:?}", data); // data의 소유권이 클로저로 이동
});
// data는 여기서 더 이상 사용 불가
handle.join().unwrap();
// 주의: move는 캡처 방식(값/참조)만 변경, 트레이트 종류는 바꾸지 않음
// move || x + 1 → Fn 구현 (x가 Copy이므로 복사되어 이동)
// move || drop(s) → FnOnce 구현 (s의 소유권 소비)
// ═══════════════════════════════════════════════════════════════
// 4. 반복자 vs 루프: 제로 코스트 추상화 증명
// ═══════════════════════════════════════════════════════════════
// C 스타일 루프
let data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let mut sum_loop = 0;
for i in 0..data.len() {
if data[i] % 2 == 0 {
sum_loop += data[i] * data[i];
}
}
// 반복자 체인 — 동일한 성능, 더 나은 가독성
let sum_iter: i32 = data.iter()
.filter(|&&n| n % 2 == 0)
.map(|&n| n * n)
.sum();
// 두 결과는 동일: 220
// LLVM이 반복자 체인을 루프와 동일한 기계어로 컴파일!
// → "추상화 비용 = 0" (Zero-Cost Abstraction)
assert_eq!(sum_loop, sum_iter);
// ═══════════════════════════════════════════════════════════════
// 5. IntoIterator 구현 — for 루프 지원
// ═══════════════════════════════════════════════════════════════
struct Matrix {
data: Vec<Vec<f64>>,
}
// IntoIterator를 구현하면 for 루프에서 직접 사용 가능
impl IntoIterator for Matrix {
type Item = Vec<f64>;
type IntoIter = std::vec::IntoIter<Vec<f64>>;
fn into_iter(self) -> Self::IntoIter {
self.data.into_iter() // 행 단위로 반복
}
}
// 참조에 대한 IntoIterator — 소유권 유지
impl<'a> IntoIterator for &'a Matrix {
type Item = &'a Vec<f64>;
type IntoIter = std::slice::Iter<'a, Vec<f64>>;
fn into_iter(self) -> Self::IntoIter {
self.data.iter()
}
}
// 사용
let m = Matrix { data: vec![vec![1.0, 2.0], vec![3.0, 4.0]] };
for row in &m { // &Matrix에 대한 IntoIterator 호출
println!("{:?}", row); // m은 빌림이므로 이후에도 사용 가능
}
// ═══════════════════════════════════════════════════════════════
// 6. 실전 반복자 어댑터: windows, chunks, peekable
// ═══════════════════════════════════════════════════════════════
let data = [1, 2, 3, 4, 5];
// windows(n): 슬라이딩 윈도우 (겹치는 부분 배열)
for w in data.windows(3) {
println!("{:?}", w);
// [1, 2, 3] → [2, 3, 4] → [3, 4, 5]
}
// 이동 평균 계산
let prices = [100.0, 102.5, 98.0, 105.0, 101.0];
let moving_avg: Vec<f64> = prices.windows(3)
.map(|w| w.iter().sum::<f64>() / w.len() as f64)
.collect(); // [100.17, 101.83, 101.33]
// chunks(n): 겹치지 않는 고정 크기 블록
let bytes = [0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE];
for chunk in bytes.chunks(2) {
println!("{:02X}{:02X}", chunk[0], chunk[1]);
// DEAD → BEEF → CAFE
}
// peekable(): 다음 요소를 소비하지 않고 미리 확인
let mut iter = vec![1, 2, 3].into_iter().peekable();
assert_eq!(iter.peek(), Some(&1)); // 소비하지 않고 확인
assert_eq!(iter.next(), Some(1)); // 이제 소비
assert_eq!(iter.peek(), Some(&2)); // 다음 요소 미리보기
// peekable 실전: 파서에서 토큰 미리보기
fn parse_numbers(input: &str) -> Vec<i32> {
let mut chars = input.chars().peekable();
let mut result = Vec::new();
while let Some(&c) = chars.peek() {
if c.is_ascii_digit() {
let num: String = chars
.by_ref()
.take_while(|c| c.is_ascii_digit())
.collect();
result.push(num.parse().unwrap());
} else {
chars.next();
}
}
result
}
| 어댑터/메서드 | 입력 | 출력 | 용도 |
|---|---|---|---|
windows(n) | 슬라이스 | 겹치는 n-크기 부분 배열 | 이동 평균, 연속 비교 |
chunks(n) | 슬라이스 | 겹치지 않는 n-크기 블록 | 바이트 처리, 배치 작업 |
chunks_exact(n) | 슬라이스 | 정확히 n개짜리만 (나머지 별도) | 고정 크기 패킷 파싱 |
peekable() | Iterator | Peekable<I> | 파서, 조건부 소비 |
by_ref() | Iterator | &mut Iterator (빌림) | 일부만 소비 후 원래 반복자 계속 사용 |
scan(state, f) | Iterator | 상태를 유지하며 변환 | 누적 합, 상태 기반 필터링 |
flat_map(f) | Iterator | 중첩 Iterator 평탄화 | 1:N 매핑 (각 요소 → 여러 요소) |
step_by(n) | Iterator/Range | n칸씩 건너뛰기 | 간격 있는 순회 |
제로 코스트 추상화의 조건: 반복자 체인이 제로 코스트가 되려면 릴리스 빌드(--release)에서 컴파일해야 합니다. 디버그 빌드에서는 최적화가 적용되지 않아 루프보다 느릴 수 있습니다. 또한 dyn Iterator(동적 디스패치)를 사용하면 인라인 최적화가 불가능하므로, 성능이 중요한 경우 제네릭(impl Iterator)을 사용하세요.
스마트 포인터 (Box, Rc, Arc, RefCell, Cow)
스마트 포인터는 포인터처럼 동작하면서 추가 메타데이터와 기능을 제공합니다. Rust의 소유권 시스템과 통합되어 메모리 안전성을 보장합니다.
// Box<T> — 힙 할당 + 단일 소유
let b = Box::new(5); // 힙에 i32 할당
println!("{}", b); // Deref로 자동 역참조
// 재귀 타입에 필수 — 컴파일 타임에 크기를 알 수 없는 경우
enum List {
Cons(i32, Box<List>),
Nil,
}
// Rc<T> — 단일 스레드 참조 카운팅
use std::rc::Rc;
let shared = Rc::new(42);
let clone1 = Rc::clone(&shared); // 참조 카운트: 2
let clone2 = Rc::clone(&shared); // 참조 카운트: 3
println!("참조 수: {}", Rc::strong_count(&shared)); // 3
// RefCell<T> — 내부 가변성 (런타임 빌림 검사)
use std::cell::RefCell;
let cell = RefCell::new(vec![1, 2, 3]);
cell.borrow_mut().push(4); // 불변 컨텍스트에서도 내부 변경 가능
println!("{:?}", cell.borrow()); // [1, 2, 3, 4]
// 주의: 규칙 위반 시 컴파일 에러가 아닌 런타임 panic!
// Rc + RefCell 조합 — 다중 소유 + 가변성
let shared = Rc::new(RefCell::new(vec![1]));
let clone = Rc::clone(&shared);
shared.borrow_mut().push(2);
clone.borrow_mut().push(3);
println!("{:?}", shared.borrow()); // [1, 2, 3]
// Cow (Clone on Write) — 읽기는 빌림, 변경 시에만 복사
use std::borrow::Cow;
fn process(input: &str) -> Cow<str> {
if input.contains(' ') {
Cow::Owned(input.replace(' ', "_")) // 변경 필요 → 복사
} else {
Cow::Borrowed(input) // 변경 불필요 → 빌림만
}
}
스마트 포인터 선택 가이드
| 질문 | 예 | 아니오 |
|---|---|---|
| 단일 소유자인가? | Box<T> — 가장 단순, 오버헤드 0 | 공유 소유 필요 → |
| 멀티스레드 공유? | Arc<T> — 원자적 참조 카운팅 | Rc<T> — 비원자적 (더 빠름) |
| 내부 가변성 필요? | RefCell<T> (단일스레드) / Mutex<T> (멀티스레드) | 불변 공유 참조로 충분 |
| 읽기 주로, 가끔 수정? | Cow<T> — 수정 시에만 복사 | 항상 수정 → 소유권 이전 |
| 커널 환경? | kernel::sync::Arc (Pin 필요), kernel::sync::Mutex | std 스마트 포인터 사용 |
Deref 강제 변환, Drop, Weak 참조
스마트 포인터의 핵심 동작은 Deref, DerefMut, Drop 트레이트에 의해 결정됩니다. 또한 Weak<T>는 순환 참조 문제를 해결하는 핵심 도구입니다.
Deref 트레이트와 강제 변환(Deref Coercion)
Deref 트레이트를 구현하면 스마트 포인터를 일반 참조처럼 사용할 수 있습니다. 컴파일러가 자동으로 역참조 강제 변환(deref coercion) 체인을 적용합니다.
| 변환 규칙 | Deref 체인 | 결과 |
|---|---|---|
&T → &U | T: Deref<Target=U> | 불변 역참조 |
&mut T → &mut U | T: DerefMut<Target=U> | 가변 역참조 |
&mut T → &U | T: Deref<Target=U> | 가변→불변 (안전) |
&T → &mut U | 불가능 — 불변→가변 변환 금지 (안전성 위반) | |
// Deref 강제 변환 체인 예시
use std::ops::Deref;
// String → &str 자동 변환
fn greet(name: &str) {
println!("안녕하세요, {}!", name);
}
let owned = String::from("Rust");
greet(&owned); // &String → &str (Deref 자동 적용)
// Box<T> → &T 자동 변환
let boxed = Box::new(String::from("world"));
greet(&boxed); // &Box<String> → &String → &str (연쇄 변환!)
// DerefMut 예시 — 가변 역참조
fn push_exclaim(s: &mut String) {
s.push('!');
}
let mut boxed_str = Box::new(String::from("hello"));
push_exclaim(&mut boxed_str); // &mut Box<String> → &mut String (DerefMut)
// 사용자 정의 Deref 구현
struct MyBox<T>(T);
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
let my = MyBox(String::from("hello"));
greet(&my); // &MyBox<String> → &String → &str
Drop 트레이트: 소멸 순서와 규칙
Drop 트레이트는 값이 스코프를 벗어날 때 자동으로 호출됩니다. 선언 역순으로 드롭되며, Copy와 상호 배타적입니다.
// Drop 순서: 선언의 역순 (스택 LIFO)
struct Resource {
name: String,
}
impl Drop for Resource {
fn drop(&mut self) {
println!("해제: {}", self.name);
}
}
{
let a = Resource { name: "A".into() }; // 1번째 선언
let b = Resource { name: "B".into() }; // 2번째 선언
let c = Resource { name: "C".into() }; // 3번째 선언
}
// 출력 순서: 해제: C → 해제: B → 해제: A (역순!)
// 명시적 조기 해제: std::mem::drop()
let lock = mutex.lock().unwrap();
// ... 임계 영역 작업 ...
drop(lock); // 스코프 끝까지 기다리지 않고 즉시 해제
// 이후 다른 스레드가 락 획득 가능
// ⚠️ Drop + Copy 상호 배타
// #[derive(Copy, Clone)] // 컴파일 에러!
// struct Resource { ... } // Drop이 있으면 Copy 불가
// 이유: Copy는 비트 복사, Drop은 정리 로직 → 이중 해제 위험
Drop 규칙 요약: ① 변수는 선언 역순으로 드롭 ② 구조체 필드는 선언 순서로 드롭 ③ std::mem::drop()으로 조기 해제 가능 ④ Drop을 구현하면 Copy 불가 ⑤ ManuallyDrop<T>로 자동 드롭 방지 가능
Weak<T>: 순환 참조 해결
Rc<T>나 Arc<T>만 사용하면 순환 참조가 발생하여 메모리 누수가 생길 수 있습니다. Weak<T>는 참조 카운트를 증가시키지 않으므로 이 문제를 해결합니다.
use std::rc::{Rc, Weak};
use std::cell::RefCell;
// 트리 구조: 부모는 Weak, 자식은 Rc
struct Node {
value: i32,
parent: RefCell<Weak<Node>>, // ← Weak로 부모 참조 (순환 방지!)
children: RefCell<Vec<Rc<Node>>>, // ← Rc로 자식 소유
}
let parent = Rc::new(Node {
value: 1,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
let child = Rc::new(Node {
value: 2,
parent: RefCell::new(Rc::downgrade(&parent)), // Weak 생성
children: RefCell::new(vec![]),
});
parent.children.borrow_mut().push(Rc::clone(&child));
// 부모 접근: Weak → Option<Rc<Node>>
if let Some(p) = child.parent.borrow().upgrade() {
println!("부모 값: {}", p.value); // 부모 값: 1
}
// 참조 카운트 확인
println!("parent strong={}, weak={}",
Rc::strong_count(&parent), // 1 (child의 Weak는 카운트 안 함!)
Rc::weak_count(&parent)); // 1 (child가 Weak 보유)
// 원본이 해제된 후 Weak 사용
let weak_ref: Weak<i32>;
{
let strong = Rc::new(42);
weak_ref = Rc::downgrade(&strong);
assert!(weak_ref.upgrade().is_some()); // Some(42)
} // strong 해제됨
assert!(weak_ref.upgrade().is_none()); // None — 안전하게 실패!
커널에서의 Weak: 리눅스 커널 Rust 바인딩에서는 kernel::sync::Arc에 대응하는 ARef를 사용하며, 약한 참조 패턴은 커널 오브젝트의 생존 추적(try_get 등)으로 구현합니다. 트리 구조의 부모-자식 관계에서 부모를 Weak로 참조하는 패턴은 커널 디바이스 트리에서도 동일하게 적용됩니다.
동시성 프로그래밍 (Thread, Channel, Mutex, RwLock)
Rust의 "Fearless Concurrency"는 소유권 시스템이 데이터 레이스를 컴파일 타임에 방지하므로, 동시성 프로그래밍에서 흔히 발생하는 버그를 원천 차단합니다.
// 스레드 생성과 join
use std::thread;
let handle = thread::spawn(|| {
println!("다른 스레드에서 실행");
42
});
let result = handle.join().unwrap(); // 42
// Arc + Mutex — 멀티스레드 공유 상태
use std::sync::{Arc, Mutex};
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
// MutexGuard가 스코프를 벗어나면 자동 unlock (RAII)
}));
}
for h in handles { h.join().unwrap(); }
println!("결과: {}", *counter.lock().unwrap()); // 10
// 채널 (mpsc: Multiple Producer, Single Consumer)
use std::sync::mpsc;
let (tx, rx) = mpsc::channel();
let tx2 = tx.clone(); // 다중 전송자
thread::spawn(move || { tx.send("hello").unwrap(); });
thread::spawn(move || { tx2.send("world").unwrap(); });
for msg in rx.iter().take(2) {
println!("수신: {}", msg);
}
// scoped thread (Rust 1.63+) — 부모 스택 데이터를 안전하게 빌림
let data = vec![1, 2, 3];
thread::scope(|s| {
s.spawn(|| { println!("스레드 1: {:?}", &data); });
s.spawn(|| { println!("스레드 2: {:?}", &data); });
});
// 스코프 종료 시 자동 join — data 여전히 유효
// RwLock — 동시 읽기 허용, 쓰기는 배타적
use std::sync::RwLock;
let config = Arc::new(RwLock::new(Config::default()));
// 여러 스레드가 동시에 읽기 가능
let read_guard = config.read().unwrap();
println!("설정: {:?}", *read_guard);
drop(read_guard); // 명시적 해제 (보통 스코프가 하지만)
// 쓰기는 모든 읽기가 끝난 후 배타적으로
let mut write_guard = config.write().unwrap();
*write_guard = Config::new("updated");
// Atomic — 잠금 없는(lock-free) 동기화
use std::sync::atomic::{AtomicUsize, Ordering};
static REQUEST_COUNT: AtomicUsize = AtomicUsize::new(0);
fn handle_request() {
let count = REQUEST_COUNT.fetch_add(1, Ordering::Relaxed);
println!("요청 #{}", count + 1);
}
// Ordering 종류 (동기화 강도 순)
// Relaxed: 순서 보장 없음 (카운터에 적합)
// Acquire: 이후 읽기/쓰기가 이전으로 재배치 안 됨
// Release: 이전 읽기/쓰기가 이후로 재배치 안 됨
// AcqRel: Acquire + Release
// SeqCst: 모든 스레드에서 동일한 순서 보장 (가장 강력, 가장 느림)
| 프리미티브 | 사용 시나리오 | 커널 대응 | 특징 |
|---|---|---|---|
Mutex<T> | 공유 상태 보호 | kernel::sync::Mutex | 상호 배제, RAII 자동 unlock |
RwLock<T> | 읽기 다수 / 쓰기 소수 | kernel::sync::RwLock (제한적) | 동시 읽기 허용, 쓰기 배타적 |
mpsc::channel() | 스레드 간 메시지 전달 | 커널 workqueue | 다중 생산자, 단일 소비자 |
AtomicUsize | 잠금 없는 카운터/플래그 | core::sync::atomic | Lock-free, 오버헤드 최소 |
Barrier | 스레드 동기화 지점 | 커널 barrier/completion | 모든 스레드가 도달할 때까지 대기 |
Condvar | 조건부 대기 | wait_queue | 특정 조건이 될 때까지 대기 |
커널 동시성 차이: 커널에서는 std::thread와 std::sync를 사용할 수 없습니다. 대신 kernel::sync::Mutex(lockdep 통합), kernel::sync::SpinLock, workqueue, kthread를 사용합니다. 커널 Mutex는 Pin이 필요하며, new_mutex! 매크로로 생성합니다. Ordering은 core::sync::atomic에서 그대로 사용 가능합니다.
Mutex 패턴, 데드락 방지, 조건 변수
실전 동시성 프로그래밍에서는 Mutex 중독(poisoning), 데드락(deadlock), 조건부 대기 등 다양한 문제에 대응해야 합니다. Rust의 타입 시스템이 데이터 레이스를 방지하지만, 논리적 데드락은 프로그래머가 직접 관리해야 합니다.
Mutex Poisoning (중독)
스레드가 Mutex를 잡고 있는 상태에서 패닉하면, 해당 Mutex는 중독(poisoned) 상태가 됩니다. 이후 lock() 호출은 PoisonError를 반환합니다.
use std::sync::{Arc, Mutex};
use std::thread;
let data = Arc::new(Mutex::new(0));
// 스레드가 락을 잡고 패닉
let data_clone = Arc::clone(&data);
let _ = thread::spawn(move || {
let mut guard = data_clone.lock().unwrap();
*guard = 42;
panic!("스레드 패닉!"); // 락을 잡은 채 패닉 → Mutex 중독
}).join();
// 중독된 Mutex 접근 — Err(PoisonError) 반환
match data.lock() {
Ok(guard) => println!("정상: {}", *guard),
Err(poisoned) => {
// 복구 패턴 1: 중독된 데이터에 접근
let guard = poisoned.into_inner();
println!("복구된 값: {}", *guard); // 복구된 값: 42
}
}
// 복구 패턴 2: unwrap_or_else로 간결하게
let mut guard = data.lock().unwrap_or_else(|e| {
eprintln!("Mutex 중독 복구: {:?}", e);
e.into_inner() // 중독 상태 무시하고 데이터 접근
});
*guard = 0; // 데이터를 안전한 상태로 리셋
데드락 방지 전략
Rust 컴파일러는 데이터 레이스를 방지하지만, 데드락은 타입 시스템으로 감지할 수 없습니다. 프로그래머가 직접 방지 전략을 적용해야 합니다.
use std::sync::{Arc, Mutex};
// ❌ 데드락 발생: 락 순서가 서로 다름
let lock_a = Arc::new(Mutex::new(1));
let lock_b = Arc::new(Mutex::new(2));
// Thread 1: A → B 순서로 락
// Thread 2: B → A 순서로 락 ← 데드락!
// ✅ 해결 1: 항상 같은 순서로 락 획득
// 모든 스레드에서 반드시 lock_a → lock_b 순서 유지
let _guard_a = lock_a.lock().unwrap();
let _guard_b = lock_b.lock().unwrap();
// ✅ 해결 2: try_lock()으로 비차단 시도
let guard_a = lock_a.lock().unwrap();
match lock_b.try_lock() {
Ok(guard_b) => {
// 두 락 모두 획득 성공 — 작업 수행
println!("A={}, B={}", *guard_a, *guard_b);
}
Err(_) => {
// lock_b 획득 실패 — guard_a도 해제하고 재시도
drop(guard_a);
eprintln!("락 획득 실패, 재시도 필요");
}
}
// ✅ 해결 3: 스코프를 줄여서 락 보유 시간 최소화
let value = {
let guard = lock_a.lock().unwrap();
*guard // 값을 복사하고 즉시 락 해제
};
// 이후 value는 락 없이 자유롭게 사용
셀프 데드락 주의: 같은 스레드에서 동일한 Mutex를 두 번 lock()하면 데드락이 발생합니다. Rust의 std::sync::Mutex는 재진입(reentrant)을 지원하지 않습니다. 커널에서는 lockdep이 이러한 잠금 순서 위반을 런타임에 감지합니다.
Condvar (조건 변수)
Condvar는 특정 조건이 충족될 때까지 스레드를 효율적으로 대기시킵니다. 바쁜 대기(busy-wait) 대신 OS 수준에서 스레드를 잠재웁니다.
use std::sync::{Arc, Mutex, Condvar};
use std::thread;
// 생산자-소비자 패턴
let pair = Arc::new((Mutex::new(false), Condvar::new()));
// 소비자: 조건이 true가 될 때까지 대기
let pair_clone = Arc::clone(&pair);
let consumer = thread::spawn(move || {
let (lock, cvar) = &*pair_clone;
let mut ready = lock.lock().unwrap();
while !*ready {
ready = cvar.wait(ready).unwrap(); // 락 해제 + 대기 + 재획득
}
println!("데이터 준비 완료!");
});
// 생산자: 조건을 true로 설정하고 알림
let (lock, cvar) = &*pair;
{
let mut ready = lock.lock().unwrap();
*ready = true;
}
cvar.notify_one(); // 대기 중인 스레드 하나 깨움
// cvar.notify_all(); — 모든 대기 스레드 깨움
consumer.join().unwrap();
Barrier: 스레드 동기화 지점
use std::sync::{Arc, Barrier};
use std::thread;
// 4개 스레드가 모두 도착해야 진행
let barrier = Arc::new(Barrier::new(4));
let mut handles = vec![];
for i in 0..4 {
let b = Arc::clone(&barrier);
handles.push(thread::spawn(move || {
println!("스레드 {} 작업 시작", i);
// ... 1단계 작업 ...
b.wait(); // 모든 스레드가 여기 도달할 때까지 대기
println!("스레드 {} 2단계 진행", i); // 모두 동시에 시작
}));
}
for h in handles { h.join().unwrap(); }
실전 패턴: 생산자-소비자 (mpsc)
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
// mpsc: Multiple Producer, Single Consumer
let (tx, rx) = mpsc::channel();
// 다중 생산자 — tx를 clone하여 여러 스레드에서 전송
for i in 0..3 {
let tx_clone = tx.clone();
thread::spawn(move || {
thread::sleep(Duration::from_millis(i * 100));
tx_clone.send(format!("메시지 {}", i)).unwrap();
});
}
drop(tx); // 원본 tx 해제 — 모든 생산자 종료 시 rx가 종료됨
// 소비자 — 모든 메시지 수신 (채널 닫힐 때까지)
for msg in rx {
println!("수신: {}", msg);
}
// 동기 채널: sync_channel(버퍼_크기)
let (tx, rx) = mpsc::sync_channel(2); // 버퍼 2개
// 버퍼가 가득 차면 send()가 블록됨 → 배압(backpressure) 제공
동기화 프리미티브 비교
| 프리미티브 | 동시 읽기 | 동시 쓰기 | 성능 특성 | 사용 시나리오 |
|---|---|---|---|---|
Mutex<T> | 불가 | 불가 | OS futex 기반, 컨텍스트 스위치 가능 | 짧은 임계 영역, I/O 대기 가능 |
RwLock<T> | 허용 | 불가 | 읽기 다수 시 Mutex보다 유리 | 읽기 95% 이상, 쓰기 드문 경우 |
SpinLock (커널) | 불가 | 불가 | 바쁜 대기, 컨텍스트 스위치 없음 | 인터럽트 핸들러, 극히 짧은 구간 |
Atomic* | 허용 | 허용 | Lock-free, 하드웨어 원자 명령어 | 카운터, 플래그, 단순 상태 |
RCU (커널) | 허용 | 불가 (교체) | 읽기 오버헤드 거의 0 | 읽기 매우 빈번, 쓰기 극히 드문 경우 |
mpsc::channel | 소유권 이전 | 큐 기반, 락 없는 전달 | 스레드 간 메시지 패싱 | |
락 선택 가이드: ① 단순 카운터/플래그 → Atomic* ② 읽기 위주 공유 데이터 → RwLock ③ 읽기/쓰기 비슷하거나 I/O 포함 → Mutex ④ 스레드 간 데이터 전달 → mpsc::channel ⑤ 커널 인터럽트 컨텍스트 → SpinLock (슬립 금지)
모듈 시스템과 크레이트
Rust의 모듈 시스템은 코드를 논리적으로 구조화하고, 가시성(visibility)을 제어합니다. 크레이트(crate)는 컴파일의 최소 단위입니다.
// 모듈 선언 — 파일 시스템과 매핑
// src/lib.rs (크레이트 루트)
pub mod network { // → src/network.rs 또는 src/network/mod.rs
pub mod tcp { // → src/network/tcp.rs
pub fn connect() { /* ... */ }
fn internal() { /* 비공개 */ }
}
pub mod udp; // → src/network/udp.rs
}
// 가시성 (Visibility)
pub // 어디서든 접근 가능
pub(crate) // 같은 크레이트 내에서만
pub(super) // 부모 모듈에서만
pub(in path) // 지정한 경로에서만
// (기본) 같은 모듈 내에서만 — 비공개
// use — 경로 단축
use std::collections::HashMap;
use std::io::{Read, Write, BufReader}; // 그룹 임포트
use crate::network::tcp; // 절대 경로 (크레이트 루트 기준)
use super::config; // 상대 경로 (부모 모듈 기준)
use std::fmt::Result as FmtResult; // 이름 충돌 방지
// 크레이트 타입
// - 바이너리 크레이트: src/main.rs (실행 파일)
// - 라이브러리 크레이트: src/lib.rs (라이브러리)
// - 워크스페이스: 여러 크레이트를 하나의 프로젝트로 관리
Cargo.toml과 의존성 관리
# Cargo.toml — 크레이트 매니페스트 파일
[package]
name = "my-project"
version = "0.1.0"
edition = "2021" # Rust 에디션 (2015, 2018, 2021, 2024)
[dependencies]
serde = { version = "1.0", features = ["derive"] } # 기능 선택
tokio = { version = "1", features = ["full"] }
[dev-dependencies] # 테스트/벤치마크에서만 사용
criterion = "0.5"
[workspace] # 워크스페이스 (다수 크레이트 관리)
members = ["core", "cli", "web"]
| 모듈 관련 키워드 | 의미 | 예시 |
|---|---|---|
mod | 모듈 선언 | mod network; → network.rs 로드 |
pub | 공개 가시성 | pub fn connect() |
pub(crate) | 크레이트 내 공개 | pub(crate) struct Inner |
use | 경로 단축 (import) | use std::collections::HashMap; |
crate:: | 크레이트 루트 절대 경로 | use crate::network::tcp; |
super:: | 부모 모듈 상대 경로 | use super::config; |
self:: | 현재 모듈 | use self::helper::parse; |
Rust 에디션(Edition) 비교
| 에디션 | 출시 | 주요 변경 | 커널 사용 |
|---|---|---|---|
| 2015 | 2015 | Rust 1.0 기반 | 미사용 |
| 2018 | 2018 | async/await, 모듈 경로 개선, NLL | 미사용 |
| 2021 | 2021 | 클로저 캡처 개선, IntoIterator 배열, or_patterns | 현재 사용 |
| 2024 | 2024 | gen 블록, unsafe_op_in_unsafe_fn 기본, RPIT 수명 | 향후 전환 예정 |
커널에서는 Cargo를 사용하지 않습니다. 커널 Rust 코드는 Kbuild 시스템으로 빌드되며, 외부 크레이트(crates.io)에 의존할 수 없습니다. 커널 전용 alloc과 kernel 크레이트만 사용 가능합니다. 에디션은 커널 빌드 설정에서 지정되며, 현재 2021 에디션을 사용합니다. 새 에디션으로의 전환은 커널 커뮤니티 합의를 거쳐 진행됩니다.
모듈 재내보내기, 워크스페이스, Cargo 기능
대규모 Rust 프로젝트에서는 모듈 재내보내기(re-export)로 깔끔한 공개 API를 구성하고, Cargo 워크스페이스로 다중 크레이트를 관리하며, feature 플래그로 조건부 컴파일을 제어합니다.
pub use를 이용한 API 파사드 패턴
내부 모듈 구조를 숨기고 사용자에게 평탄한 API를 제공하는 패턴입니다. 라이브러리의 lib.rs에서 내부 모듈의 타입을 최상위로 재내보내기 합니다.
// src/lib.rs — 라이브러리 진입점
mod internal {
pub mod parser {
pub struct Parser { /* ... */ }
pub struct Token { /* ... */ }
pub fn parse(input: &str) -> Vec<Token> { /* ... */ vec![] }
}
pub mod codegen {
pub struct Generator { /* ... */ }
pub fn emit(tokens: &[super::parser::Token]) -> String { String::new() }
}
}
// 사용자는 mylib::Parser로 접근 (mylib::internal::parser::Parser 대신)
pub use internal::parser::{Parser, Token, parse};
pub use internal::codegen::{Generator, emit};
// 선택적 재내보내기 — 이름 변경도 가능
pub use internal::parser::Parser as MainParser;
모듈 가시성 경계와 구조 모범 사례
| 가시성 키워드 | 접근 범위 | 용도 |
|---|---|---|
pub | 외부 크레이트 포함 모든 곳 | 공개 API |
pub(crate) | 현재 크레이트 내부 | 크레이트 내부 공유 유틸리티 |
pub(super) | 부모 모듈 | 형제 모듈 간 공유 |
pub(in path) | 지정 경로 범위 | 세밀한 접근 제어 |
| (기본, 없음) | 현재 모듈 + 하위 모듈 | 구현 세부사항 은닉 |
// 권장 프로젝트 구조
// src/
// lib.rs ← pub use로 공개 API 정의
// error.rs ← pub(crate) 에러 타입
// config.rs ← pub(crate) 설정
// parser/
// mod.rs ← parser 모듈 진입점
// lexer.rs ← pub(super) 내부 구현
// ast.rs ← pub(super) AST 정의
// codegen/
// mod.rs
// backend.rs
mod parser;
mod codegen;
mod error;
// 공개 API만 재내보내기
pub use parser::Parser;
pub use codegen::Generator;
pub use error::Error;
Cargo 워크스페이스
워크스페이스는 여러 관련 크레이트를 하나의 프로젝트로 관리합니다. 공유 의존성, 빌드 캐시, 일관된 버전 관리가 가능합니다.
# 워크스페이스 루트 Cargo.toml
[workspace]
members = [
"crates/core",
"crates/cli",
"crates/server",
]
resolver = "2"
# 워크스페이스 수준 의존성 — 모든 멤버가 공유
[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
log = "0.4"
# 워크스페이스 수준 패키지 정보 상속
[workspace.package]
version = "0.1.0"
edition = "2021"
authors = ["Team <team@example.com>"]
license = "MIT OR Apache-2.0"
# 멤버 크레이트 Cargo.toml (crates/cli/Cargo.toml)
[package]
name = "myproject-cli"
version.workspace = true # 워크스페이스에서 상속
edition.workspace = true
authors.workspace = true
[dependencies]
myproject-core = { path = "../core" } # 워크스페이스 내 크레이트 참조
serde.workspace = true # 워크스페이스 의존성 상속
clap = { version = "4", features = ["derive"] } # 로컬 의존성
Cargo feature 플래그와 조건부 컴파일
# Cargo.toml — feature 정의
[features]
default = ["json"] # 기본 활성 기능
json = ["dep:serde_json"] # 선택적 의존성 활성화
yaml = ["dep:serde_yaml"]
full = ["json", "yaml"] # 복합 기능
unstable = [] # 의존성 없는 플래그
[dependencies]
serde = "1.0"
serde_json = { version = "1.0", optional = true }
serde_yaml = { version = "0.9", optional = true }
// src/lib.rs — feature에 따른 조건부 컴파일
#[cfg(feature = "json")]
pub mod json_support {
use serde_json;
pub fn to_json<T: serde::Serialize>(val: &T) -> String {
serde_json::to_string(val).unwrap()
}
}
#[cfg(feature = "yaml")]
pub mod yaml_support {
// yaml feature 활성 시에만 컴파일
}
// cfg를 표현식으로 조합
#[cfg(all(feature = "json", not(feature = "unstable")))]
pub fn stable_json_api() { /* ... */ }
#[cfg(any(feature = "json", feature = "yaml"))]
pub fn serialize() { /* ... */ }
// cfg_attr — 조건부 속성 적용
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Config {
name: String,
value: i32,
}
Cargo 프로파일
| 프로파일 | 명령어 | opt-level | debug | 용도 |
|---|---|---|---|---|
dev | cargo build | 0 | true | 개발/디버깅 (빠른 빌드) |
release | cargo build --release | 3 | false | 배포용 (최적화) |
test | cargo test | 0 | true | 테스트 실행 |
bench | cargo bench | 3 | false | 벤치마크 실행 |
# Cargo.toml — 프로파일 커스터마이징
[profile.dev]
opt-level = 1 # 개발 중에도 약간의 최적화
overflow-checks = true # 정수 오버플로 검사
[profile.release]
opt-level = 3 # 최대 최적화
lto = true # 링크 타임 최적화
codegen-units = 1 # 단일 코드젠 유닛 (더 나은 최적화)
strip = "symbols" # 심볼 제거 (바이너리 크기 감소)
panic = "abort" # unwinding 대신 abort (커널에서 필수)
# 커스텀 프로파일 (Rust 1.57+)
[profile.release-debug]
inherits = "release"
debug = true # 릴리스 최적화 + 디버그 심볼
커널에서의 모듈 구조: 리눅스 커널 Rust 코드는 Cargo를 사용하지 않지만, 모듈 가시성(pub, pub(crate))과 재내보내기(pub use)는 동일하게 사용합니다. 커널의 rust/kernel/ 디렉토리는 kernel 크레이트를 형성하며, lib.rs에서 각 하위 모듈을 pub mod로 선언합니다. 조건부 컴파일은 Cargo feature 대신 Kconfig 기반의 cfg 속성을 사용합니다.
매크로 시스템 (선언적 + 절차적)
Rust 매크로는 컴파일 타임에 코드를 생성합니다. 선언적 매크로(macro_rules!)는 패턴 매칭으로, 절차적 매크로는 토큰 스트림을 변환하여 코드를 생성합니다.
// 선언적 매크로 (macro_rules!)
macro_rules! my_vec {
// 빈 벡터
() => { Vec::new() };
// 쉼표로 구분된 요소들
( $( $x:expr ),+ $(,)? ) => {
{
let mut v = Vec::new();
$( v.push($x); )+
v
}
};
}
let v = my_vec![1, 2, 3]; // Vec::new() + push(1) + push(2) + push(3)
// 매크로 매개변수 지시자 (designators)
// $x:expr — 표현식
// $x:ident — 식별자
// $x:ty — 타입
// $x:pat — 패턴
// $x:stmt — 문장
// $x:block — 블록
// $x:item — 아이템 (함수, 구조체 등)
// $x:tt — 토큰 트리 (가장 유연)
// 반복 연산자: $( ... )* (0+회), $( ... )+ (1+회), $( ... )? (0 or 1)
// 절차적 매크로 — derive 예시 (별도 크레이트 필요)
// proc_macro_derive(MyTrait)를 구현하면:
#[derive(Debug, Clone, MyTrait)] // MyTrait 구현 코드가 자동 생성됨
struct Data { value: i32 }
// 커널에서의 매크로 활용
// module! { ... } — 커널 모듈 메타데이터 생성
// pin_init! { ... } — Pin 초기화 코드 생성
// #[vtable] — C vtable 래퍼 자동 생성
| 매크로 종류 | 선언 위치 | 호출 형태 | 대표 예시 | 커널 사용 |
|---|---|---|---|---|
| 선언적 | 같은 크레이트 | name!() | vec![], println!, matches! | pr_info!, new_mutex! |
| derive | 별도 proc-macro 크레이트 | #[derive(Name)] | #[derive(Debug, Clone)] | #[derive(Zeroable)] |
| 속성 | 별도 proc-macro 크레이트 | #[name] | #[test], #[tokio::main] | #[vtable], #[pin_data] |
| 함수형 | 별도 proc-macro 크레이트 | name!() | html! {}, sqlx::query!() | module! {}, pin_init! {} |
// 커널 module! 매크로 — 확장 결과 (개념)
module! {
type: MyModule,
name: "my_module",
license: "GPL",
}
// 위 매크로가 생성하는 코드 (개략):
// - #[used] static __LOG_PREFIX: &[u8] = b"my_module\0";
// - #[no_mangle] pub extern "C" fn init_module() → c_int
// - #[no_mangle] pub extern "C" fn cleanup_module()
// - MODULE_INFO 섹션 (.modinfo) 생성: license, author 등
// → C의 module_init()/module_exit()/MODULE_LICENSE() 조합을 대체
매크로 디버깅: cargo expand (cargo-expand 설치 필요)로 매크로 확장 결과를 확인할 수 있습니다. 커널에서는 make LLVM=1 rust-analyzer 후 IDE에서 매크로 확장을 볼 수 있습니다. macro_rules! 작성 시 $x:tt(토큰 트리)가 가장 유연한 지시자입니다.
테스트 작성과 실행
Rust는 언어 수준에서 테스트를 지원합니다. #[test] 속성으로 단위 테스트를 작성하고, cargo test로 실행하며, 통합 테스트와 문서 테스트까지 일관된 체계를 제공합니다.
단위 테스트 기본: #[test]와 테스트 모듈
관례적으로 각 소스 파일 하단에 #[cfg(test)] 모듈을 작성합니다. 이 모듈은 cargo test 실행 시에만 컴파일됩니다.
// src/lib.rs
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
pub fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err("0으로 나눌 수 없습니다".to_string())
} else {
Ok(a / b)
}
}
#[cfg(test)]
mod tests {
use super::*; // 부모 모듈의 모든 항목 가져오기
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
#[test]
fn test_add_negative() {
assert_eq!(add(-1, 1), 0);
}
#[test]
fn test_divide_ok() {
let result = divide(10.0, 3.0);
assert!(result.is_ok());
assert!((result.unwrap() - 3.333).abs() < 0.01);
}
#[test]
fn test_divide_by_zero() {
assert!(divide(1.0, 0.0).is_err());
}
}
assert 매크로 계열
| 매크로 | 용도 | 실패 시 출력 |
|---|---|---|
assert!(expr) | 표현식이 true인지 검증 | 조건 표현식 출력 |
assert_eq!(left, right) | 두 값이 같은지 비교 | left와 right 값 모두 출력 |
assert_ne!(left, right) | 두 값이 다른지 비교 | left와 right 값 모두 출력 |
debug_assert!(expr) | 디버그 빌드에서만 검증 | 릴리스 빌드에서 제거 |
assert!(expr, "msg {}", v) | 커스텀 실패 메시지 포함 | 지정한 메시지 출력 |
#[test]
fn test_custom_message() {
let value = 42;
assert!(
value > 0 && value < 100,
"값이 범위를 벗어남: {}, 기대 범위: 1~99",
value
);
assert_eq!(
value % 2, 0,
"{}는 짝수여야 합니다", value
);
assert_ne!(value, 0, "값이 0이면 안 됩니다");
}
#[should_panic]과 Result 반환 테스트
// 패닉이 예상되는 테스트
#[test]
#[should_panic]
fn test_panic() {
panic!("이 패닉은 의도된 것입니다");
}
// 특정 패닉 메시지를 검증
#[test]
#[should_panic(expected = "index out of bounds")]
fn test_index_panic() {
let v = vec![1, 2, 3];
let _ = v[99]; // 범위 초과 접근 → 패닉
}
// Result<T, E>를 반환하는 테스트 — ? 연산자 사용 가능
#[test]
fn test_with_result() -> Result<(), String> {
let result = divide(10.0, 2.0)?;
assert_eq!(result, 5.0);
Ok(())
}
// #[ignore] — 기본적으로 건너뛰기 (cargo test -- --ignored로 실행)
#[test]
#[ignore]
fn expensive_test() {
// 시간이 오래 걸리는 테스트
std::thread::sleep(std::time::Duration::from_secs(60));
}
통합 테스트와 문서 테스트
통합 테스트는 tests/ 디렉토리에 독립 파일로 작성합니다. 각 파일은 별도 크레이트처럼 취급되어 오직 공개 API만 테스트합니다.
// tests/integration_test.rs — 통합 테스트
use mylib::{add, divide}; // 공개 API만 접근 가능
#[test]
fn test_add_and_divide() {
let sum = add(10, 20);
let result = divide(sum as f64, 3.0).unwrap();
assert!((result - 10.0).abs() < 0.001);
}
// tests/common/mod.rs — 통합 테스트 공유 유틸리티
pub fn setup() -> TestContext {
// 테스트 환경 초기화
TestContext { /* ... */ }
}
/// 두 수를 더합니다.
///
/// # Examples
///
/// ```
/// use mylib::add;
///
/// assert_eq!(add(2, 3), 5);
/// assert_eq!(add(-1, 1), 0);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
/// # Examples 내에서 에러를 숨기는 패턴
///
/// ```
/// # fn main() -> Result<(), String> { // #으로 시작하면 문서에 표시 안 됨
/// let result = mylib::divide(10.0, 2.0)?;
/// assert_eq!(result, 5.0);
/// # Ok(())
/// # }
/// ```
테스트 조직화와 실행 옵션
| 테스트 유형 | 위치 | 접근 범위 | 실행 명령 |
|---|---|---|---|
| 단위 테스트 | 소스 파일 내 #[cfg(test)] mod tests | 비공개 함수 포함 전체 | cargo test |
| 통합 테스트 | tests/*.rs | 공개 API만 | cargo test --test 파일명 |
| 문서 테스트 | /// 주석 내 코드 블록 | 공개 API만 | cargo test --doc |
| 벤치마크 | benches/*.rs | 공개 API만 | cargo bench |
# 주요 cargo test 옵션
$ cargo test # 모든 테스트 실행
$ cargo test test_add # 이름에 "test_add" 포함된 테스트만
$ cargo test -- --nocapture # println! 출력 표시
$ cargo test -- --test-threads=1 # 직렬 실행 (병렬 방지)
$ cargo test -- --ignored # #[ignore] 테스트만 실행
$ cargo test -- --include-ignored # 무시된 테스트 포함 전체 실행
$ cargo test --lib # 단위 테스트만
$ cargo test --doc # 문서 테스트만
Criterion 벤치마크
안정적인 벤치마크를 위해 criterion 크레이트를 사용합니다. 표준 라이브러리의 벤치마크 기능은 nightly에서만 사용 가능하지만, criterion은 stable에서도 작동합니다.
// benches/my_benchmark.rs
use criterion::{criterion_group, criterion_main, Criterion};
use mylib::add;
fn bench_add(c: &mut Criterion) {
c.bench_function("add 두 수", |b| {
b.iter(|| add(20, 30))
});
}
fn bench_group(c: &mut Criterion) {
let mut group = c.benchmark_group("산술 연산");
for size in [10, 100, 1000] {
group.bench_with_input(
criterion::BenchmarkId::new("add", size),
&size,
|b, &s| b.iter(|| add(s, s)),
);
}
group.finish();
}
criterion_group!(benches, bench_add, bench_group);
criterion_main!(benches);
커널 Rust 테스트: 리눅스 커널의 Rust 코드는 KUnit 프레임워크와 통합된 테스트를 사용합니다. #[test] 대신 KUnit 매크로를 사용하며, 사용자 공간의 cargo test와는 다른 방식으로 실행됩니다. 커널 모듈 테스트는 samples/rust/ 디렉토리에서 예제를 확인할 수 있습니다.
OOP와 트레이트 객체 (dyn Trait)
Rust는 전통적인 클래스 기반 상속 대신 트레이트(trait)와 컴포지션을 통해 다형성을 구현합니다. 런타임 다형성이 필요할 때는 트레이트 객체(dyn Trait)를 사용하며, 이는 vtable 기반의 동적 디스패치를 수행합니다.
Rust의 OOP 접근법: 상속 대신 컴포지션
| 전통적 OOP (C++/Java) | Rust 방식 | 메커니즘 |
|---|---|---|
| 클래스 상속 | 트레이트 구현 | impl Trait for Type |
| 가상 함수 (vtable) | 트레이트 객체 | dyn Trait |
| 추상 클래스 | 트레이트 (기본 구현 포함) | trait T { fn f() { ... } } |
| 다중 상속 | 다중 트레이트 구현 | impl A for T + impl B for T |
| protected 멤버 | pub(crate) / pub(super) | 모듈 가시성 시스템 |
| 템플릿 메서드 패턴 | 트레이트 기본 메서드 + 필수 메서드 | 컴파일 타임 보장 |
트레이트 객체와 동적 디스패치
// 트레이트 정의
trait Shape {
fn area(&self) -> f64;
fn name(&self) -> &str;
}
struct Circle { radius: f64 }
struct Rectangle { width: f64, height: f64 }
impl Shape for Circle {
fn area(&self) -> f64 { std::f64::consts::PI * self.radius.powi(2) }
fn name(&self) -> &str { "원" }
}
impl Shape for Rectangle {
fn area(&self) -> f64 { self.width * self.height }
fn name(&self) -> &str { "직사각형" }
}
// 정적 디스패치 (제네릭) — 컴파일 타임에 타입 결정, 단형화(monomorphization)
fn print_area_static<T: Shape>(shape: &T) {
println!("{}의 넓이: {:.2}", shape.name(), shape.area());
}
// 동적 디스패치 (트레이트 객체) — 런타임에 vtable로 메서드 호출
fn print_area_dynamic(shape: &dyn Shape) {
println!("{}의 넓이: {:.2}", shape.name(), shape.area());
}
// 이종(heterogeneous) 컬렉션 — 서로 다른 타입을 하나의 Vec에 저장
fn total_area(shapes: &[Box<dyn Shape>]) -> f64 {
shapes.iter().map(|s| s.area()).sum()
}
fn main() {
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 5.0 }),
Box::new(Rectangle { width: 3.0, height: 4.0 }),
];
println!("총 넓이: {:.2}", total_area(&shapes));
}
트레이트 객체 메모리 레이아웃
트레이트 객체는 팻 포인터(fat pointer)로 구현됩니다. 데이터 포인터와 vtable 포인터, 두 개의 포인터 크기(16바이트, 64비트 시스템)를 가집니다.
객체 안전성 규칙 (Object Safety)
모든 트레이트가 트레이트 객체(dyn Trait)로 사용될 수 있는 것은 아닙니다. 객체 안전성을 만족해야 합니다.
| 규칙 | 허용 | 금지 (객체 안전하지 않음) |
|---|---|---|
반환 타입에 Self | fn method(&self) -> i32 | fn clone(&self) -> Self |
| 제네릭 타입 매개변수 | fn method(&self, x: i32) | fn method<T>(&self, x: T) |
메서드에 &self 수신자 | fn f(&self), fn f(&mut self) | fn f() (self 없는 연관 함수) |
Sized 바운드 | trait T { } | trait T: Sized { } |
// 객체 안전한 트레이트
trait Drawable {
fn draw(&self);
fn bounding_box(&self) -> (f64, f64, f64, f64);
}
// 객체 안전하지 않은 트레이트 (dyn으로 사용 불가)
trait NotObjectSafe {
fn clone_self(&self) -> Self; // Self 반환 → 금지
fn compare<T>(&self, other: &T); // 제네릭 매개변수 → 금지
}
// 우회 방법: where Self: Sized로 특정 메서드를 제외
trait PartiallyObjectSafe {
fn draw(&self); // 객체 안전
fn clone_self(&self) -> Self where Self: Sized; // dyn에서 제외
}
상태 패턴 (State Pattern) 구현
// 트레이트 객체를 활용한 상태 패턴
trait State {
fn request_review(self: Box<Self>) -> Box<dyn State>;
fn approve(self: Box<Self>) -> Box<dyn State>;
fn content<'a>(&self, _post: &'a Post) -> &'a str { "" }
}
struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
fn new() -> Post {
Post { state: Some(Box::new(Draft {})), content: String::new() }
}
fn add_text(&mut self, text: &str) { self.content.push_str(text); }
fn content(&self) -> &str {
self.state.as_ref().unwrap().content(self)
}
fn request_review(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.request_review());
}
}
fn approve(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.approve());
}
}
}
// 각 상태 구현
struct Draft {}
impl State for Draft {
fn request_review(self: Box<Self>) -> Box<dyn State> { Box::new(PendingReview {}) }
fn approve(self: Box<Self>) -> Box<dyn State> { self } // 초안은 승인 불가
}
struct PendingReview {}
impl State for PendingReview {
fn request_review(self: Box<Self>) -> Box<dyn State> { self }
fn approve(self: Box<Self>) -> Box<dyn State> { Box::new(Published {}) }
}
struct Published {}
impl State for Published {
fn request_review(self: Box<Self>) -> Box<dyn State> { self }
fn approve(self: Box<Self>) -> Box<dyn State> { self }
fn content<'a>(&self, post: &'a Post) -> &'a str { &post.content }
}
Newtype 패턴
외부 타입에 트레이트를 구현하거나, 타입에 추가 의미를 부여할 때 사용합니다. 고아 규칙(orphan rule)을 우회하는 관용적 방법입니다.
// Newtype으로 외부 타입에 트레이트 구현
use std::fmt;
struct Wrapper(Vec<String>); // Vec<String>을 감싸는 새 타입
impl fmt::Display for Wrapper {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}
// 타입 안전성 강화 — 같은 기본 타입을 의미론적으로 구분
struct Meters(f64);
struct Kilometers(f64);
impl Meters {
fn to_km(&self) -> Kilometers { Kilometers(self.0 / 1000.0) }
}
// Deref 구현으로 내부 타입 메서드 위임
use std::ops::Deref;
impl Deref for Wrapper {
type Target = Vec<String>;
fn deref(&self) -> &Vec<String> { &self.0 }
}
let w = Wrapper(vec!["hello".to_string(), "world".to_string()]);
println!("길이: {}", w.len()); // Deref로 Vec의 len() 직접 호출
println!("표시: {}", w); // Display 구현 사용
정적 vs 동적 디스패치 선택 기준: 컴파일 타임에 타입을 알 수 있다면 제네릭(impl Trait)을 사용하세요. 단형화로 인라인 최적화가 가능합니다. 이종 컬렉션이 필요하거나 타입이 런타임에 결정될 때만 dyn Trait을 사용하세요. 트레이트 객체는 vtable 간접 호출 오버헤드가 있으며, 인라인이 불가능합니다. 커널에서는 성능이 중요하므로 가능하면 정적 디스패치를 선호합니다.
메모리 레이아웃 (repr, size_of, align_of, 패딩)
시스템 프로그래밍과 커널 개발에서 데이터의 메모리 레이아웃을 정확히 제어하는 것은 필수입니다. Rust는 #[repr] 속성으로 레이아웃을 명시적으로 지정할 수 있습니다.
// 메모리 크기와 정렬 확인
use std::mem;
println!("i32: size={}, align={}", mem::size_of::<i32>(), mem::align_of::<i32>());
println!("bool: size={}, align={}", mem::size_of::<bool>(), mem::align_of::<bool>());
println!("&str: size={}, align={}", mem::size_of::<&str>(), mem::align_of::<&str>());
// &str = fat pointer: 포인터(8) + 길이(8) = 16 바이트
// #[repr(C)] — C ABI 호환 레이아웃 (커널 FFI 필수)
#[repr(C)]
struct CCompatible {
a: u8, // offset 0, padding 3
b: u32, // offset 4
c: u8, // offset 8, padding 3
} // 총 12 바이트
// #[repr(packed)] — 패딩 제거
#[repr(packed)]
struct Packed {
a: u8, b: u32, c: u8,
} // 6 바이트 (비정렬 접근 → 일부 아키텍처에서 느리거나 위험)
// 열거형의 최적화
println!("Option<Box<i32>>: {}", mem::size_of::<Option<Box<i32>>>());
// = 8 바이트! (Box가 null이 될 수 없으므로 None을 0으로 표현 — 널 포인터 최적화)
// #[repr(C, align(64))] — 캐시라인 정렬
#[repr(C, align(64))]
struct CacheAligned {
data: [u8; 32],
} // 64 바이트 (캐시라인 경계에 정렬)
repr 속성 비교
| repr | 필드 순서 | 패딩 | 크기 | FFI 호환 | 주요 용도 |
|---|---|---|---|---|---|
repr(Rust) (기본) | 컴파일러 재배치 | 최적화 | 최소 | 불가 | 일반 Rust 코드 |
repr(C) | 선언 순서 유지 | C 규칙 | C와 동일 | 호환 | FFI, 커널 바인딩 |
repr(packed) | 선언 순서 | 없음 | 필드 합 | 부분적 | 프로토콜 헤더, 하드웨어 레지스터 |
repr(transparent) | 단일 필드 | 내부와 동일 | 내부와 동일 | 내부 타입에 준함 | 뉴타입 패턴 |
repr(align(N)) | 조합 가능 | 정렬 패딩 | N의 배수 | 조합 가능 | 캐시라인 정렬, SIMD |
repr(u8/u16/...) | enum 전용 | 판별자 크기 지정 | 명시적 | 호환 | C enum 바인딩 |
제로 크기 타입(ZST)과 니치 최적화
// === 제로 크기 타입 (Zero-Sized Type, ZST) ===
use std::mem;
// 유닛 타입 — 크기 0
assert_eq!(mem::size_of::<()>(), 0);
// PhantomData — 크기 0, 타입 시스템용 마커
use std::marker::PhantomData;
struct Slice<'a, T> {
ptr: *const T,
len: usize,
_marker: PhantomData<&'a T>, // 크기 0, 수명 'a와 T를 소유한 것처럼 동작
}
// PhantomData는 메모리를 차지하지 않으면서 수명/소유권 관계를 표현
// 커널에서 C 포인터를 래핑할 때 수명을 추적하는 데 핵심적
// Vec<()>는 카운터와 동일 — 원소당 0바이트
let v: Vec<()> = vec![(); 1000];
assert_eq!(mem::size_of_val(&*v), 0); // 데이터: 0 바이트!
// === 니치 최적화 (Niche Optimization) ===
// Option<T>의 크기를 T와 동일하게 만드는 최적화
// NonZero* 타입: 0이 아닌 값만 보유 → None에 0 사용
use std::num::NonZeroU64;
assert_eq!(mem::size_of::<Option<NonZeroU64>>(), 8); // u64와 동일!
assert_eq!(mem::size_of::<u64>(), 8); // 동일
// 참조/Box/포인터: null이 될 수 없음 → None에 null 사용
assert_eq!(mem::size_of::<Option<&i32>>(), 8); // 포인터 크기와 동일!
assert_eq!(mem::size_of::<Option<Box<i32>>>(), 8); // Box도 동일!
// bool: true(1)/false(0) → Option<bool>은 None에 2 사용
assert_eq!(mem::size_of::<Option<bool>>(), 1); // bool과 동일!
커널에서의 메모리 레이아웃: 커널 Rust에서 #[repr(C)]는 C 구조체와의 FFI에 필수입니다. Opaque<T>는 내부적으로 MaybeUninit<T>를 사용하여 C 타입과 동일한 크기·정렬을 보장합니다. PhantomData는 포인터의 수명을 추적하는 데 커널 코드 전반에서 사용됩니다. 니치 최적화 덕분에 Option<NonNull<T>>은 추가 메모리 없이 nullable 포인터를 표현합니다.
unsafe Rust 심층
unsafe는 Rust의 안전성 보증을 우회하는 명시적 탈출구입니다. 안전성을 포기하는 것이 아니라, 안전성 책임을 컴파일러에서 개발자로 이전하는 것입니다. 커널 개발에서 unsafe는 C FFI, 하드웨어 접근, 성능 최적화에 필수입니다.
// unsafe 블록 — 5가지 추가 능력
// 1. 원시 포인터 역참조
let x = 42;
let ptr: *const i32 = &x;
unsafe {
println!("*ptr = {}", *ptr); // 원시 포인터 역참조
}
// 2. unsafe 함수 호출
unsafe fn dangerous() { /* 호출자가 안전성 보장 */ }
unsafe { dangerous(); }
// 3. 가변 정적 변수 접근
static mut COUNTER: u32 = 0;
unsafe { COUNTER += 1; } // 데이터 레이스 위험 → Atomic* 권장
// 4. unsafe 트레이트 구현 (예: Send, Sync)
unsafe impl Send for MyType {} // 개발자가 스레드 안전성 보장
// 안전한 추상화로 감싸기 — 커널 코딩 관례
pub fn safe_wrapper(data: &[u8]) -> Result<u32, Error> {
if data.len() < 4 {
return Err(Error::InvalidInput);
}
// SAFETY: data.len() >= 4가 위에서 확인되었으므로
// data[0..4] 접근은 범위 내입니다.
let val = unsafe {
*(data.as_ptr() as *const u32)
};
Ok(val)
}
unsafe 5대 기능 상세
| 기능 | 위험성 | 커널 사용 빈도 | 대표 사례 |
|---|---|---|---|
| 원시 포인터 역참조 | 댕글링, 정렬 불량, 범위 초과 | 매우 높음 | MMIO 레지스터 접근, C 반환 포인터 사용 |
| unsafe 함수 호출 | 전제조건(precondition) 미충족 | 매우 높음 | C FFI 호출 (bindings::*), intrinsics |
| static mut 접근 | 데이터 레이스 | 낮음 (Atomic 권장) | 전역 카운터 (권장하지 않음) |
| unsafe trait 구현 | 트레이트 계약 미이행 | 중간 | unsafe impl Send/Sync, GlobalAlloc |
| union 필드 접근 | 잘못된 변형 읽기 | 낮음 | C union과의 FFI |
Soundness (건전성) 개념
// === Soundness: safe API가 UB를 유발하지 않는 성질 ===
// "safe 코드만으로는 절대 UB(정의되지 않은 동작)를 만들 수 없다"
// 나쁜 예: unsound한 safe 함수 (버그!)
pub fn bad_index(v: &Vec<u8>, i: usize) -> &u8 {
// UNSAFE BUG: i 범위 검사 없이 unsafe 접근
unsafe { v.get_unchecked(i) } // ← safe 인터페이스인데 UB 가능!
}
// 좋은 예: sound한 safe 함수
pub fn good_index(v: &Vec<u8>, i: usize) -> Option<&u8> {
if i < v.len() {
// SAFETY: i < v.len()이 검증되었으므로 범위 내 접근
Some(unsafe { v.get_unchecked(i) })
} else {
None // 범위 밖이면 None 반환 — UB 불가능
}
}
// 커널 Rust의 핵심 원칙:
// 1. safe API는 반드시 sound해야 함 (어떤 인자 조합도 UB 없음)
// 2. unsafe fn은 전제조건을 문서화하고 호출자에게 책임 이전
// 3. unsafe 블록은 SAFETY 주석으로 전제조건 충족을 증명
unsafe 위의 안전한 추상화 구축 패턴
Rust에서 unsafe는 "위험한 코드"가 아니라 "컴파일러에게 증명할 수 없는 불변 조건을 프로그래머가 보증한다"는 선언입니다. 핵심은 unsafe 코드를 안전한 인터페이스로 감싸서 나머지 코드가 안전하게 사용할 수 있게 만드는 것입니다.
SAFETY 주석 패턴
커널 Rust 코드에서는 모든 unsafe 블록에 // SAFETY: 주석을 필수로 작성합니다. 이 주석은 "왜 이 unsafe 사용이 안전한지"를 증명하며, 코드 리뷰에서 가장 중요하게 검토되는 부분입니다.
// === SAFETY 주석 패턴 — 커널 Rust의 핵심 문서화 규칙 ===
// 1. unsafe fn에는 # Safety 섹션으로 전제조건 문서화
/// 원시 포인터에서 슬라이스를 생성합니다.
///
/// # Safety
///
/// - `ptr`은 유효하며 `len * size_of::<T>()` 바이트 동안 읽기 가능해야 합니다.
/// - `ptr`의 메모리는 반환된 수명 동안 변경되지 않아야 합니다.
/// - `ptr`은 `T`에 대해 올바르게 정렬되어야 합니다.
pub unsafe fn slice_from_raw<'a, T>(
ptr: *const T,
len: usize,
) -> &'a [T] {
// SAFETY: 호출자가 ptr 유효성, 정렬, 수명을 보증함
unsafe { core::slice::from_raw_parts(ptr, len) }
}
// 2. unsafe 블록에는 SAFETY 주석으로 조건 충족 증명
fn safe_wrapper(v: &Vec<u8>, idx: usize) -> Option<u8> {
if idx < v.len() {
// SAFETY: idx < v.len()이 위에서 검증되었으므로
// get_unchecked는 범위 내 접근만 수행함
Some(unsafe { *v.get_unchecked(idx) })
} else {
None
}
}
// 3. unsafe_op_in_unsafe_fn 린트 (Rust 2024 에디션 기본값)
// unsafe fn 내부에서도 unsafe 블록을 명시적으로 사용해야 함
#![deny(unsafe_op_in_unsafe_fn)]
unsafe fn example(ptr: *const u32) -> u32 {
// 이전 에디션: *ptr 직접 사용 가능 (unsafe fn이므로)
// Rust 2024: 반드시 unsafe 블록으로 감싸야 함
// SAFETY: 호출자가 ptr 유효성을 보증함
unsafe { *ptr }
}
주요 unsafe 작업과 안전한 대안
| unsafe 작업 | 용도 | 위험 요소 | 안전한 대안 |
|---|---|---|---|
std::mem::transmute | 타입 간 비트 재해석 | UB 가능 (비유효 비트 패턴) | TryFrom, bytemuck::cast |
MaybeUninit<T> | 초기화 지연 | 미초기화 읽기 = UB | Option<T>, Default::default() |
ptr::read / write | 원시 포인터 읽기/쓰기 | dangling/null/정렬 오류 | &T / &mut T 참조 사용 |
ptr.offset(n) | 포인터 산술 | 범위 밖 = UB | 슬라이스 인덱싱, get() |
slice::from_raw_parts | 포인터→슬라이스 | 수명/정렬/유효성 | &[T] 직접 전달 |
impl Send/Sync | 스레드 안전성 선언 | 데이터 레이스 가능 | 자동 구현 사용 |
union 필드 접근 | 비트 필드 조작 | 잘못된 variant 읽기 | enum 사용 |
MaybeUninit과 transmute 안전 패턴
use core::mem::MaybeUninit;
// === MaybeUninit — 초기화를 지연하면서 안전하게 사용 ===
fn init_array() -> [u32; 4] {
let mut arr: [MaybeUninit<u32>; 4] = [MaybeUninit::uninit(); 4];
for (i, elem) in arr.iter_mut().enumerate() {
elem.write((i as u32) * 10); // 각 요소 초기화
}
// SAFETY: 위 루프에서 4개 요소 모두 write()로 초기화 완료
unsafe {
arr.map(|elem| elem.assume_init())
}
}
// === transmute 대신 안전한 패턴 ===
// 나쁜 예: transmute는 컴파일 타임에 크기만 검사
// let val: u32 = unsafe { std::mem::transmute(bytes) };
// 좋은 예: from_ne_bytes는 안전하고 의도가 명확
let bytes: [u8; 4] = [0x01, 0x02, 0x03, 0x04];
let val = u32::from_ne_bytes(bytes); // 안전한 변환!
// === 원시 포인터 산술 — 안전한 대안 ===
let data = [1u32, 2, 3, 4];
// 위험: offset은 범위 밖이면 UB
// let val = unsafe { *data.as_ptr().offset(2) };
// 안전한 대안: 슬라이스 인덱싱
let val = data.get(2).copied(); // Option<u32> 반환
Miri로 UB 검출
Miri는 Rust의 공식 UB(Undefined Behavior) 검출 도구입니다. MIR(Mid-level Intermediate Representation)을 해석 실행하여 메모리 안전성 위반, 데이터 레이스, 정렬 오류 등을 런타임에 감지합니다. cargo +nightly miri test로 실행하며, 특히 unsafe 코드의 soundness를 검증하는 데 필수적입니다.
// Miri가 감지하는 주요 UB 패턴:
// 1. 초기화되지 않은 메모리 읽기
// 2. dangling 포인터 역참조
// 3. 정렬 위반 (unaligned read/write)
// 4. 잘못된 enum discriminant
// 5. 가변 참조 에일리어싱 (Stacked Borrows 위반)
// === Miri로 검증하는 예시 ===
#[test]
fn test_safe_abstraction() {
let v = vec![1, 2, 3];
// 이 코드가 안전한지 Miri가 검증
let result = safe_wrapper(&v, 1);
assert_eq!(result, Some(2));
// 범위 밖 접근 — UB 없이 None 반환
let result = safe_wrapper(&v, 10);
assert_eq!(result, None);
}
// 실행: cargo +nightly miri test test_safe_abstraction
// Miri는 Stacked Borrows 모델로 참조 규칙 위반도 감지
unsafe_op_in_unsafe_fn 린트: Rust 2024 에디션부터 unsafe fn 본문 내에서도 unsafe 작업을 수행하려면 unsafe { } 블록을 명시해야 합니다. 이전 에디션에서는 unsafe fn 자체가 암묵적으로 전체 본문을 unsafe 블록으로 취급했지만, 이제는 각 unsafe 작업마다 SAFETY 주석을 작성하도록 강제하여 soundness 검증을 더 세밀하게 할 수 있습니다. 커널 Rust는 이 린트를 이미 deny 수준으로 적용하고 있습니다.
FFI (Foreign Function Interface)
FFI는 Rust와 다른 언어(주로 C) 사이의 함수 호출 인터페이스입니다. 커널 Rust 코드의 핵심 기반이며, bindgen이 C 헤더에서 Rust 바인딩을 자동 생성합니다.
// C 함수를 Rust에서 호출
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
extern "C" {
fn strlen(s: *const c_char) -> usize;
fn printf(format: *const c_char, ...) -> i32;
}
let c_string = CString::new("hello").unwrap();
let len = unsafe { strlen(c_string.as_ptr()) }; // 5
// Rust 함수를 C에서 호출 가능하게
#[no_mangle]
pub extern "C" fn rust_add(a: i32, b: i32) -> i32 {
a + b
}
// repr(C) 구조체 — C와 동일한 메모리 레이아웃
#[repr(C)]
pub struct Point {
pub x: f64,
pub y: f64,
}
// 콜백 패턴 — C 함수 포인터를 Rust 클로저로
type Callback = extern "C" fn(i32) -> i32;
extern "C" {
fn register_callback(cb: Callback);
}
extern "C" fn my_callback(value: i32) -> i32 {
value * 2
}
unsafe { register_callback(my_callback); }
커널 FFI 패턴: Opaque 타입과 Helper 함수
커널에서는 C 구조체를 직접 조작하는 대신 Opaque<T>로 감싸 Rust 코드가 C 내부 구현에 의존하지 않도록 합니다. C 매크로나 인라인 함수는 bindgen으로 생성할 수 없으므로, rust/helpers/에 C 헬퍼 래퍼를 작성합니다.
| 패턴 | C 측 | Rust 측 | 용도 |
|---|---|---|---|
| Opaque<T> | struct mutex | Opaque<bindings::mutex> | C 구조체를 불투명 타입으로 래핑, 내부 접근 차단 |
| Helper 함수 | static inline void mutex_lock() | rust_helper_mutex_lock() | 인라인 함수/매크로를 C 래퍼로 노출 |
| bindgen 바인딩 | extern int register_chrdev() | bindings::register_chrdev() | 일반 C 함수를 자동 바인딩 |
| repr(C) 구조체 | C 구조체와 동일 레이아웃 | #[repr(C)] struct Point { ... } | 양측에서 직접 접근하는 공유 데이터 |
| 콜백 등록 | void (*callback)(void *) | extern "C" fn callback(...) | C → Rust 방향 콜백 |
// === 커널 Opaque 타입 패턴 ===
use kernel::types::Opaque;
// C의 struct mutex를 Rust에서 불투명하게 래핑
// → Rust 코드가 C 구조체 필드에 직접 접근하지 못하게 차단
pub struct Mutex<T> {
mutex: Opaque<bindings::mutex>, // C mutex를 불투명하게 보유
data: UnsafeCell<T>, // 보호할 데이터
}
// Opaque<T>는 내부 C 구조체의 크기·정렬만 알고, 필드에 접근하지 않음
// → C 측 구조체가 변경되어도 Rust 코드 수정 최소화
// === Helper 함수 (rust/helpers/ 디렉터리) ===
// C 측 (rust/helpers/mutex.c):
// void rust_helper_mutex_lock(struct mutex *lock) {
// mutex_lock(lock); // 인라인 함수를 래핑
// }
// EXPORT_SYMBOL_GPL(rust_helper_mutex_lock);
//
// Rust 측 (bindgen이 자동 생성):
// extern "C" { fn rust_helper_mutex_lock(lock: *mut mutex); }
// === 안전한 추상화 완성 예시 ===
impl<T> Mutex<T> {
pub fn lock(&self) -> Guard<'_, T> {
// SAFETY: self.mutex는 init()에서 초기화되었으며,
// Guard의 수명이 &self의 수명에 묶여 있어
// mutex가 유효한 동안만 잠금이 유지됩니다.
unsafe { bindings::rust_helper_mutex_lock(self.mutex.get()); }
Guard { mutex: self }
}
}
// Guard의 Drop이 unlock을 보장 (RAII)
impl<T> Drop for Guard<'_, T> {
fn drop(&mut self) {
// SAFETY: lock()에서 잠금을 획득했으므로 해제가 유효합니다.
unsafe { bindings::rust_helper_mutex_unlock(self.mutex.mutex.get()); }
}
}
FFI 주의사항: C와 Rust 사이에서 전달하는 모든 타입은 ABI 호환성이 보장되어야 합니다. (1) #[repr(C)] 없는 Rust 구조체를 C에 전달하면 UB(정의되지 않은 동작), (2) C의 NULL 포인터를 Rust &T로 받으면 UB — 반드시 *const T로 받아 null 검사 후 변환, (3) C 문자열은 NUL 종단이 필수 — CStr/CString을 사용하세요.
async/await와 Future
Rust의 비동기 프로그래밍은 제로 코스트 추상화입니다. async fn은 컴파일 타임에 상태 머신으로 변환되며, 런타임(executor)이 Future를 폴링하여 실행합니다.
// Future 트레이트 (핵심)
trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output>;
}
enum Poll<T> {
Ready(T), // 완료 — 결과 반환
Pending, // 미완료 — Waker가 깨울 때까지 대기
}
// async fn — 컴파일러가 Future를 구현하는 상태 머신 생성
async fn fetch_data(url: &str) -> Result<String, Error> {
let response = http::get(url).await?; // 일시 중지 지점 1
let body = response.text().await?; // 일시 중지 지점 2
Ok(body)
}
// async 블록
let future = async {
let a = task_a().await;
let b = task_b().await;
a + b
};
// 동시 실행 — join!으로 여러 Future를 병렬 실행
let (a, b) = tokio::join!(
fetch_data("https://api1.example.com"),
fetch_data("https://api2.example.com"),
);
// select! — 가장 먼저 완료되는 Future 선택
tokio::select! {
result = task_a() => println!("A 완료: {:?}", result),
result = task_b() => println!("B 완료: {:?}", result),
}
async fn → 상태 머신 변환 과정
컴파일러가 async fn을 어떻게 상태 머신 enum으로 변환하는지 이해하면, async/await의 동작 원리를 깊이 파악할 수 있습니다.
// === 원본 async fn ===
async fn fetch_and_process(url: &str) -> Result<String, Error> {
let response = http_get(url).await?; // await 지점 1
let data = response.json().await?; // await 지점 2
Ok(format!("{:?}", data))
}
// === 컴파일러가 생성하는 상태 머신 (개념적) ===
enum FetchAndProcessState {
// 상태 0: 아직 시작 안 됨
Start { url: String },
// 상태 1: http_get() 대기 중 — 로컬 변수 url 보존
WaitingHttp { future: HttpGetFuture, url: String },
// 상태 2: json() 대기 중 — response 보존
WaitingJson { future: JsonFuture, response: Response },
// 상태 3: 완료
Done,
}
// 각 poll() 호출 시 현재 상태에서 다음 상태로 전이
// 장점: 힙 할당 최소화, 정확한 크기의 enum으로 표현
| async 개념 | 설명 | C/커널 대응 |
|---|---|---|
async fn | 호출 시 Future를 반환 (아직 실행 안 됨) | 작업 구조체 생성 |
.await | Future를 폴링, 미완료 시 제어권 반환 | schedule()로 양보 |
Executor | Future들을 스케줄링하고 폴링 | 커널 워크큐/스케줄러 |
Waker | I/O 완료 시 Executor에 알림 | 인터럽트/completion |
join! | 여러 Future를 동시에 폴링 | 여러 워크 아이템 동시 큐잉 |
select! | 가장 먼저 완료되는 Future 선택 | wait_event() + 조건 검사 |
Stream | 비동기 반복자 (여러 값을 순차 yield) | 링 버퍼에서 순차 읽기 |
커널에서의 async: 커널은 tokio 같은 사용자 공간 런타임을 사용할 수 없습니다. 커널 Rust에서는 workqueue, 타이머, completion 등 기존 커널 비동기 메커니즘을 Rust로 래핑하여 사용합니다. async/await 문법의 커널 지원은 아직 초기 단계이며, 현재는 커널 내부적으로 Poll 기반의 직접적인 상태 머신 패턴이 주로 사용됩니다.
Async 런타임과 실전 비동기 패턴
Rust의 async/await는 런타임에 독립적입니다. Future 트레이트는 표준 라이브러리에 정의되지만, 실제 실행은 별도의 런타임(executor)이 담당합니다. 사용자 공간에서는 tokio, async-std 등이 있고, 커널/임베디드에서는 embassy 같은 no_std 런타임을 사용합니다.
tokio vs async-std 비교
| 항목 | tokio | async-std |
|---|---|---|
| 실행 모델 | 멀티스레드 work-stealing | 멀티스레드 (tokio 기반으로 전환) |
| 타이머 | tokio::time::sleep | async_std::task::sleep |
| I/O | epoll/kqueue/IOCP 직접 통합 | tokio의 mio 사용 |
| 채널 | tokio::sync::mpsc | async_std::channel |
| 생태계 | 사실상 표준 (hyper, tonic, axum) | 소규모 |
| no_std | 불가 | 불가 |
| 커널 사용 | 불가 (사용자 공간 전용) | 불가 (사용자 공간 전용) |
select!와 join! 패턴
use tokio::time::{sleep, Duration, timeout};
// === select! — 가장 먼저 완료되는 Future 선택 ===
// 여러 비동기 작업 중 먼저 끝나는 하나만 처리
async fn race_example() {
tokio::select! {
result = fetch_from_primary() => {
println!("주 서버 응답: {:?}", result);
}
result = fetch_from_backup() => {
println!("백업 서버 응답: {:?}", result);
}
_ = sleep(Duration::from_secs(5)) => {
println!("타임아웃! 5초 초과");
}
}
// 하나가 완료되면 나머지는 자동으로 drop(취소)
}
// === join! — 모든 Future를 동시에 실행하고 전부 완료 대기 ===
async fn parallel_fetch() -> (Data, Data, Data) {
let (a, b, c) = tokio::join!(
fetch_users(),
fetch_orders(),
fetch_products(),
);
// 세 작업이 동시에 실행되며, 모두 완료될 때까지 대기
// 하나라도 panic하면 나머지도 중단
(a, b, c)
}
// === try_join! — Result를 반환하는 Future들의 동시 실행 ===
async fn parallel_fetch_fallible() -> Result<(), Error> {
let (users, orders) = tokio::try_join!(
fetch_users(), // Result<Vec<User>, Error>
fetch_orders(), // Result<Vec<Order>, Error>
)?; // 하나라도 Err이면 즉시 반환, 나머지 취소
Ok(())
}
Stream 트레이트 (비동기 반복자)
use tokio_stream::{StreamExt, Stream};
use std::pin::Pin;
// Stream은 비동기 버전의 Iterator
// Iterator: fn next(&mut self) -> Option<Item>
// Stream: fn poll_next(self: Pin<&mut Self>, cx) -> Poll<Option<Item>>
// === Stream 소비 패턴 ===
async fn process_stream(stream: impl Stream<Item = Event>) {
// StreamExt가 제공하는 어댑터 사용
let mut stream = stream
.filter(|e| e.is_important()) // 필터링
.map(|e| e.transform()) // 변환
.take(100); // 최대 100개
while let Some(item) = stream.next().await {
process(item).await;
}
}
// === async 채널을 Stream으로 변환 ===
async fn channel_as_stream() {
let (tx, rx) = tokio::sync::mpsc::channel::<String>(32);
let mut stream = tokio_stream::wrappers::ReceiverStream::new(rx);
while let Some(msg) = stream.next().await {
println!("수신: {}", msg);
}
}
no_std 비동기: 커널과 임베디드
// === Embassy — no_std async 런타임 (임베디드/커널용) ===
// tokio와 달리 힙 할당 없이 동작, 인터럽트 기반 실행
// Embassy의 executor는 컴파일 타임에 태스크 수가 결정됨
#[embassy_executor::task]
async fn blink_led(led: Output<'static>) {
loop {
led.set_high();
Timer::after_millis(500).await; // 비동기 대기 (CPU 슬립)
led.set_low();
Timer::after_millis(500).await;
}
}
// === 커널 Rust에서의 비동기 패턴 ===
// 커널은 자체 스케줄러가 있으므로 사용자 공간 런타임 불필요
// workqueue + completion을 Rust로 래핑하는 패턴
use kernel::sync::Completion;
struct AsyncIoRequest {
completion: Completion,
buffer: Vec<u8>,
}
impl AsyncIoRequest {
// Poll 기반 상태 머신 — Future 트레이트와 유사
fn poll(&self) -> Poll<&[u8]> {
if self.completion.is_done() {
Poll::Ready(&self.buffer)
} else {
Poll::Pending
}
}
}
실전 비동기 패턴: 타임아웃, 재시도, 백오프
use tokio::time::{timeout, sleep, Duration};
// === 타임아웃 패턴 ===
async fn fetch_with_timeout(url: &str) -> Result<Response, Error> {
match timeout(Duration::from_secs(10), http_get(url)).await {
Ok(Ok(resp)) => Ok(resp),
Ok(Err(e)) => Err(e),
Err(_) => Err(Error::Timeout),
}
}
// === 지수 백오프 재시도 패턴 ===
async fn retry_with_backoff<F, Fut, T, E>(
mut f: F,
max_retries: u32,
) -> Result<T, E>
where
F: FnMut() -> Fut,
Fut: Future<Output = Result<T, E>>,
{
let mut delay = Duration::from_millis(100);
for attempt in 0..max_retries {
match f().await {
Ok(val) => return Ok(val),
Err(e) if attempt == max_retries - 1 => return Err(e),
Err(_) => {
sleep(delay).await;
delay *= 2; // 지수 백오프: 100ms → 200ms → 400ms
}
}
}
unreachable!()
}
// 사용 예시
let result = retry_with_backoff(
|| fetch_with_timeout("https://api.example.com/data"),
3, // 최대 3회 재시도
).await;
Pin과 자기 참조 Future: async fn이 지역 변수에 대한 참조를 await 지점 이후에도 유지하면, 컴파일러가 생성한 Future는 자기 참조 구조체가 됩니다. 이 경우 Pin이 필수인데, 메모리 이동 시 내부 포인터가 무효화되기 때문입니다. Box::pin(async { ... })으로 힙에 고정하거나, tokio::pin! 매크로로 스택에 고정할 수 있습니다.
고급 타입 시스템 (Associated Types, HRTB, GAT, PhantomData)
Rust의 타입 시스템은 매우 표현력이 높으며, 타입 레벨에서 복잡한 불변 조건을 인코딩할 수 있습니다.
// 연관 타입(Associated Types) — 트레이트에서 출력 타입을 지정
trait Iterator {
type Item; // 연관 타입 — 구현체가 하나의 Item 타입을 지정
fn next(&mut self) -> Option<Self::Item>;
}
// vs 제네릭: trait Iter<T> { fn next() -> Option<T>; }
// 연관 타입은 구현체당 하나, 제네릭은 여러 개 구현 가능
// HRTB (Higher-Ranked Trait Bounds) — 모든 수명에 대해 트레이트 바운드
fn apply<F>(f: F)
where
F: for<'a> Fn(&'a str) -> &'a str, // 모든 수명 'a에 대해 적용
{
let s = String::from("hello");
println!("{}", f(&s));
}
// GAT (Generic Associated Types) — 연관 타입에 제네릭 적용
trait LendingIterator {
type Item<'a> where Self: 'a;
fn next<'a>(&'a mut self) -> Option<Self::Item<'a>>;
}
// PhantomData — 타입 수준 마커 (런타임 비용 0)
use std::marker::PhantomData;
struct Inches;
struct Meters;
struct Length<Unit> {
value: f64,
_unit: PhantomData<Unit>, // 크기 0, 타입 구분만 제공
}
impl<Unit> Length<Unit> {
fn new(value: f64) -> Self {
Length { value, _unit: PhantomData }
}
}
// Length<Inches>와 Length<Meters>는 다른 타입 → 단위 혼동 방지!
// 타입 상태 패턴 (Typestate Pattern) — 상태 전이를 타입으로 인코딩
struct Locked;
struct Unlocked;
struct Door<State> {
_state: PhantomData<State>,
}
impl Door<Locked> {
fn unlock(self) -> Door<Unlocked> { Door { _state: PhantomData } }
}
impl Door<Unlocked> {
fn open(&self) { println!("문 열림"); }
}
// Door<Locked>에서는 open() 호출 불가 → 컴파일 타임에 상태 검증!
커널에서의 타입 상태 패턴
// 커널 드라이버에서 타입 상태 패턴 활용 — 초기화 상태 관리
struct Uninitialized;
struct Initialized;
struct Active;
struct Device<State> {
base: IoMem,
irq: u32,
_state: PhantomData<State>,
}
impl Device<Uninitialized> {
fn init(self) -> Result<Device<Initialized>> {
// 하드웨어 초기화 시퀀스...
Ok(Device { base: self.base, irq: self.irq, _state: PhantomData })
}
// start()를 호출하면 컴파일 에러! 초기화 안 된 상태에서 시작 불가
}
impl Device<Initialized> {
fn start(self) -> Result<Device<Active>> {
// IRQ 등록, DMA 설정...
Ok(Device { base: self.base, irq: self.irq, _state: PhantomData })
}
}
impl Device<Active> {
fn read_status(&self) -> u32 { /* 레지스터 읽기 */ 0 }
fn stop(self) -> Device<Initialized> { /* 정지 */
Device { base: self.base, irq: self.irq, _state: PhantomData }
}
}
// 사용: Device::new().init()?.start()?.read_status()
// init() 없이 start() → 컴파일 에러 → 잘못된 순서를 타입이 방지!
| 고급 타입 기능 | 설명 | 활용 예시 |
|---|---|---|
| 연관 타입 | 트레이트에서 출력 타입 1개 지정 | Iterator { type Item; } |
| HRTB | 모든 수명에 대한 트레이트 바운드 | for<'a> Fn(&'a str) -> &'a str |
| GAT | 연관 타입에 제네릭 파라미터 | type Item<'a> where Self: 'a |
| PhantomData | 크기 0 타입 마커 | 단위 구분, 타입 상태 패턴 |
| 타입 상태 | 상태 전이를 타입으로 인코딩 | Device<Init> → Device<Active> |
| Newtype | 기존 타입을 새 타입으로 래핑 | struct Meters(f64) — 단위 혼동 방지 |
고급 타입 기법: HRTB, GAT, Sealed Trait
Rust의 타입 시스템은 Haskell에 버금가는 표현력을 제공합니다. Higher-Rank Trait Bounds(HRTB), Generic Associated Types(GAT), sealed trait 등 고급 패턴을 통해 컴파일 타임에 복잡한 불변 조건을 인코딩할 수 있습니다.
Higher-Rank Trait Bounds (HRTB)
HRTB는 for<'a> 문법으로 "모든 가능한 수명에 대해" 트레이트 바운드를 지정합니다. 주로 클로저나 함수 포인터를 인자로 받을 때 필요합니다.
// === HRTB — 모든 수명에 대해 동작하는 함수 바운드 ===
// 1. 기본 HRTB 패턴
fn apply_to_ref<F>(f: F, data: &[String])
where
F: for<'a> Fn(&'a str) -> &'a str, // 어떤 수명이든 처리
{
for s in data {
let result = f(s.as_str());
println!("{}", result);
}
}
// 2. HRTB가 필요한 이유 — 클로저와 수명의 관계
fn call_twice<F>(f: F)
where
F: for<'a> Fn(&'a i32) -> i32,
{
let x = 10;
f(&x); // 'a = x의 수명
{
let y = 20;
f(&y); // 'a = y의 수명 (x와 다름!)
}
// for<'a> 없이는 두 번의 호출에서 서로 다른 수명을 처리 불가
}
// 3. Fn 트레이트의 암묵적 HRTB
// 실제로 `Fn(&str) -> bool`은 `for<'a> Fn(&'a str) -> bool`의 축약형
fn filter_strings(data: &[String], predicate: impl Fn(&str) -> bool) {
// predicate는 암묵적으로 for<'a> Fn(&'a str) -> bool
for s in data.iter().filter(|s| predicate(s)) {
println!("{}", s);
}
}
Generic Associated Types (GAT)
// === GAT — 연관 타입에 제네릭 파라미터를 추가 ===
// Rust 1.65에서 안정화 (2022년)
// 문제: GAT 없이는 "빌린 데이터를 반환하는 반복자"를 표현 불가
trait LendingIterator {
// GAT: 연관 타입 Item이 수명 파라미터 'a를 가짐
type Item<'a> where Self: 'a;
fn next<'a>(&'a mut self) -> Option<Self::Item<'a>>;
}
// 구현: 슬라이스에서 윈도우를 빌려주는 반복자
struct WindowsMut<'w, T> {
data: &'w mut [T],
pos: usize,
size: usize,
}
impl<'w, T> LendingIterator for WindowsMut<'w, T> {
type Item<'a> = &'a mut [T] where Self: 'a;
fn next<'a>(&'a mut self) -> Option<&'a mut [T]> {
if self.pos + self.size <= self.data.len() {
let window = &mut self.data[self.pos..self.pos + self.size];
self.pos += 1;
Some(window)
} else {
None
}
}
}
// === GAT의 또 다른 활용: async 트레이트 (Rust 1.75 이전) ===
trait AsyncDatabase {
type GetFuture<'a>: Future<Output = Option<Vec<u8>>> where Self: 'a;
fn get<'a>(&'a self, key: &'a str) -> Self::GetFuture<'a>;
}
PhantomData를 활용한 타입 레벨 프로그래밍
use std::marker::PhantomData;
// === PhantomData — 크기 0으로 타입 정보를 전달 ===
// 1. 단위 구분 패턴
struct Meters;
struct Seconds;
struct Measurement<Unit> {
value: f64,
_unit: PhantomData<Unit>, // 크기 0, 컴파일 타임에만 존재
}
impl<Unit> Measurement<Unit> {
fn new(value: f64) -> Self {
Measurement { value, _unit: PhantomData }
}
}
// 다른 단위끼리 연산하면 컴파일 에러!
fn add_same_unit<U>(
a: Measurement<U>,
b: Measurement<U>, // 같은 Unit만 허용
) -> Measurement<U> {
Measurement::new(a.value + b.value)
}
// 2. 수명 마커로 소유권 추적
struct BorrowedHandle<'a> {
raw: *mut c_void,
_lifetime: PhantomData<&'a ()>, // raw 포인터에 수명 부여
}
// PhantomData<&'a ()>로 인해 컴파일러가 수명을 추적
// raw 포인터만으로는 수명 정보가 없어 dangling 위험
Sealed Trait 패턴
// === Sealed Trait — 외부 크레이트의 구현을 금지하는 패턴 ===
// 공개 트레이트에 메서드를 추가해도 하위 호환성 유지 가능
mod private {
pub trait Sealed {} // 비공개 모듈 — 외부 접근 불가
}
pub trait MyApi: private::Sealed {
fn operation(&self);
// 나중에 메서드 추가해도 하위 호환성 유지
}
pub struct TypeA;
pub struct TypeB;
impl private::Sealed for TypeA {}
impl private::Sealed for TypeB {}
impl MyApi for TypeA {
fn operation(&self) { /* ... */ }
}
impl MyApi for TypeB {
fn operation(&self) { /* ... */ }
}
// 외부 크레이트: impl MyApi for ExternalType {} → 컴파일 에러!
// private::Sealed를 구현할 수 없으므로
Extension Trait과 Never 타입
// === Extension Trait — 기존 타입에 메서드 추가 ===
trait IteratorExt: Iterator + Sized {
fn try_collect_vec(self) -> Result<Vec<Self::Item>, Error>
where
Self::Item: TryInto<ValidItem>,
{
self.map(|item| item.try_into()).collect()
}
fn debug_each(self, label: &str) -> Self
where
Self::Item: std::fmt::Debug,
{
self.inspect(move |item| {
eprintln!("[{}] {:?}", label, item);
})
}
}
// 모든 Iterator에 자동 구현 (blanket impl)
impl<I: Iterator> IteratorExt for I {}
// === Never 타입 (!) — 절대 반환하지 않는 타입 ===
fn exit_process() -> ! {
std::process::exit(1); // 절대 반환하지 않음
}
// Never 타입은 모든 타입으로 변환 가능 (bottom type)
let val: u32 = match some_result {
Ok(n) => n,
Err(_) => exit_process(), // !는 u32로 변환됨
};
// Infallible — 실패할 수 없는 변환을 표현
impl From<Infallible> for MyError {
fn from(never: Infallible) -> Self {
match never {} // 빈 match — Infallible은 값이 존재할 수 없음
}
}
패턴 선택 가이드: Sealed trait는 라이브러리 API의 하위 호환성이 중요할 때, Extension trait는 기존 타입을 확장할 때, HRTB는 콜백 함수가 다양한 수명의 참조를 처리해야 할 때, GAT는 연관 타입이 수명이나 제네릭을 필요로 할 때 사용합니다. 커널 Rust에서는 sealed trait(드라이버 API 안정성)과 PhantomData(수명/타입 상태 추적)가 특히 많이 활용됩니다.
Pin과 Unpin 심층
Pin<P>은 "이 포인터가 가리키는 값은 메모리에서 이동할 수 없다"는 보증을 타입 시스템으로 제공합니다. 자기 참조 구조체(self-referential struct)와 async/await의 핵심 기반입니다.
// Pin 기본 사용
use std::pin::Pin;
// Unpin 타입은 Pin이 투명 — 자유롭게 이동 가능
let mut val = 42;
let pinned = Pin::new(&mut val); // i32: Unpin → OK
// !Unpin 타입 — 이동 방지 강제
use std::marker::PhantomPinned;
struct SelfRef {
data: String,
ptr: *const String, // data를 가리키는 원시 포인터
_pin: PhantomPinned, // !Unpin 마커
}
// 안전한 Pin 생성
let pinned = Box::pin(SelfRef {
data: String::from("hello"),
ptr: std::ptr::null(),
_pin: PhantomPinned,
});
// std::mem::swap(&mut *pinned, ...); ← 컴파일 에러! 이동 불가
// async fn이 생성하는 Future는 !Unpin
// → async 런타임이 Pin<Box<dyn Future>>로 관리
Pin API 메서드 정리
| 메서드 | 조건 | 동작 | 용도 |
|---|---|---|---|
Pin::new(ptr) | T: Unpin | 안전하게 Pin 생성 | 일반 타입의 Pin 래핑 |
Pin::into_inner(pin) | T: Unpin | Pin에서 포인터 추출 | Pin 해제 |
Box::pin(val) | 항상 가능 | 힙 할당 + Pin 고정 | !Unpin 타입의 Pin 생성 |
unsafe Pin::new_unchecked(ptr) | unsafe | 조건 없이 Pin 생성 | 직접 Pin 보증 시 |
pin.as_ref() | 항상 | Pin<&T> 반환 | 불변 접근 |
pin.as_mut() | 항상 | Pin<&mut T> 반환 | 가변 접근 (이동 없이) |
pin.get_mut() | T: Unpin | &mut T 반환 | Unpin 타입의 가변 참조 획득 |
unsafe pin.get_unchecked_mut() | unsafe | &mut T 반환 | !Unpin에서 가변 참조 (주의!) |
// === pin! 매크로 (std::pin::pin!) — 스택 고정 ===
use std::pin::pin;
// 힙 할당 없이 스택에서 Pin 생성
let mut future = pin!(async {
let data = fetch_data().await;
process(data).await
});
// poll 호출 가능
let waker = futures::task::noop_waker();
let mut cx = Context::from_waker(&waker);
let result = future.as_mut().poll(&mut cx);
// === Pin 프로젝션 — 구조체 필드 접근 ===
// Pin<&mut Struct>에서 개별 필드를 Pin<&mut Field>로 접근
impl MyStruct {
// #[pin] 필드에 대한 안전한 프로젝션
fn pinned_field(self: Pin<&mut Self>) -> Pin<&mut InnerType> {
// SAFETY: pinned_field는 구조적으로 고정(structurally pinned)됨
unsafe { self.map_unchecked_mut(|s| &mut s.pinned_field) }
}
}
no_std 프로그래밍 (임베디드/커널 진입점)
no_std는 Rust 표준 라이브러리(std)를 사용하지 않는 환경입니다. 임베디드 시스템, OS 커널, 부트로더 등 운영체제가 없거나 제한된 환경에서 필수입니다. 리눅스 커널 Rust는 no_std 환경입니다.
// no_std 크레이트 선언
#![no_std] // std 사용 금지 선언
// 사용 가능: core 라이브러리 전체
use core::fmt;
use core::option::Option;
use core::result::Result;
// alloc 크레이트 (전역 할당자 필요)
extern crate alloc;
use alloc::vec::Vec;
use alloc::string::String;
use alloc::boxed::Box;
// 커널의 경우: 커널 전용 alloc 크레이트 사용
// - Vec::try_push() (할당 실패 시 Err 반환)
// - Box::try_new() (할당 실패 시 Err 반환)
// - panic! 사용 금지 — 커널 oops 유발
// 커널 no_std의 핵심 차이점
// 1. println! → pr_info! (커널 로그)
// 2. Vec::push() → Vec::try_push()? (할당 실패 처리)
// 3. std::sync::Mutex → kernel::sync::Mutex
// 4. std::thread → 커널 workqueue/kthread
// 5. panic → Result 에러 전파 필수
// 6. 전역 할당자: 커널의 kmalloc/kfree
std → no_std(커널) API 대응표
| std (사용자 공간) | 커널 Rust 대체 | 차이점 |
|---|---|---|
println!("...") | pr_info!("...\n") | 커널 로그 시스템(printk)으로 출력 |
Vec::push(v) | Vec::try_push(v)? | 할당 실패 시 Err 반환 (panic 없음) |
Box::new(v) | Box::try_new(v)? | 할당 실패 처리 필수 |
String::from("...") | CString::try_from_fmt(...)? | C 호환 문자열 (NUL 종단) |
std::sync::Mutex | kernel::sync::Mutex | 커널 mutex, lockdep 통합, Pin 필요 |
std::thread::spawn | Workqueue / kthread | 커널 스레드 모델 사용 |
std::time::Duration | kernel::time::Jiffies | 커널 타이밍 단위 (jiffies) |
std::fs::File | VFS 추상화 | 직접 파일 접근 불가 |
panic!("...") | return Err(code::EINVAL) | panic은 커널 oops → 반드시 Result 사용 |
no_std 환경에서 필요한 lang items
// === 독립 no_std 바이너리 (임베디드/OS 개발) ===
#![no_std]
#![no_main] // main() 대신 커스텀 진입점 사용
// panic 핸들러 — 필수 (std 없이는 기본 핸들러가 없음)
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
loop {} // 무한 루프 (halt)
}
// 전역 할당자 — alloc 크레이트 사용 시 필수
#[global_allocator]
static ALLOC: MyAllocator = MyAllocator;
// 커널에서는 위 항목들이 이미 정의되어 있음:
// - panic_handler → kernel_panic() 호출
// - global_allocator → kmalloc/kfree 기반
// - 드라이버 개발자는 이들을 직접 정의할 필요 없음
| no_std 필수 항목 | 일반 임베디드 | 리눅스 커널 |
|---|---|---|
#![no_std] | 직접 선언 | 커널 빌드 시스템이 자동 적용 |
#[panic_handler] | 직접 구현 | 커널이 제공 (BUG/oops) |
#[global_allocator] | 직접 구현 | 커널이 제공 (kmalloc) |
| OOM 핸들러 | #[alloc_error_handler] | try_* API로 명시적 처리 |
| 진입점 | #[no_main] + 커스텀 | module! 매크로 |
커널에서 절대 사용 불가: std::*, panic!()/unwrap()/expect() (최소화), async-std/tokio, println!, format!() (커널 대체: fmt!()), 힙 할당 실패를 무시하는 모든 API. 커널 환경에서는 "할당이 항상 성공한다"는 가정을 할 수 없습니다.
커널 Rust 프로그래밍
Linux 커널에서의 Rust 프로그래밍(바인딩, Kbuild 통합, 드라이버 작성, 안전성 모델, 테스팅, 디버깅)은 별도 전문 문서로 분리되었습니다.
관련 문서
Rust 언어와 커널 적용에 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.
사이트 내부 문서
외부 참고 자료
| 자료 | URL | 설명 |
|---|---|---|
| Rust for Linux 공식 | rust-for-linux.com | 프로젝트 홈페이지, 최신 소식 |
| 커널 Rust 문서 | docs.kernel.org/rust | 공식 커널 Rust 문서 |
| Rust Book | doc.rust-lang.org/book | Rust 공식 학습서 (The Book) |
| Rust by Example | rust-by-example | 예제 중심 학습 |
| Rustonomicon | nomicon | unsafe Rust 심층 가이드 |
| GitHub 저장소 | Rust-for-Linux/linux | 소스 코드 및 이슈 트래커 |
| LKML Rust | lore.kernel.org | 메일링 리스트 아카이브 |