12. Unsafe Rust: 통제된 위험 🔴
학습 목표:
- 컴파일러가 검증할 수 없는 다섯 가지 Unsafe 초능력과 그 사용 시점을 이해합니다.
- 건전한 추상화(Sound Abstraction): 안전한 API로 비안전한 내부 로직을 감싸는 법을 배웁니다.
- C 언어와 통신하기 위한 FFI 패턴을 익힙니다.
- 흔히 발생하는 **미정의 동작(UB)**의 함정과 아레나/슬래브 할당자 패턴을 학습합니다.
Unsafe의 다섯 가지 초능력
unsafe 키워드는 컴파일러가 안전성을 보장할 수 없는 다섯 가지 특수 작업을 허용합니다.
#![allow(unused)] fn main() { unsafe { // 1. RAW 포인터 역참조 let value = *raw_ptr; // 2. Unsafe 함수 또는 메서드 호출 let mem = std::alloc::alloc(layout); // 3. 가변 정적 변수(static mut) 접근 및 수정 COUNTER += 1; // 4. Unsafe 트레이트 구현 unsafe impl Send for MyType {} // 5. 유니온(Union) 필드 접근 let f = u.f; } }
핵심 원칙:
unsafe가 빌림 검사기(Borrow checker)를 끄는 것은 아닙니다. 오직 위 다섯 가지 능력만 해제하며, 나머지 모든 Rust 규칙은 여전히 적용됩니다.
건전한 추상화 (Sound Abstraction) 작성
unsafe의 진정한 목적은 위험한 로직을 내부에 숨기고, 외부 사용자에게는 절대 오용할 수 없는 안전한 인터페이스를 제공하는 것입니다.
건전한 Unsafe 코드의 3계명:
- 불변성(Invariant) 문서화: 모든
unsafe블록 위에// SAFETY:주석을 달아 왜 이 작업이 안전한지 논리적으로 설명하세요. - 캡슐화: 사용자가 안전한 API만 사용해도 내부에서 미정의 동작(UB)이 발생하지 않도록 설계하세요.
- 최소화:
unsafe블록은 필요한 최소한의 범위로 유지하세요.
커스텀 할당자: 아레나(Arena)와 슬래브(Slab)
대규모 시스템에서는 표준 할당자(malloc)의 오버헤드를 줄이기 위해 특화된 할당 패턴을 사용합니다.
- 아레나(Arena): 포인터를 앞으로 밀기만 하는 매우 빠른 할당 방식입니다. 개별 해제는 불가능하며, 아레나 전체를 한꺼번에 해제하여 메모리 파편화를 방지합니다. 프레임 단위 처리나 요청 단위 작업에 최적입니다.
- 슬래브(Slab): 고정된 크기의 슬롯들을 미리 확보해 두고 재사용합니다. 메모리 파편화가 전혀 없으며, 고정 크기 객체(예: 커넥션 풀)를 빈번하게 생성하고 삭제할 때 최고의 성능을 냅니다.
미정의 동작(UB)의 일반적인 함정
| 함정 | 원인 | 방지법 |
|---|---|---|
| Null 역참조 | Null 포인터 접근 | ptr.is_null() 체크 시 수행 |
| 댄글링 포인터 | 해제된 메모리 접근 | 수명(Lifetime) 규칙 준수 |
| 데이터 경합 | 동기화 없는 static mut 접근 | Atomic이나 Mutex 사용 |
| Aliasing 위반 | 동일 데이터에 두 개 이상의 &mut 생성 | Rust의 대여 규칙 엄수 |
📝 연습 문제: Unsafe를 활용한 안전한 래퍼 제작 ★★★ (~45분)
고정 크기 스택 할당 벡터인 FixedVec<T, N>을 구현해 보세요.
push,pop,as_slice기능을 구현하되, 내부적으로는MaybeUninit을 사용하세요.- 모든 공개 API는 안전해야 하며, 내부의 모든
unsafe에는 상세한// SAFETY:주석을 달아야 합니다. Drop구현을 통해 초기화된 요소들이 정상적으로 소멸되도록 하세요.
📌 요약
- FFI 경계나 성능 병목 구간 외에는
unsafe사용을 지양하세요. - 모든 비안전한 연산 뒤에는 이를 안전하게 만드는 논리적 증거가 있어야 합니다.
- 아레나와 슬래브 할당자는 일반적인 힙 할당보다 수십 배 빠를 수 있습니다.
- 복잡한 메모리 레이아웃이 필요하다면 **
repr(C)**를 사용하여 C 언어와의 호환성을 확보하세요.