Rust unsafe
이 강좌에서 이야기한 모든 보장, 그러니까 댕글링 참조자가 없고, 데이터 경쟁이 없고, 범위를 벗어난 접근이 없다는 건 모두 컴파일러가 지켜 줘요. unsafe는 컴파일러에게 “네가 안전한지 확인할 수 없는 일을 할게. 대신 책임은 내가 질게”라고 말하는 방법이에요.
unsafe가 하는 일과 하지 않는 일
섹션 제목: “unsafe가 하는 일과 하지 않는 일”unsafe가 Rust의 안전 검사를 통째로 꺼 버린다고 오해하는 경우가 많아요. 그렇지 않아요. unsafe 블록 안에서는 평소에 금지된 일을 딱 다섯 가지 더 할 수 있어요.
- 원시 포인터(raw pointer) 역참조하기
unsafe함수 호출하기- 가변
static변수에 접근하거나 수정하기 unsafe트레이트 구현하기union의 필드에 접근하기
그 밖의 모든 것, 즉 대여 규칙, 이동(move) 규칙, 타입 검사는 unsafe 코드 안에서도 그대로 적용돼요. unsafe는 다섯 가지 특정 작업을 위한 메스이지, 안전 장치를 끄는 스위치가 아니에요.
원시 포인터
섹션 제목: “원시 포인터”Rust의 일반 참조자(&와 &mut)는 언제나 올바르게 정렬된 유효한 데이터를 가리켜요. 이게 컴파일러가 보장하는 것 중 하나예요. 원시 포인터(*const T나 *mut T)는 그 보장을 내려놓아요. 그래서 원시 포인터를 읽으려면 unsafe가 필요해요.
fn main() { let value = 5; let pointer = &value as *const i32;
unsafe { println!("{}", *pointer); }}실행 결과:
5원시 포인터를 만드는 건 안전한 코드에서도 할 수 있어요. unsafe가 필요한 건 역참조(포인터가 가리키는 값을 읽기)할 때뿐이에요. 컴파일러가 더 이상 그 포인터가 유효한지 확인할 수 없으니까요.
unsafe 함수 호출하기
섹션 제목: “unsafe 함수 호출하기”어떤 함수는 그 자체에 unsafe가 붙어 있어요. 컴파일러가 대신 확인해 줄 수 없는 요구 사항이 있기 때문이에요. 이런 함수를 호출하는 건 “그 요구 사항은 내가 직접 확인했어”라고 약속하는 거예요.
unsafe fn dangerous() { println!("이 함수는 여러분이 요구 사항을 확인했다고 믿어요");}
fn main() { unsafe { dangerous(); }}실행 결과:
이 함수는 여러분이 요구 사항을 확인했다고 믿어요unsafe를 안전한 함수로 감싸기
섹션 제목: “unsafe를 안전한 함수로 감싸기”표준 라이브러리에는 실제로 호출할 수 있는 unsafe 함수들이 있어요. 그중 하나가 슬라이스의 get_unchecked예요. numbers[i]처럼 동작하지만, 범위 검사를 건너뛰어요. i가 범위를 벗어나도 패닉하지 않고, 그 자리에 우연히 있는 메모리를 그냥 읽어요. 이걸 정의되지 않은 동작(undefined behavior)이라고 해요. 프로그램이 죽을 수도 있고, 쓰레기 값을 출력할 수도 있고, 멀쩡해 보이다가 어느 날 갑자기 문제가 생길 수도 있어요.
그래서 이 함수를 호출하는 규칙은 “유효하다고 확실히 아는 인덱스로만 호출하기”예요. 보통은 평범하고 안전한 함수 안에서 그 규칙을 직접 확인하는 패턴을 써요.
fn first_or_zero(numbers: &[i32]) -> i32 { if numbers.is_empty() { return 0; } // SAFETY: 슬라이스가 비어 있지 않으니 인덱스 0은 범위 안이에요. unsafe { *numbers.get_unchecked(0) }}
fn main() { println!("{}", first_or_zero(&[7, 8, 9])); println!("{}", first_or_zero(&[]));}실행 결과:
70호출하는 쪽에서 보이는 걸 보세요. fn first_or_zero(numbers: &[i32]) -> i32예요. 시그니처에 unsafe가 없고, 잘못 호출할 방법도 없어요. 함수가 unsafe 블록에 닿기 전에 규칙부터 확인하니까요. unsafe는 작게, 안쪽에 숨겨져 있어요.
경험 많은 Rust 개발자들이 지키는 습관이 두 가지 있어요.
- 모든
unsafe블록 위에// SAFETY:주석을 쓰세요. 규칙이 왜 지켜지는지 적는 거예요. 다음에 코드를 읽을 사람과, 미래의 자신을 위해서요. unsafe블록은 최대한 작게 유지하세요. 그래야 다시 확인해야 할 코드가 줄어요.
솔직히 말하면, 실제 코드에서는 이 함수를 이렇게 쓰지 않을 거예요. numbers.first()가 같은 일을 안전하게 해 주고, 컴파일러는 필요 없다고 증명할 수 있는 범위 검사를 아주 잘 없애 주거든요. 하지만 이 패턴(규칙을 확인한 다음, 안전한 함수 안에서 unsafe를 호출하기)은 Vec, String 같은 타입이 만들어지는 방식 그대로예요. FFI 페이지에서도 같은 방법으로 C 함수를 감싸요.
애초에 unsafe가 왜 있을까
섹션 제목: “애초에 unsafe가 왜 있을까”Rust가 안전이 전부라면, 왜 이런 걸 허용할까요? 실제로는 완벽하게 안전한데도 컴파일 시간에 안전하다고 증명하는 게 정말로 불가능한 일들이 있기 때문이에요. 하드웨어와 직접 대화하기, C 라이브러리 호출하기(FFI 참고), 특정 고성능 자료 구조 만들기 같은 것들이요. unsafe 덕분에 표준 라이브러리(그리고 드물게는 여러분의 코드)가 이런 저수준 부품을 구현할 수 있고, 그 위에 쌓이는 나머지 코드는 모두 평범하고 완전히 검사받는 안전한 Rust로 남아요.
솔직한 득과 실
섹션 제목: “솔직한 득과 실”unsafe를 쓴다고 해서 코드에 버그가 생기는 건 아니에요. 그 블록 안에서는 컴파일러가 특정 종류의 실수를 더 이상 잡아 줄 수 없으니, 같은 보장을 여러분이 직접 지켜야 한다는 뜻이에요. 경험 많은 Rust 개발자 대부분은 unsafe 코드를 직접 쓰는 일이 아주 적어요. unsafe는 보통 라이브러리 맨 아래쪽의 작고 꼼꼼히 검토된 부분에만 있고, 다른 모두가 쓸 수 있도록 안전한 API로 감싸져 있어요.
연습 문제
섹션 제목: “연습 문제”1. 원시 포인터로 값 바꾸기
섹션 제목: “1. 원시 포인터로 값 바꾸기”let mut score = 10;을 만들고, 그 값을 가리키는 *mut i32 원시 포인터를 만든 다음, 포인터를 통해 5를 더하세요. 그리고 score를 출력하세요. 힌트: 포인터는 &mut score as *mut i32로 만들고, unsafe 블록 안에서 *pointer += 5라고 쓰세요.
정답 보기
fn main() { let mut score = 10; let pointer = &mut score as *mut i32; // 만드는 건 안전해요
unsafe { *pointer += 5; // 포인터를 통해 쓰는 건 안전하지 않아요 }
println!("점수 = {score}");}실행 결과:
점수 = 15score가 아직 살아 있고, 값을 쓰는 동안 다른 누구도 score를 쓰지 않으니 실제로는 안전해요. unsafe를 쓰는 순간, 이런 약속을 지키는 건 여러분의 몫이에요.
2. 안전한 get_or
섹션 제목: “2. 안전한 get_or”fn get_or(numbers: &[i32], index: usize, default: i32) -> i32를 작성하세요. index 위치의 항목을 반환하고, 인덱스가 범위를 벗어나면 default를 반환해요. 값을 읽을 때는 get_unchecked를 쓰고, 이 함수가 규칙을 어기는 방식으로는 절대 호출될 수 없게 만드세요. 힌트: 먼저 index < numbers.len()을 비교하고, 그게 참일 때만 get_unchecked를 호출하세요.
정답 보기
fn get_or(numbers: &[i32], index: usize, default: i32) -> i32 { if index < numbers.len() { // SAFETY: 방금 index가 범위 안인지 확인했어요. unsafe { *numbers.get_unchecked(index) } } else { default }}
fn main() { let numbers = [10, 20, 30]; println!("{}", get_or(&numbers, 1, -1)); println!("{}", get_or(&numbers, 5, -1));}실행 결과:
20-1인덱스 5는 unsafe 블록에 절대 닿지 않으니 안전하게 처리돼요. 비교해 보면, 완전히 안전한 버전은 numbers.get(index).copied().unwrap_or(default)이고, 평소에는 이렇게 쓰면 돼요.
unsafe로 할 수 있게 되는 작업은 딱 다섯 가지예요(주로 원시 포인터와 unsafe 함수 호출). 나머지는 여전히 모두 검사받아요.- 원시 포인터(
*const T/*mut T)는 안전한 코드에서도 만들 수 있어요. 역참조할 때만unsafe가 필요해요. unsafe는 규칙부터 확인하는 작고 안전한 함수로 감싸고, 왜 문제가 없는지// SAFETY:주석으로 설명하세요.unsafe는 저수준 코드(하드웨어 접근, C 연동)를 만들 수 있도록 존재하고, 그걸 쓰는 코드는 안전하게 유지돼요.- 초보자라면 직접 쓸 일은 드물어요. 먼저 안전한 추상화를 찾아보세요.
이해도 점검
섹션 제목: “이해도 점검”이해도 점검
1. unsafe 블록이 실제로 허용해 주는 건 무엇일까요?
2. 안전한 코드에서 원시 포인터를 만들 수 있을까요?
3. 초보자라면 unsafe를 직접 쓸 일이 얼마나 자주 있을까요?
4. unsafe 코드를 쓰면 프로그램에 버그가 생기는 걸까요?
점수: 0 / 4
더 알아보기
섹션 제목: “더 알아보기”- 안전하지 않은 러스트 — The Rust Programming Language 한국어판: 다섯 가지 “unsafe 슈퍼파워”를 자세히 다루고, 원시 포인터 위에 안전한
split_at_mut를 만드는 예제도 있어요. - The Rustonomicon (영어): unsafe 코드를 올바르게 쓰는 법을 다루는 공식 가이드예요. 고급 내용이지만 첫 장은 꼭 읽어 볼 만해요.
unsafe키워드 — 표준 라이브러리 문서 (영어): unsafe 블록, 함수, 트레이트와// SAFETY:주석 관례를 설명해요.- Unsafety — The Rust Reference (영어):
unsafe가 필요한 작업의 정확한 목록이에요. slice::get_unchecked— 표준 라이브러리 문서 (영어): 실제 unsafe 함수예요. “Safety” 절에 지켜야 할 규칙이 적혀 있어요.