
Rust 'unsafe' 제대로 파헤치기: 개발자가 알아야 할 모든 것
Rust는 그 강력함과 안전성으로 많은 개발자들의 사랑을 받는 언어입니다.
하지만 Rust의 심오한 영역에는 'unsafe'라는 키워드가 존재하며, 이는 끊임없는 논쟁과 오해의 중심에 서 있습니다.
마치 숙련된 외과의사의 메스와 같아서, 정확하게 사용하면 생명을 살릴 수 있지만, 잘못 다루면 치명적인 결과를 초래할 수 있죠.
왜 'unsafe'는 이토록 많은 오해를 낳는 걸까요? 그리고 우리는 이 키워드를 어떻게 이해하고 받아들여야 할까요?
'unsafe', Borrow Checker를 '우회'하는 것인가?

일부 커뮤니티 유저들 사이에서는 'unsafe Rust'가 컴파일러의 핵심 안전 장치인 Borrow Checker를 비활성화한다고 생각하는 경향이 있습니다.
하지만 이는 'unsafe'의 작동 방식에 대한 미묘한 부분에서 비롯된 오해일 수 있습니다.
사실, 공식 문서에서는 "'unsafe' doesn’t turn off the Borrow Checker or disable any other of Rust’s safety checks"라고 명확히 밝히고 있습니다.
'unsafe' 키워드는 안전한 Rust에서는 불가능한 특정 동작을 허용하지만, 그렇다고 해서 Rust의 기본적인 안전 규칙 자체가 사라지는 것은 아닙니다.
마치 규정 속도를 초과할 수 있는 특수 차량을 허가하는 것과 같지만, 그렇다고 모든 교통 법규가 무시되는 것은 아닌 것과 같죠.
문제는 'unsafe'가 Raw Pointer(*const T
, *mut T
)의 dereference를 허용한다는 점입니다.
안전한 Rust에서는 참조(&T
, &mut T
)를 통해 데이터에 접근하며, 이때 Borrow Checker는 소유권, 빌림, 그리고 생명주기 규칙을 엄격하게 적용하여 데이터 경쟁과 댕글링 포인터를 방지합니다.
하지만 'unsafe' 블록 내에서는 Raw Pointer를 사용하여 메모리 주소에 직접 접근할 수 있으며, 이 과정에서 Borrow Checker의 감시망을 벗어날 수 있습니다.
한 커뮤니티 유저는 다음과 같은 코드를 예시로 제시하며, 'unsafe'가 Borrow Checker를 우회할 수 있다고 주장합니다.
pub fn main() {
let mut value = 42;
let ptr = &mut value as *mut i32;
let r1: &mut i32;
let r2: &mut i32;
unsafe {
r1 = &mut *ptr;
r2 = &mut *ptr;
}
*r1 += 1;
*r2 += 1;
println!("value: {}", value);
}
이 코드는 안전한 Rust에서는 Borrow Checker에 의해 금지되는, 하나의 가변 데이터에 대한 두 개의 가변 참조를 'unsafe' 블록을 사용하여 생성합니다.
이는 마치 은행 금고 하나의 열쇠를 두 사람에게 동시에 주는 것과 같아서, 데이터의 일관성을 해칠 수 있는 위험한 상황을 만듭니다.
이 유저는 과거에 "unsafe 코드는 Borrow Checker를 비활성화하지 않는다"는 주장을 믿었지만, 실제로 Low-level 코드를 작성하면서 Borrow Checker를 우회하는 방법을 발견하고 놀랐다고 합니다.
이때부터 그는 Rust에 대한 주장을 액면 그대로 믿기 어렵게 되었고, 숨겨진 세부 사항과 '말장난'이 존재할 수 있음을 깨달았다고 토로합니다.
이에 대해 다른 유저는 "Borrow Checker를 비활성화하는 것과 우회하는 것은 엄연히 다르다"고 반박합니다.
위의 예시는 Raw Pointer를 dereference하여 Borrow Checker의 검사를 거치지 않는 새로운 참조를 만드는 것이지, Borrow Checker 자체를 끄는 것은 아니라는 주장입니다.
안전한 Rust 코드를 'unsafe'로 감싼다고 해서 Borrow Checker의 규칙이 무효화되는 것은 아니라는 것이죠.
마치 기존 도로를 폐쇄하는 것이 아니라, 그 옆에 검문소가 없는 새로운 길을 만드는 것과 비슷하다는 비유입니다.
이 논쟁은 '비활성화'와 '우회'라는 단어의 의미론적 차이에서 비롯된 것으로 볼 수 있습니다.
중요한 점은 'unsafe'를 사용하면 Borrow Checker가 일반적으로 방지하는 특정 유형의 메모리 접근이 가능해지며, 이는 개발자가 추가적인 책임을 져야 함을 의미한다는 것입니다.
'unsafe', 안전하지 않은 것을 안전하게 만드는 것일까?
또 다른 커뮤니티 유저는 "'unsafe'는 안전하지 않은 것을 안전하게 만드는 것처럼 사용되는 것이 이해되지 않는다"고 지적합니다.
컴파일러 입장에서 'unsafe'는 일종의 경고 신호처럼 느껴질 수 있다는 것이죠.
이에 대해 다른 유저들은 다음과 같이 설명합니다. "'unsafe'는 코드를 안전하게 만드는 것이 아니라, 컴파일러가 검증할 수 없는 부분을 개발자가 의도적으로 작성했으며, 그 안전성을 개발자 스스로 보장하겠다는 약속과 같다"고 말합니다.
마치 컴파일러에게 "이 부분은 내가 책임질 테니, 너는 그냥 넘어가"라고 말하는 것과 같다는 것이죠.
Vec<T>
와 같은 표준 라이브러리 타입을 구현할 때 'unsafe'는 불가피하게 사용됩니다.
Vec
은 내부적으로 힙 메모리를 직접 관리하며, 버퍼의 크기를 조정하거나 새로운 요소를 추가할 때 Low-level의 메모리 조작이 필요합니다.
이러한 작업은 본질적으로 안전하지 않지만, Vec
의 개발자는 이러한 'unsafe'한 내부 구현을 안전한 인터페이스로 감싸서, 사용자가 Vec
을 사용할 때는 메모리 안전성을 걱정하지 않도록 합니다.
이는 마치 위험한 화학 물질을 다루는 연구원이 안전 장비를 착용하고 실험을 수행하고, 그 결과를 안전한 형태로 일반 사용자에게 제공하는 것과 같습니다.
일부 유저들은 'unsafe' 대신 manually_verified_safety
나 reduced_safety_checks
와 같이 더 명확한 의미를 전달하는 이름을 선호하기도 합니다.
하지만 'unsafe'라는 용어가 이미 널리 사용되고 있으며, 그 자체로 개발자에게 경각심을 주는 효과도 있다는 점을 간과할 수 없습니다.
마치 "여기에는 용이 산다(here_be_dragons)"라는 고대 지도의 문구처럼, 'unsafe'는 개발자에게 특별한 주의를 환기시키는 역할을 하는 것이죠.
'unsafe', 책임의 전환
결론적으로, 'unsafe' 키워드는 Rust의 안전 규칙을 완전히 무시하는 것이 아니라, 특정 상황에서 컴파일러의 자동 검증을 개발자의 수동 검증으로 전환하는 메커니즘입니다.
개발자는 'unsafe' 블록 내의 코드가 Soundness를 갖도록, 즉 Undefined Behavior(UB)를 일으키지 않도록 보장해야 할 책임을 집니다. 이는 마치 건축가가 안전 기준을 준수하여 건물을 설계하고 시공하는 것과 같습니다.
'unsafe'는 FFI(Foreign Function Interface)를 통해 C와 같은 다른 언어의 코드를 호출하거나, 성능critical한 영역에서 Low-level 최적화를 수행해야 할 때 필수적인 도구입니다.
숙련된 개발자는 'unsafe'를 사용하여 안전한 추상화 계층을 구축하고, Low-level의 복잡성을 사용자로부터 숨길 수 있습니다.
마치 복잡한 기계 장치를 전문가가 설계하고, 일반 사용자는 간단한 인터페이스를 통해 그 기능을 사용하는 것과 같습니다.
따라서 우리는 'unsafe'를 단순히 "위험한 것"으로 치부하기보다는, Rust 생태계의 중요한 일부분으로 이해하고, 필요한 경우 신중하게 사용해야 합니다.
핵심은 'unsafe'를 사용하는 이유를 명확히 하고, 그 사용이 코드 전체의 안전성(Soundness)에 어떤 영향을 미치는지 깊이 생각하는 것입니다.
마치 무거운 책임을 맡은 사람처럼, 'unsafe'를 사용할 때는 그 무게를 인지하고 신중하게 접근해야 합니다.
'Rust' 카테고리의 다른 글
인터뷰 번역) Tauri vs 다른 Rust GUI 프레임워크: Arboretum 개발자의 이야기 (1) | 2025.02.09 |
---|---|
녹슬지 않는 튼튼함, Rust의 강력한 타입 시스템: TypeScript 경험을 넘어선 새로운 세계 (0) | 2025.02.09 |
Rust 초보자의 경험: OOP 한계와 컴파일 시간 문제점 완벽 분석 (0) | 2025.02.09 |
리눅스 커널과 Rust의 갈등: Hector Martin 사퇴 이후의 미래와 과제 (0) | 2025.02.09 |
Rust로 간단한 CLI 유효성 검사 도구 만들기 (0) | 2024.11.24 |