// 공유 참조 (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의 수명 시스템은 "이 참조가 가리키는 데이터가 참조보다 먼저 사라지는가?"를 컴파일 타임에 검사하여, 댕글링 참조를 원천적으로 불가능 하게 만듭니다.
수명(Lifetime) 범위 시각화
수명(Lifetime) 범위 시각화
1
fn main() {
2
let r;
3
{
4
let x = 5;
6
7
println!("{}", r); ← r 사용 (댕글링)
8
r = &x; ← r가 x를 빌림
수명 타임라인 — 막대의 길이가 곧 수명 기간 (시간: 왼쪽 → 오른쪽)
'a = r의 수명
L2: let r
L8: }
'b = x의 수명
L4: let x
L6: }
⚠ x 소멸 — r은 아직 살아있음
시간 →
E
컴파일러 에러: `x` does not live long enough
'b(x의 수명)이 'a(r의 수명)보다 먼저 끝나므로, 'a가 'b를 영원히 참조할 수 없음 (댕글링)
→ C에서는 런타임 UAF 버그, 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의 참조가 원본보다 오래 살지 않음을 보장하고, 콜백(Callback) 함수에 전달하는 참조의 수명은 콜백의 실행 기간과 맞아야 합니다. 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 참조 버그가 원천 차단됩니다.
Enum 메모리 레이아웃 (Tagged Union)
Enum 메모리 레이아웃: Tagged Union
enum Shape {
Circle(f64),
Rect { w: f64, h: f64 },
Point,
}
메모리 레이아웃 (총 24바이트, 모든 variant 동일 크기)
tag (판별자) · 0~7바이트
데이터 (union) · 8~23바이트 (최대 16바이트)
0
8
24
tag = 0
radius: 3.14
(미사용)
tag = 1
w: 5.0
h: 3.0
tag = 2
Point는 데이터 미사용 (전부 패딩)
24바이트 — 같은 공간이 variant에 따라 다르게 해석됨 (가장 큰 variant 기준)
니치(Niche) 최적화
Option<&T>: NULL(0x0)로 None 표현 → 판별자 불필요
Some(&T)
유효 포인터
None
0x0 (NULL)
→ size_of::<Option<&T>>() == 8
(판별자 오버헤드 0 — 포인터 8바이트 그대로)
C tagged union: 판별자(Discriminant)를 직접 관리 → 값-태그 불일치 버그 가능
Rust enum: 컴파일러가 판별자 자동 관리 + 패턴 매칭으로 타입 안전하게 접근
// 일반 구조체
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)]HashHashMap/HashSet 키로 사용 모든 필드가 Hash
#[derive(Default)]DefaultT::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는 튜플, 구조체, 열거형 내부 데이터를 분해하여 추출할 수 있습니다.
패턴 매칭 평가 흐름
match 표현식 평가 흐름
match 표현식
(스크러티니 1회 평가)
Arm 1: 패턴₁
(가드 if guard₁)
일치
expr₁ 실행
→ 결과 값 반환
불일치
Arm 2: 패턴₂
(가드 if guard₂)
일치
expr₂ 실행
→ 결과 값 반환
불일치
⋮ 필요한 만큼
arm 추가 가능
_ 와일드카드
(모든 경우 항상 일치)
일치
기본 expr 실행
→ 결과 값 반환
최종 결과
값 반환
* 컴파일러가 모든 경우를 검사(exhaustiveness)하며,
위에서부터 순서대로 첫 번째 일치 arm만 실행됩니다.
패턴 종류 문법 설명 예시
리터럴 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는 반환 타입에 에러 가능성이 명시 되고, 런타임 비용이 없으며, 컴파일러가 처리를 강제합니다.
? 연산자의 에러 전파 흐름
? 연산자의 에러 전파 — Err 이 콜 스택을 거슬러 올라가며 타입이 변환된다
① 호출 체인 (아래로 호출)
② 에러 전파 (위로 반환)
main()
process()?
process()
parse_data()?
parse_data()
read_file()?
read_file()
File::open(path)?
호출
호출
호출
main() 최종 수신
Err(AppError)
? : From 변환
ParseIntError → AppError
? : From 변환
io::Error → ParseIntError
read_file() 발생
Err(io::Error)
? 전파
? 전파
? 전파
호출 방향 (Ok 값은 아래로 전달)
Err 전파 — ? 가 오류를 콜 스택 위로 반환
각 단계의 ? 가 From::from 으로 에러 타입을 변환한 뒤 즉시 반환한다
? = match { Ok(v) => v, Err(e) => return Err(From::from(e)) }
// 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) 지연(Latency) 기본값 (클로저)
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 는 즉시 프로세스(Process)를 종료하며 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를, 애플리케이션(Application)에는 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 ());
에러 처리 결정 트리
에러 처리 결정 트리 (Error Handling Decision Tree)
에러가 발생할 수 있는가?
아니오
에러 없음 · 그대로 진행
예
복구 가능한 에러인가?
아니오 (복구 불가 · 버그)
panic! (사용 금지 지양)
예
라이브러리·앱·커널 중 어디인가?
라이브러리
구조화된 에러 enum
• thiserror 매크로
• Display + Error 구현
• From 변환으로 ? 지원
애플리케이션
anyhow / Box<dyn Error>
• .context()로 설명 추가
• bail!로 빠른 에러 반환
• 사용자 친화적 메시지
커널
kernel::error::Error
• unwrap / expect 금지
• ? 연산자로 에러 전파
• C 에러 코드(-EINVAL) 매핑
공통 원칙: 에러를 가능한 한 빨리 위로 전파하고,
최종 처리 지점(main 등)에서 사용자에게 보여줄 메시지로 변환
💡
라이브러리 vs 애플리케이션 에러 처리: 라이브러리는 에러를 구조화 하여 호출자가 match로 분기할 수 있게 해야 합니다(thiserror). 애플리케이션은 에러를 사용자에게 설명 하는 데 집중해야 합니다(anyhow). 두 접근법을 혼합하면 안 됩니다: 라이브러리에서 anyhow::Error를 반환하면 호출자가 에러 종류를 판별할 수 없고, 애플리케이션에서 지나치게 세분화된 에러 타입을 만들면 불필요한 복잡성만 늘어납니다.
트레이트(Trait)와 제네릭(Generics)
트레이트는 공유 동작을 정의하는 Rust의 인터페이스 시스템입니다. 제네릭과 결합하여 타입 안전하면서도 유연한 코드를 작성할 수 있으며, 단형화(monomorphization) 를 통해 런타임 오버헤드가 없습니다.
Rust 트레이트 계층 구조
Rust 트레이트 계층 구조
실선 화살표 = 수퍼트레이트(Supertrait) 요구 (부모 → 자식) · 점선 = 블랭킷 구현 · 색 = 계열 구분
① 표준 수퍼트레이트 계층 — 진짜 상속 관계 (아래 트레이트를 구현하려면 위 수퍼트레이트가 반드시 필요)
비교 계열 (Ord)
복사 계열 (Clone)
PartialEq
동등 비교 (==, !=)
Clone
깊은 복사
Eq
완전 동등
PartialOrd
부분 순서 비교 (<, <=)
Copy
비트 복사
Ord
전체 순서 비교
Eq는 PartialEq 요구
PartialOrd는 PartialEq 요구
Copy는 Clone 요구
Ord = PartialOrd + Eq
(복수 수퍼트레이트 — 화살표 2개)
Eq + PartialOrd
② 마커 / 자동 트레이트 — 계층이 아닌 '표시' (화살표 없음, 컴파일러 자동 처리)
Sized
암묵 마커
Send
스레드 이동
Sync
스레드 공유
Unpin
고정 해제
Sized는 크기 확정, Send / Sync / Unpin은 컴파일러가 자동 파생하는 auto trait — 상속 관계가 아니라 속성만 부여되며, unsafe 구현으로 직접 추가 가능
③ 기능 트레이트 — 동작 · 변환 · 반복 · 연산 (서로 독립, 블랭킷 구현으로 확장)
Display / Debug
출력 ({} / {:?})
From / Into / TryFrom
타입 변환
Iterator / IntoIterator
반복
Deref / DerefMut
역참조 연산
ToString
블랭킷 구현 (Blanket impl)
Display를 구현한 모든 타입은 ToString도 자동 보유
impl<T: Display> ToString for T — 점선은 상속이 아닌 자동 확장
// 트레이트 정의
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 ,
}
정적 디스패치(Dispatch) 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 + CA를 구현하려면 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의 표준 컬렉션은 소유권 시스템과 통합되어 메모리 안전합니다. 모든 힙 할당 컬렉션은 스코프 종료 시 자동 해제됩니다.
Rust 주요 컬렉션 선택 가이드
Rust 주요 컬렉션 선택 가이드
4개 주요 컬렉션의 구조 · 복잡도 · 용도, 그리고 조건별 선택 흐름 · 커널 Rust 주의사항
① 주요 컬렉션 — 구조와 시간복잡도
Vec<T>
연속 메모리 · 순차 배열
인덱스 O(1) · push/pop O(1)*
탐색 O(n)
범용 · 가장 많이 사용
HashMap<K,V>
해시 기반 키-값 맵
조회/삽입/삭제 O(1)*
순서 보장 없음
키로 빠른 조회용
BTreeMap<K,V>
B-트리 정렬 키-값 맵
조회/삽입/삭제 O(log n)
키 정렬 · 범위 탐색
정렬 순회가 필요할 때
HashSet / BTreeSet
중복 없는 집합
contains O(1)* / O(log n)
합 · 교 · 차집합
중복 제거 · 멤버십 검사
② 선택 기준 — 조건이 맞으면 오른쪽 컬렉션을 권장
1. 키 정렬 · 범위 탐색이 필요한가?
BTreeMap / BTreeSet
2. 키로 빠른 단일 조회가 필요한가?
HashMap
3. 중복 제거 · 멤버십 검사인가?
HashSet
4. 그 밖의 일반적인 순서 배열?
Vec<T> (기본)
③ 커널 Rust 주의 — 할당 실패 가능 → try_ API 필수
⚠ 커널 Rust: 할당은 언제나 실패 가능(OOM) → try_* API 필수
std의 push() / new() / reserve()는 실패 시 panic → 커널에서 사용 불가 (커널에선 panic 금지)
Vec::try_push()
Vec::try_with_capacity()
Box::try_new()
→ Result<_, AllocError>
각 try_* API 는 Result<_, AllocError> 를 반환하므로 '?' 연산자로 에러를 안전하게 전파합니다.
HashMap은 커널 Rust에서 미제공 → 기존 C 해시 테이블(rhashtable)을 래핑해 사용 · 커널 컬렉션은 kernel::alloc의 Vec 사용
// 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 힙 순 우선순위(Priority) 큐
LinkedList<T>O(1) O(n) 삽입 순 양단 삽입/삭제 (거의 사용 안함)
std vs kernel 컬렉션 API 비교
작업 std (사용자 공간(User Space)) kernel (커널 공간(Kernel Space)) 차이 이유
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 래핑) 커널 해시 테이블(Hash Table) 재사용
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()로 미리 예약하면 불필요한 재할당을 방지할 수 있습니다.
Vec 내부 메모리 레이아웃
Vec<i32> 내부 메모리 레이아웃
스택의 Vec 구조체(ptr · len · cap)가 힙의 연속 블록을 가리킴 · 용량이 찼을 때의 2배 성장 전략
① Vec<i32> 의 실제 메모리 — 스택 구조체가 힙 블록을 가리킴
스택 (Vec<i32> 구조체)
ptr
→ 0x1000
len
3
capacity
4
총 24바이트 (ptr 8 + len 8 + cap 8)
가리킴
힙 (연속 메모리 블록 — 16바이트)
0x1000
0x1004
0x1008
0x100c
10
[0]
20
[1]
30
[2]
미사용
[3]
capacity = 4 (전체 할당)
② 용량 성장 전략 (Doubling Strategy) — 가득 차면 2배로 재할당
Vec::new()
len=0 · cap=0
힙 할당 없음
첫 push(10)
len=1 · cap=4
첫 할당: 4슬롯
push(20,30,40)
len=4 · cap=4
가득 참
push(50)
len=5 · cap=8
재할당 2배 + 복사
용량(cap)이 가득 차면 재할당 시 2배로 성장(cap 4 → 8 → 16…)하고, 기존 요소를 새 블록으로 복사합니다.
대안: Vec::with_capacity(100)
→ len=0 · cap=100 — 처음부터 100개 슬롯을 할당하므로 push 100회까지 재할당이 발생하지 않습니다.
예상 용량을 알고 있다면 미리 예약해 불필요한 재할당과 복사를 피할 수 있습니다 (성능 최적화).
메서드 동작 시간 복잡도 용도
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) 와 제로 코스트 추상화 덕분에 가독성을 유지하면서 최적의 성능을 달성합니다.
반복자 어댑터 체인과 지연 평가
반복자 어댑터 체인 (지연 평가)
어댑터는 지연(요소를 소비하지 않음), 소비자만 즉시 순회 · 소비자가 '당길 때' 요소가 통과
① 어댑터 체인 — 소스 → 어댑터들 → 소비자
어댑터 체인 (지연 lazy) — 중간에 아무 일도 안 일어남
소비자 (즉시 eager)
소스
[1,2,3,4,5]
.iter()
Iterator 생성
.filter(|&n| n % 2 == 0)
짝수만 (조건 필터)
.map(|n| n * n)
제곱 변환
.sum()
소비자
20
4 + 16 = 20
② 지연 평가 원리 — 소비자가 '당길 때'만 요소가 체인을 통과한다
소스 요소 (next())
filter(n % 2 == 0)
map(n * n)
sum 누적
[1]
탈락 · 홀수
—
0
[2]
통과 ✓
4
4
[3]
탈락 · 홀수
—
4
[4]
통과 ✓
16
20
[5]
탈락 · 홀수
—
20
None (체인 종료) → 최종 결과 20
filter/map은 새 배열을 만들지 않고, sum이 요소를 하나씩 당겨와 즉시 누적합니다 (중간 Vec 없음)
③ 정리 — 어댑터는 지연, 소비자는 즉시
어댑터 (Adapter) — 지연 평가
새 Iterator를 반환할 뿐, 요소를 소비하지 않음
filter · map · take · skip · zip · chain · enumerate · flat_map …
소비자 (Consumer) — 즉시 평가
Iterator를 실제로 순회하며 결과 생성 (체인의 최종 단계)
collect · sum · count · any · all · find · fold · for_each …
// 클로저(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 수준의 성능을 유지하면서도 고수준의 가독성을 확보할 수 있습니다.
트레이트 시그니처 캡처 방식 호출 횟수 자동 구현 조건
FnOnceself소유권 이동 (값) 최대 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개짜리만 (나머지 별도) 고정 크기 패킷(Packet) 파싱
peekable()Iterator Peekable<I> 파서, 조건부 소비
by_ref()Iterator &mut Iterator (빌림) 일부만 소비 후 원래 반복자 계속 사용
scan(state, f)Iterator 상태를 유지하며 변환 누적 합, 상태 기반 필터링
flat_map(f)Iterator 중첩 Iterator 평탄화 1:N 매핑(Mapping) (각 요소 → 여러 요소)
step_by(n)Iterator/Range n칸씩 건너뛰기 간격 있는 순회
⚠️
제로 코스트 추상화의 조건: 반복자 체인이 제로 코스트가 되려면 릴리스 빌드(--release) 에서 컴파일해야 합니다. 디버그 빌드에서는 최적화가 적용되지 않아 루프보다 느릴 수 있습니다. 또한 dyn Iterator(동적 디스패치)를 사용하면 인라인 최적화가 불가능하므로, 성능이 중요한 경우 제네릭(impl Iterator)을 사용하세요.
스마트 포인터 (Box, Rc, Arc, RefCell, Cow)
스마트 포인터는 포인터처럼 동작하면서 추가 메타데이터와 기능을 제공합니다. Rust의 소유권 시스템과 통합되어 메모리 안전성을 보장합니다.
스마트 포인터 비교 매트릭스
스마트 포인터 비교 매트릭스
행 = 비교 기준 · 열 = 스마트 포인터 — 아래로 읽으면 기준별, 가로로 읽으면 포인터별 특징
① 속성별 비교 — 열마다 색상으로 포인터 구분
기준
Box<T>
Rc<T>
Arc<T>
RefCell<T>
Cow<'a, T>
소유권
단일 소유
공유 (비원자 카운트)
공유 (원자 카운트)
단일 소유 + 내부 가변
빌림 또는 소유
오버헤드
없음 (포인터 1개)
카운터 (비원자)
원자 카운터 (동기화)
런타임 빌림 검사
변경 시에만 복사
스레드 안전성
Send + Sync
!Send · !Sync
Send + Sync
!Sync (단일 스레드)
T에 따름
대표 용도
재귀 · DST · dyn Trait
단일 스레드 공유
멀티스레드 공유
불변 컨텍스트에서 변경
선택적 복사 (파서 등)
② 일반적인 조합 패턴 — 여러 스마트 포인터를 함께 사용
Rc<RefCell<T>>
단일 스레드에서 여러 소유자 + 내부 가변성
Arc<Mutex<T>>
멀티스레드에서 여러 소유자 + 동기화된 가변성
Arc<RwLock<T>>
멀티스레드 + 읽기 다수 / 쓰기 소수 최적화
Box<dyn Trait>
트레이트 객체 — 동적 디스패치 (런타임 다형성)
Cow<'a, str>
불변이면 &str 빌림, 변경 필요 시 String으로 복사
// 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> — 원자적(Atomic) 참조 카운팅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 → &UT: Deref<Target=U>불변 역참조
&mut T → &mut UT: DerefMut<Target=U>가변 역참조
&mut T → &UT: 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>는 참조 카운트를 증가시키지 않으므로 이 문제를 해결합니다.
순환 참조 문제와 Weak 해결
순환 참조 문제와 Weak 해결
양쪽 모두 Node A와 B가 서로 참조 — 차이는 오른쪽에서 B→A가 Weak(strong 증가 없음)라는 점
① 순환 참조 — 메모리 누수
② Weak로 해결 — 정상 해제
Node A
strong 2 · weak 0
Node B
strong 2 · weak 0
Rc
Rc
A↔B가 서로를 강하게 잡음 → strong 2 유지
스코프 종료에도 해제되지 않음 (누수)
count가 0이 되는 순간이 영원히 오지 않음
Node A
strong 1 · weak 1
Node B
strong 1 · weak 0
Rc
Weak (strong +0)
B→A가 Weak라 A의 strong을 올리지 않음
스코프 종료 → strong 0 → 정상 해제
Weak는 참조하는 대상의 수명을 연장하지 않음
③ Weak<T> 사용 흐름 — 생성부터 조회까지
Rc::new(val)
strong 1 · weak 0
Rc::downgrade(&rc)
Weak<T> 생성 (strong +0)
weak.upgrade()
Option<Rc<T>> 반환
Some(Rc) / None
원본 생존 여부
Weak<T> 동작 원리
· Weak는 strong_count를 증가시키지 않으므로 순환 참조에서 한쪽을 약하게 연결해 누수를 막습니다.
· strong_count가 0이 되면 Weak가 있어도 값은 즉시 해제됩니다.
· 해제 후 weak.upgrade()는 None을 반환 → '?' 로 안전하게 실패를 처리할 수 있습니다.
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로 참조하는 패턴은 커널 디바이스 트리(Device Tree)에서도 동일하게 적용됩니다.
동시성 프로그래밍 (Thread, Channel, Mutex, RwLock)
Rust의 "Fearless Concurrency" 는 소유권 시스템이 데이터 레이스를 컴파일 타임에 방지하므로, 동시성 프로그래밍에서 흔히 발생하는 버그를 원천 차단합니다.
Send/Sync 동시성 모델
Send/Sync 트레이트와 동시성 모델
Send: 소유권 이전
✓ i32, String
✓ Vec, Box
✗ !Send: Rc
Sync: &T 공유
✓ i32, RwLock
✓ &T 공유
✗ !Sync: RefCell
② 동시성 프리미티브 — 데이터 보호 · 메시지 전달 · 잠금-프리
Mutex<T>
상호 배제 잠금
RwLock<T>
읽기·쓰기 잠금
Channel
mpsc 메시지 전달
Atomic*
AtomicBool · Usize
데이터 보호: Arc<Mutex<T>> · 메시지 전달: mpsc::channel() · 잠금-프리: Atomic*
"공유 메모리로 통신하지 말고, 통신으로 메모리를 공유하라" — Go 격언 (Rust에서도 유효)
// 스레드 생성과 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상호 배제(Mutual Exclusion), RAII 자동 unlock
RwLock<T>읽기 다수 / 쓰기 소수 kernel::sync::RwLock (제한적)동시 읽기 허용, 쓰기 배타적
mpsc::channel()스레드 간 메시지 전달 커널 workqueue 다중 생산자, 단일 소비자
AtomicUsize잠금(Lock) 없는 카운터/플래그 core::sync::atomicLock-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 기반, 컨텍스트 스위치 가능 짧은 임계 영역(Critical Section), I/O 대기 가능
RwLock<T>허용 불가 읽기 다수 시 Mutex보다 유리 읽기 95% 이상, 쓰기 드문 경우
SpinLock (커널)불가 불가 바쁜 대기, 컨텍스트 스위치 없음 인터럽트(Interrupt) 핸들러(Handler), 극히 짧은 구간
Atomic*허용 허용 Lock-free, 하드웨어 원자 명령어 카운터, 플래그, 단순 상태
RCU (커널)허용 불가 (교체) 읽기 오버헤드 거의 0 읽기 매우 빈번, 쓰기 극히 드문 경우
mpsc::channel소유권 이전 큐 기반, 잠금 없는 전달 스레드 간 메시지 패싱
💡
잠금 선택 가이드: ① 단순 카운터/플래그 → Atomic* ② 읽기 위주 공유 데이터 → RwLock ③ 읽기/쓰기 비슷하거나 I/O 포함 → Mutex ④ 스레드 간 데이터 전달 → mpsc::channel ⑤ 커널 인터럽트 컨텍스트 → SpinLock (슬립(Sleep) 금지)
모듈 시스템과 크레이트
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 async closure(AsyncFn), RPIT 수명 자동 캡처(RFC 3498), unsafe extern 블록·unsafe 속성, unsafe_op_in_unsafe_fn 기본 경고, gen 키워드 예약 향후 전환 예정
ℹ️
커널에서는 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)지정 경로 범위 세밀한 접근 제어(Access Control)
(기본, 없음) 현재 모듈 + 하위 모듈 구현 세부사항 은닉
// 권장 프로젝트 구조
// 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 워크스페이스
워크스페이스는 여러 관련 크레이트를 하나의 프로젝트로 관리합니다. 공유 의존성, 빌드 캐시(Cache), 일관된 버전 관리가 가능합니다.
# 워크스페이스 루트 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 용도
devcargo build0 true 개발/디버깅 (빠른 빌드)
releasecargo build --release3 false 배포용 (최적화)
testcargo test0 true 테스트 실행
benchcargo bench3 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!)는 패턴 매칭으로, 절차적 매크로 는 토큰 스트림을 변환하여 코드를 생성합니다.
매크로 확장 과정
매크로 확장 과정
매크로는 컴파일 타임에 토큰을 치환하여 일반 Rust 코드로 바꿉니다
① 확장 파이프라인 — vec![1, 2, 3] 예시
① 소스 코드
매크로 호출
vec![1, 2, 3]
② 토큰화
문자열 → 토큰 스트림
vec ! [ 1 , 2 , 3 ]
③ 매크로 확장
패턴 매칭 · 치환
Push, New, ...
④ 확장된 코드
일반 Rust 코드
{ let mut v; ... }
확장 후 코드는 일반 Rust 코드로 컴파일 — 매크로 규칙은 사용처(소비자)에서 한 번만 적용됩니다
② 두 가지 매크로 방식 — '③ 매크로 확장'을 구현하는 방법
선언적 매크로 (macro_rules!)
패턴 매칭 기반 코드 생성
· 방식: 토큰 패턴 매칭 → 치환
· 대표: vec!, println!, matches!
· 위생성: 위생적(hygienic) 변수
· 커널: pr_info!, new_mutex!
절차적 매크로 (proc_macro)
TokenStream → TokenStream 함수
· 방식: 함수형 TokenStream 변환
· 형태: derive · 속성 · 함수형 매크로
· 위생성: 규칙을 직접 작성
· 커널: module!, #[vtable]
선언적 = 간단한 패턴 치환 · 절차적 = 더 강력하지만 별도 proc-macro 크레이트 필요
cargo expand 로 확장 결과를 직접 확인할 수 있습니다
// 선언적 매크로 (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비트 시스템)를 가집니다.
트레이트 객체 메모리 레이아웃
트레이트 객체 메모리 레이아웃
dyn Trait 는 16바이트 Fat Pointer — 실제 데이터와 vtable 두 곳을 가리킵니다
&dyn Shape (Fat Pointer)
가리키는 실제 데이터
data_ptr (8 bytes)
vtable_ptr (8 bytes)
Circle (Heap)
radius: f64 = 5.0
data_ptr
vtable_ptr
vtable — Shape for Circle
타입마다 컴파일 타임에 하나씩 생성 · 정적 메모리에 저장
▸ 컴파일러 메타데이터
drop_in_place: fn(*mut Circle)
size: usize = 8
align: usize = 8
▸ 사용자 메서드 — 동적 디스패치
area: fn(&Circle) -> f64 [Circle::area]
name: fn(&Circle) -> &str [Circle::name]
Fat Pointer 는 16바이트 (data_ptr + vtable_ptr 각 8바이트) — 런타임 다형성의 원리
area/name 은 구체 타입의 메서드 구현을 가리켜 호출 시 vtable 을 통해 간접 실행됩니다
객체 안전성 규칙 (Object Safety)
모든 트레이트가 트레이트 객체(dyn Trait)로 사용될 수 있는 것은 아닙니다. 객체 안전성을 만족해야 합니다.
규칙 허용 금지 (객체 안전하지 않음)
반환 타입에 Self fn method(&self) -> i32fn 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, 패딩(Padding))
시스템 프로그래밍과 커널 개발에서 데이터의 메모리 레이아웃을 정확히 제어하는 것은 필수입니다. Rust는 #[repr] 속성으로 레이아웃을 명시적으로 지정할 수 있습니다.
구조체 메모리 레이아웃
구조체 메모리 레이아웃 비교
같은 struct { a: u8, b: u32, c: u8 } — repr 방식에 따라 크기와 배치가 달라집니다
① 바이트맵 비교 — 각 칸 = 1바이트 (위 숫자는 바이트 offset)
repr(Rust) — 컴파일러 최적화 · 총 8바이트
0
1
2
3
4
5
6
7
b: u32 (4B)
a: u8
c: u8
pad
컴파일러가 필드를 재배치(a·c는 1B이므로 나란히)하여 패딩을 최소화합니다
repr(C) — 선언 순서 유지 · 총 12바이트
0
1
2
3
4
5
6
7
8
9
10
11
a: u8
pad (3B)
b: u32 (4B)
c: u8
pad (3B)
선언 순서 유지 → a 뒤 3B, c 뒤 3B 패딩 — C 구조체와 동일 레이아웃 (FFI에 필수)
③ repr 속성 한눈에 보기
repr(Rust)
기본 — 컴파일러가 필드 재배치로 패딩을 최소화 (위 예시 8B)
repr(C)
선언 순서 유지 · C 구조체와 동일 레이아웃 (12B) — FFI 필수
repr(packed)
패딩 없이 밀착 배치 (6B) — 비정렬 접근은 느리거나 위험할 수 있음
repr(transparent)
뉴타입 패턴 — 내부 타입과 동일한 레이아웃·정렬 보장
repr(align(N))
최소 정렬을 N 바이트로 강제 — 캐시라인 정렬 등에 활용
repr(Rust)는 미지정 시 기본 · 정확한 FFI 바이트 크기가 필요하면 repr(C) 또는 repr(transparent) 사용
// 메모리 크기와 정렬 확인
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) (기본)컴파일러 재배치(Relocation) 최적화 최소 불가 일반 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 안전 경계
Safe Rust ↔ Unsafe Rust 경계 모델
unsafe 는 '위험한 코드'가 아니라 — 안전한 추상화로 감싸 경계를 관리하는 도구입니다
① 두 영역 — 컴파일러 보증 vs unsafe 의 5가지
Safe Rust 영역
컴파일러가 보증하는 것
✓ 메모리 안전 (UAF·더블 프리 없음)
✓ 데이터 레이스 없음
✓ 댕글링 · 널 포인터 역참조 없음
✓ 타입 안전성
대부분의 드라이버·비즈니스 로직이 여기
Unsafe Rust 영역
unsafe 로만 가능한 5가지
1. 원시 포인터 (*const T, *mut T) 역참조
2. unsafe 함수 · 메서드 호출
3. static mut · unsafe 트레이트
4. union 필드 접근
커널: C FFI · MMIO · inline asm · HW 레지스터
SAFETY:
주석 필수
② 경계 관리 — 안전 API가 unsafe 블록을 감싸는 래퍼 패턴
① 안전 코드
pub fn area(&self)
컴파일러 안전 보장
② 안전 API (Safe wrapper)
SAFETY: 전제조건을 주석으로 계약화
unsafe { ... }
FFI · raw ptr · MMIO
④ 안전한 결과
보장 안전 반환
외부는 항상 안전한 API 만
unsafe 는 안전 API 안에 격리되므로 — 호출자(외부)는 절대 unsafe 를 직접 만나지 않습니다
커널 드라이버: 필요한 곳에서만 unsafe 를 쓰고, 나머지는 전부 안전하게 유지
// 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 FFI 호출 흐름
C ↔ Rust FFI 호출 흐름
중앙의 FFI 경계를 사이에 두고 C와 Rust가 양방향으로 함수를 호출합니다
① FFI 경계와 양방향 호출
C 언어
Rust
FFI 경계 (unsafe)
Rust → C 호출
C 함수
size_t strlen(const char*)
Rust 호출부
extern "C" · unsafe { ... }
C → Rust 호출
C 호출부
rust_fn(); /* extern */
Rust 노출 함수
#[no_mangle] extern "C" fn
② C ↔ Rust 타입 매핑 — FFI 로 주고받는 대표 타입
C 타입
Rust 타입
비고
int / unsigned int
c_int / c_uint
플랫폼 의존 크기
char * / const char *
*mut/*const c_char
CStr / CString 변환
void *
*mut c_void
불투명 포인터
size_t / ssize_t
usize / isize
포인터 크기
struct foo *
*mut bindings::foo
bindgen 생성
시그니처 선언과 타입 매핑은 bindgen / cbindgen 으로 자동 생성하면 실수 위험을 크게 줄일 수 있습니다
// 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 매크로나 인라인 함수(Inline Function)는 bindgen으로 생성할 수 없으므로, rust/helpers/에 C 헬퍼 래퍼를 작성합니다.
패턴 C 측 Rust 측 용도
Opaque<T> struct mutexOpaque<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를 폴링(Polling)하여 실행합니다.
Future 폴링 상태 머신
Future 폴링 상태 머신
Executor 가 poll() → Future 가 Pending/Ready 로 응답 — Pending 이면 대기 후 재-poll 하는 루프
① 폴링 순환 흐름
Executor
tokio · async-std
poll(cx)
Future (상태 머신)
async fn → 컴파일러가 생성한 enum
각 await 지점 = 상태(variant)
await₁
await₂
완료
Poll::Ready(T)
완료 — 결과값 반환
결과값 T
Poll::Pending
아직 준비 안 됨 → 대기
Pending → Waker 가 깨우면 다시 poll() — 반복
② 핵심 개념
· Future 는 지연(lazy) — .await 또는 executor 가 poll() 할 때만 진행
· 상태 머신 — async fn 의 각 .await 지점이 enum variant 로 변환 → 힙 할당 최소화 · 제로 코스트
· Pending 이면 Waker 등록 후 대기 — Waker 가 깨우면 다시 poll() 하며 반복
· Ready(T) 반환 시 완료 — .await 는 이 시점에서 진행을 재개
// 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를 반환 (아직 실행 안 됨) 작업 구조체 생성
.awaitFuture를 폴링, 미완료 시 제어권 반환 schedule()로 양보(Yield)
ExecutorFuture들을 스케줄링하고 폴링 커널 워크큐/스케줄러(Scheduler)
WakerI/O 완료 시 Executor에 알림 인터럽트/completion
join!여러 Future를 동시에 폴링 여러 워크 아이템 동시 큐잉
select!가장 먼저 완료되는 Future 선택 wait_event() + 조건 검사
Stream비동기 반복자 (여러 값을 순차 yield) 링 버퍼(Ring Buffer)에서 순차 읽기
⚠️
커널에서의 async: 커널은 tokio 같은 사용자 공간 런타임을 사용할 수 없습니다. 커널 Rust에서는 workqueue, 타이머(Timer), 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::sleepasync_std::task::sleep
I/O epoll/kqueue/IOCP 직접 통합 tokio의 mio 사용
채널 tokio::sync::mpscasync_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는 컴파일 타임에 태스크(Task) 수가 결정됨
#[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의 타입 시스템은 매우 표현력이 높으며, 타입 레벨에서 복잡한 불변 조건을 인코딩할 수 있습니다.
타입 상태 패턴 상태 전이
타입 상태 패턴 (Typestate Pattern) — 커널 디바이스 생명 주기
디바이스의 상태를 타입(Device<State>)으로 인코딩 — 잘못된 전이를 컴파일 타임에 차단합니다
① 생명 주기 상태 전이
Uninitialized
Device<Uninitialized>
new() 만 가능
Initialized
Device<Initialized>
start() 가능
Active
Device<Active>
read / write 가능
.init()?
.start()?
.stop()
② 패턴의 세 가지 효과
✗ 금지된 전이
Device<Uninitialized>
.start() ← 호출 불가
→ 컴파일 에러
start() 는 Initialized / Active
상태에서만 존재합니다
오류 → Drop (자동 정리)
.init()? / .start()? 실패 시
반환된 Err 는 그대로 반환되고
부분 상태의 장치도
Drop(RAII) 로 자동 정리 —
메모리 누수 없음
핵심 통찰
잘못된 상태 전이
= 컴파일 에러
(런타임 비용 0)
PhantomData<State> 로 상태를
타입에 인코딩 · 크기 0 바이트
// 연관 타입(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 개념도: for<'a> 바운드의 수명 범위
Higher-Rank Trait Bounds (HRTB)
for<'a> — 'a 를 '하나의 고정 수명'이 아닌 '모든 수명에 걸친' 다형 수명으로 선언합니다
① 일반 수명 바운드 vs HRTB — 수명 커버리지
일반 수명 바운드
fn apply<'a>(f: impl Fn(&'a str))
'a 는 호출 시점에 하나로 고정
'a (하나의 수명만)
특정 수명 하나에만 동작 → 제한적
vs
HRTB — for<'a>
fn apply(f: impl for<'a> Fn(&'a str))
'a 는 호출마다 새로 결정 — 모든 수명
'a₁
'a₂
'a₃ …
어떤 수명이든 처리 가능 → 다형적
② 실전 활용 예시 — 언제 HRTB 가 필요한가
fn call_with_ref(
f: impl for<'a> Fn(&'a i32) -> &'a i32,
) {
let x = 42; // 'a = x 의 수명
let r = f(&x); // 어떤 수명이든 처리 OK
}
클로저 f 는 호출되는 순간의 수명을
그때그때 알아야 하므로 미리 하나로
고정할 수 없습니다
→ for<'a> 바운드 필수
for<'a> 가 없으면 수명이 추론되지
않아 컴파일 실패합니다
참고: 그냥 Fn(&str) 로 써도 Rust 는 내부적으로 for<'a> Fn(&'a str) 로 해석합니다
// === 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 메모리 모델
Pin 메모리 모델 — 자기 참조 보호
자기 참조 구조체가 move 되면 댕글링 — Pin 은 주소를 고정해 자기 참조를 지킵니다
① Pin 없이 move 시 댕글링 vs Pin 으로 고정
✗ Pin 없이 — move 시 댕글링 (UAF)
SelfRef (구조체)
주소 0x1000
ptr → 0x1000
이동 전
move!
0x2000: data
ptr → 0x1000 ✗
(이동 후 주소와 불일치)
이동 후
0x1000 은 해제됨
ptr 이 해제된 0x1000 을 가리킴
→ UAF (Use After Free) 발생
✓ Pin<&mut T> — 이동 방지
SelfRef (고정)
주소 0x2000
ptr → 0x2000
🔒
주소 고정
✗ move 차단!
Pin 이 메모리 이동을 막아
자기 참조가 항상 유효
② Unpin 트레이트 — 이동 허용 여부
T: Unpin (대부분의 타입)
Pin<&mut T> → &mut T 자유 변환
자기 참조 없음 → 이동 안전
int · String · Vec 적 등
T: !Unpin (async 상태 머신 등)
Pin<&mut T> → &mut T 추출 불가
이동 방지가 컴파일 타임에 강제됨
await 지점 사이 자기 참조 유지
③ Pin 실무 — 커널 활용 · 생성 · 핵심
커널에서의 Pin
Mutex · ListHead 등 자기 참조
구조체 초기화는 pin_init! 매크로
Pin 생성 방법
Box::pin(val)
Pin::new(val) — Unpin 만 · unsafe new_unchecked
핵심
타입 시스템으로 이동 방지
런타임 비용 0 (제로 코스트)
// 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: UnpinPin에서 포인터 추출 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 커널, 부트로더(Bootloader) 등에서 필수입니다.
Linux 커널 Rust 코드는 전적으로 no_std 환경에서 동작합니다.
std · core · alloc 세 계층 관계
Rust 표준 라이브러리는 세 계층으로 분리됩니다. no_std 환경에서도 일부 계층은 사용할 수 있습니다.
크레이트 의존성 주요 내용 no_std 가용
coreOS 불필요 (순수 언어 기능) Option, Result, 기본 트레이트, 반복자, 원자 타입, fmt ✅ 항상 사용 가능
alloc글로벌 할당자(Allocator) 필요 Box, Vec, String, Arc, BTreeMap — 힙 할당 컨테이너(Container) ✅ 할당자 구현 시
stdOS 필요 (파일·스레드·소켓(Socket)) alloc + core + 파일 I/O, 스레드, 네트워크, 환경 변수 ❌ 커널에서 불가
no_std 프로그램 기본 골격
#![no_std] // std 비활성화 — core 크레이트만 암묵적으로 사용
#![no_main] // fn main() 대신 직접 진입점 정의 (OS 없으므로)
use core::panic::PanicInfo;
// no_std에서는 패닉 핸들러를 직접 정의해야 함
#[panic_handler]
fn panic (_info: &PanicInfo) -> ! {
loop {} // 커널: BUG() 매크로 호출 또는 무한 루프로 시스템 정지
}
// 힙 할당이 필요하다면 글로벌 할당자(GlobalAlloc) 구현 필요
// 커널에서는 rust/kernel/allocator.rs가 kmalloc 기반 할당자를 제공
extern crate alloc; // 할당자 구현 후 alloc 크레이트 활성화
use alloc::vec::Vec; // Vec 사용 가능 (힙 할당)
std → core / alloc API 대응표
std 타입·함수 no_std 대응 크레이트 비고
std::option::Optioncore::option::Optioncore 프리루드로 자동 포함
std::result::Resultcore::result::Resultcore 프리루드로 자동 포함
std::fmt::Writecore::fmt::Writecore write! 매크로 사용 가능
std::sync::atomiccore::sync::atomiccore AtomicU32 등 원자 타입 사용 가능
std::mem::*core::mem::*core size_of, align_of, transmute 등
std::boxed::Boxalloc::boxed::Boxalloc 글로벌 할당자 구현 필요
std::vec::Vecalloc::vec::Vecalloc 글로벌 할당자 구현 필요
std::string::Stringalloc::string::Stringalloc 글로벌 할당자 구현 필요
std::sync::Arcalloc::sync::Arcalloc 글로벌 할당자 구현 필요
std::thread— 없음 커널은 자체 태스크(Task) 스케줄러 사용
std::fs— 없음 OS 파일시스템(Filesystem) API 불가
println!pr_info!(), pr_err!()kernel 커널 전용 로그 매크로
커널 Rust 프로그래밍
상세 가이드: 리눅스 커널 Rust 프로그래밍 가이드 에서 Rust for Linux 아키텍처, 커널 모듈 작성, misc/platform/PCI/네트워크 드라이버, Workqueue/Timer API, KUnit 테스팅, C→Rust 마이그레이션 전략을 다룹니다.
Rust 1.85 & 2024 Edition (2025~2026)
2025년 2월 20일에 릴리스된 Rust 1.85.0 은 Rust 2024 Edition 을 stable로 승격한 버전으로,
2015/2018/2021에 이은 네 번째 Edition입니다. Edition은 하위 호환을 깨는 변경을 크레이트 단위로 선택 할 수 있게 해 주는 장치로,
오래된 크레이트와 최신 크레이트가 동일한 툴체인에서 함께 빌드되는 구조를 유지합니다.
한 줄 요약: Cargo.toml에 edition = "2024"만 적으면 async closure, RPIT 수명 자동 캡처, unsafe extern 블록 등 2024 Edition 문법이 활성화됩니다. 단, let chains처럼 에디션은 2024지만 이후 툴체인(1.88+)에서 안정화된 기능 도 있으므로, rustup update stable로 최신 안정 버전을 유지하고 rust-version을 함께 지정하는 것이 좋습니다. cargo fix --edition으로 기존 2021 크레이트를 대부분 자동 변환할 수 있습니다.
async closure — async || { ... }
오랫동안 Rust에서는 FnMut이 캡처한 가변 참조를 반환하는 Future에 담을 수 없어서, "비동기 클로저"를 흉내 내려면 impl Fn() -> impl Future<Output=T> 같은 복잡한 시그니처가 필요했습니다.
1.85는 AsyncFn/AsyncFnMut/AsyncFnOnce 트레이트와 async || {} 리터럴을 도입해 이 문제를 근본적으로 해결했습니다.
// 2024 Edition 전 — 복잡한 HRTB 시그니처
fn retry_old <F, Fut, T>(mut f: F) -> Fut ::Output
where
F: FnMut () -> Fut ,
Fut: Future <Output = Result <T, Error >>, { /* ... */ }
// 2024 Edition — AsyncFn 트레이트로 간결하게
async fn retry <T, E>(mut op: impl AsyncFnMut () -> Result <T, E>) -> Result <T, E> {
let mut delay = Duration ::from_millis(100 );
for _ in 0 ..3 {
match op().await {
Ok (v) => return Ok (v),
Err (_) => tokio::time::sleep(delay).await ,
}
delay *= 2 ;
}
op().await
}
// 호출부 — async 클로저 리터럴
retry(async || fetch("https://api" ).await ).await ?;
let chains — if let ... && ... && let ...
조건문 안에 let과 불리언 식을 &&로 섞어 쓸 수 있게 되었습니다.
중첩된 if let이 사라져 early-return 패턴이 훨씬 평탄해집니다.
💡
안정화 시점 참고: let chains는 2024 Edition 전용 기능이지만, 1.85.0에는 아직 nightly였고 Rust 1.88.0 (2025-06-26)에서 stable로 승격되었습니다. 2024 Edition을 쓰더라도 1.88 이상의 툴체인이 필요합니다.
// Before (2021 Edition)
if let Some (user) = lookup(id) {
if user.is_admin() {
if let Some (session) = user.session() {
process(session);
}
}
}
// 2024 Edition — let chains
if let Some (user) = lookup(id)
&& user.is_admin()
&& let Some (session) = user.session()
{
process(session);
}
RPIT 수명 자동 캡처 (RFC 3498)
impl Trait 반환 타입에서 수명 매개변수를 자동으로 캡처하도록 바뀌었습니다.
2021 Edition에서는 impl Iterator<Item=&'a T> + '_처럼 명시적으로 써야 했던 경우가 많이 줄어듭니다.
// 2021 Edition — 수명 캡처를 명시해야 함
fn search <'a >(xs: &'a [i32 ], needle: i32 )
-> impl Iterator <Item = &'a i32 > + 'a {
xs.iter().filter(move |&&x| x == needle)
}
// 2024 Edition — 수명이 자동 캡처됨
fn search <'a >(xs: &'a [i32 ], needle: i32 )
-> impl Iterator <Item = &'a i32 > {
xs.iter().filter(move |&&x| x == needle)
}
unsafe extern 블록과 unsafe 속성
extern "C" 함수 선언은 본질적으로 안전하지 않지만(호출자 측 안전 책임), 과거에는 선언 시점 에는 unsafe가 붙지 않아 모호했습니다. 2024 Edition은 이 구분을 명시화합니다.
// 2024 Edition — 블록 자체에 unsafe, 함수 호출은 여전히 unsafe
unsafe extern "C" {
pub fn kmalloc (size: usize , flags: u32 ) -> *mut c_void ;
// safe 약속이 가능한 함수는 safe로 표시 가능
pub safe fn kernel_version () -> u32 ;
}
// 안전성이 호출 측 조건에 달린 속성도 unsafe로 명시
#[unsafe(no_mangle)]
#[unsafe(export_name = "my_handler" )]
pub extern "C" fn handler () { /* ... */ }
그 밖의 주요 변경
항목 2021 Edition 2024 Edition
if let / match 임시값 수명 블록 끝까지 보존 분기별로 즉시 drop (drop 순서 재정의)
pattern에서 & 사용 deref 자동 매칭 명시적 & 필요한 경우 확대 (RFC 3627)
unsafe 속성 #[no_mangle] 등을 그냥 사용#[unsafe(no_mangle)] 필수
rustfmt 스타일 언어 Edition과 연동 style_edition = "2024"로 독립 진화
trait diag 자동 추천 무제한 #[diagnostic::do_not_recommend]로 크레이트 저자가 제어
Cargo resolver rust-version 무시 의존성 선택 시 rust-version 반영
Edition 전환 절차
# 1) 현재 크레이트 lint 통과시키기
cargo fix --edition
# 2) Cargo.toml에서 edition 올리기
[package]
name = "mycrate"
edition = "2024"
rust-version = "1.85" # MSRV 명시
# 3) 재빌드 — 남아있는 변경은 수동 수정
cargo build --all-targets
cargo test
주의: 2024 Edition의 임시값 수명 변경은 if let ... { ... } 블록에서 RAII 락을 쥐는 패턴(예: if let Some(x) = mutex.lock().as_ref() { ... })에 영향을 미칩니다. 의도하지 않은 락 해제가 발생할 수 있으므로, cargo fix --edition이 자동으로 추가하는 match 재작성 힌트를 반드시 검토하세요.
Rust 1.86~1.90 주요 변경
1.86 (2025-04-03): trait upcasting 안정화 — &dyn Sub를 수퍼트레이트인 &dyn Super로 암묵 변환 가능. 안전 함수에 #[target_feature] 사용 가능(target_feature_11), 슬라이스·HashMap의 get_disjoint_mut로 동시 가변 참조, missing_abi lint 기본 경고.
1.87 (2025-05-15): 표준 라이브러리의 익명 파이프 (std::io::pipe) 도입, 대상 target_feature가 켜진 안전 코드에서 std::arch intrinsics 직접 호출, asm!에서 러스트 코드 라벨로 점프, trait 정의의 impl Trait 정밀 캡처(+ use<..>), Vec::extract_if 안정화.
1.88 (2025-06-26): 2024 Edition에서 let chains (if let ... && let ...) 안정화, naked 함수 (#[unsafe(naked)] + naked_asm!) 도입, HashMap::extract_if/HashSet::extract_if 등 API 추가.
1.89 (2025-08-07): 상수 제네릭 인자 _ 추론([false; _]), mismatched_lifetime_syntaxes lint, x86 target_feature 확장(sha512/sm3/sm4/kl, avx512bw), extern "C" 함수에서 i128/u128 허용.
1.90 (2025-09-18): x86_64-unknown-linux-gnu 타깃에서 LLD 기본 링커 전환(링크 성능 향상, -Clinker-features=-lld로 해제 가능), Cargo 워크스페이스 전체 발행 cargo publish --workspace 네이티브 지원.
💡
버전 착각 주의: LazyLock/LazyCell는 1.80 (2024-07-25), #![expect(lint)]는 1.81 (2024-09-05)에 각각 안정화되었습니다. 만약 항목이 위 1.86~1.90 목록에 없으면 후속 릴리스보다 앞선 버전에서 이미 안정화된 것입니다. 본 목록(1.85~1.90)은 각 해당 릴리스 시점의 공식 블로그 릴리스 노트를 기준으로 작성되었습니다.
Rust 언어와 커널 적용에 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.
사이트 내부 문서
외부 참고 자료
자료 URL 설명
— Rust 공식 학습 자료 —
The Rust Programming Language The Rust Programming Language (doc.rust-lang.org) Rust 공식 학습서, 소유권·트레이트·동시성 등 핵심 개념 체계적 설명
Rust by Example Rust by Example (doc.rust-lang.org) 실행 가능한 예제 중심의 Rust 학습 가이드
Rust Reference The Rust Reference (doc.rust-lang.org) Rust 언어의 공식 명세 — 문법, 타입 시스템, 메모리 모델 상세 정의
Rustonomicon The Rustonomicon — unsafe Rust 가이드 (doc.rust-lang.org) unsafe 코드 작성 규칙, 미정의 동작, FFI, 원시 포인터 심층 해설
Rust Edition Guide Rust Edition Guide (doc.rust-lang.org) Rust 2015/2018/2021/2024 에디션별 변경 사항과 마이그레이션 안내
Rust Compiler Error Index Rust Compiler Error Index (doc.rust-lang.org) rustc 에러 코드(E0xxx) 전체 목록과 해결 방법
— 표준 라이브러리 & API —
Rust 표준 라이브러리 Rust Standard Library Documentation (doc.rust-lang.org) std 크레이트 전체 API 레퍼런스 — Vec, HashMap, Arc, Mutex 등
Rust API Guidelines Rust API Guidelines (rust-lang.github.io) 관용적 Rust API 설계 원칙 — 네이밍, 타입 안전성, 문서화 규칙
— 도구 & 생태계 —
Cargo Book The Cargo Book (doc.rust-lang.org) Rust 패키지 매니저 및 빌드 시스템 공식 문서
Clippy Lint 목록 Clippy Lints (rust-lang.github.io) Clippy 정적 분석 린트 전체 목록 — correctness, style, performance 분류
Rustup 문서 Rustup Documentation (rust-lang.github.io) Rust 툴체인 관리자 — 설치, 업데이트, 크로스 컴파일(Cross Compilation) 타겟 관리
crates.io Rust 패키지 레지스트리 (crates.io) Rust 크레이트 공식 저장소 — 검색, 다운로드, 의존성 관리
docs.rs 크레이트 API 문서 호스팅 (docs.rs) crates.io에 게시된 모든 크레이트의 자동 생성 API 문서
— 고급 주제 & 패턴 —
Rust Design Patterns Rust Design Patterns (rust-unofficial.github.io) Rust 관용 패턴, 안티패턴, 함수형/OOP 디자인 패턴 모음
Rust Performance Book The Rust Performance Book (nnethercote.github.io) 컴파일 시간·런타임 성능 최적화 기법, 프로파일링(Profiling), 벤치마크 가이드
Rust Atomics and Locks Rust Atomics and Locks (marabos.nl) 원자적 연산(Atomic Operation), 메모리 순서, 잠금 구현 — Mara Bos 저, 동시성 심화
Asynchronous Programming in Rust Async Book (rust-lang.github.io) async/await, Future 트레이트, 비동기 런타임 공식 가이드
— 커널 & 시스템 프로그래밍 —
Rust for Linux 공식 Rust for Linux 프로젝트 홈페이지 (rust-for-linux.com) 프로젝트 현황, 최신 소식, 기여 가이드
커널 Rust 문서 Kernel Rust Documentation (docs.kernel.org) 공식 커널 Rust 지원 문서 — 빌드 요구사항, API 바인딩
GitHub 저장소 Rust-for-Linux/linux (github.com) 커널 Rust 소스 코드 및 이슈 트래커
LKML Rust Rust for Linux 메일링 리스트 (lore.kernel.org) 커널 Rust 관련 패치(Patch), 논의, RFC 아카이브
Rust 공식 블로그 Rust Blog (blog.rust-lang.org) 릴리스 공지, 안정화 기능 소개, 로드맵 공유
Rust Playground Rust Playground (play.rust-lang.org) 브라우저에서 Rust 코드 즉시 실행 및 공유