안전하지 않은 러스트

우리가 여지껏 논해온 모든 코드들은 컴파일 타임에 강제되는 러스트의 메모리 안전성 보장을 갖습니다. 그러나, 러스트는 이러한 메모리 안전성 보장을 강제하지 않는 숨겨진 내부의 두번째 언어를 갖고 있습니다: 이것을 안전하지 않은 러스트 (unsafe Rust) 라고 부르며 그저 보통의 러스트와 비슷하게 동작하지만, 우리에게 추가적인 슈퍼파워를 제공합니다.

안전하지 않은 러스트는 정적 분석이 선천적으로 보수적이기 때문에 존재합니다. 컴파일러가 어떤 코드에 대한 안전성을 보장하는지 혹은 아닌지를 결정하는 시도를 할 때, 유효하지 않은 프로그램을 허용하는 것보다는 유효한 프로그램을 불허하는 편이 더 낫습니다. 그 코드가 괜찮았을지라도, 러스트가 그렇게 말할 수 있을 때까지는 괜찮은게 아닙니다! 이러한 경우, 우리는 컴파일러에게 “날 믿어, 내가 뭘 하고 있는지 알고 있어” 라고 말하기 위해서 안전하지 않은 코드를 이용할 수 있습니다. 이것의 단점이라면 우리가 고스란히 위험성은 떠안고 이를 사용해야 한다는 점입니다: 만일 안전하지 않은 코드를 부정확하게 사용한다면, 널 포인터 역참조와 같은 메모리 불안전성으로 인한 문제가 발생할 수 있습니다.

러스트가 안전하지 않은 또다른 자아를 갖고 있는 또 하나의 이유는 밑바탕이 되는 컴퓨터 하드웨어가 선천적으로 안전하지 않기 때문입니다. 만일 러스트가 안전하지 않은 연산을 허용하지 않았다면, 우리는 특정한 작업을 수행할 수 없었을 겁니다. 러스트는 우리가 저수준의 시스템 프로그래밍, 예를 들면 운영체제와 직접 상호작용을 하거나 심지어 우리만의 운영체제를 작성하는 등을 하는 것을 허용하고 싶어합니다. 저수준의 시스템 프로그래밍 작업은 이 언어의 목표 중 하나입니다. 안전하지 않은 러스트를 가지고 무엇을 할 수 있으며 또 어떻게 하는지에 대해서 탐구해봅시다.

안전하지 않은 슈퍼파워

안전하지 않은 러스트로 전환하기 위해서는 unsafe 키워드를 이용하며, 그 다음 안전하지 않은 코드를 감싸주는 새 블록을 시작합니다. 우리는 안전하지 않은 러스트 내에서 4개의 행동을 할 수 있는데, 이를 안전하지 않은 슈퍼파워라고 부르며, 안전한 러스트 내에서는 할 수 없는 것들입니다. 이 슈퍼파워들은 다음과 같은 것들을 하는 능력입니다:

  • 로우 포인터 (raw pointer) 를 역참조하기
  • 안전하지 않은 함수 혹은 메소드 호출하기
  • 가변 정적 변수 (mutable static variable) 의 접근 혹은 수정하기
  • 안전하지 않은 트레잇 구현하기

unsafe가 빌림 검사기 혹은 다른 어떤 러스트의 안전성 검사 기능을 끄는 게 아니라는 것을 이해하는 것은 중요합니다: 만일 여러분이 안전하지 않은 코드 내에서 참조자를 이용한다면, 이것은 여전히 검사될 것입니다. unsafe 키워드는 메모리 안전성을 위해 컴파일러에 의해 검사될 수 없는 위의 네가지 기능을 사용할 수 있는 능력만을 제공할 뿐입니다. 안전하지 않은 블록 내에서도 우리는 여전히 어느 정도의 안전성을 갖습니다.

더불어 unsafe는 블록 내의 코드가 필연적으로 위험하다던가 절대적으로 메모리 안전성 문제를 가지고 있음을 의미하는 것이 아닙니다: 그 의도는 unsafe 블록 내의 코드가 올바른 방법으로 메모리에 접근할 것임을 우리가 프로그래머로서 확실히 해두는 것입니다.

사람은 실수를 할 수 있고, 실수는 일어날 것이지만, 위의 네 가지 안전하지 않은 연산이 unsafe이라고 명시된 블록 내에 있도록 요구함으로써 우리는 메모리 안전성과 관련된 어떠한 에러라도 틀림없이 unsafe 블록 내에 있을 것임을 알게 될 것입니다. unsafe 블록을 작게 유지하세요; 후에 여러분이 메모리 버그를 찾아나갈 때 감사함을 느낄 것입니다.

안전하지 않은 코드를 최대한 격리하기 위해서는 안전하지 않은 코드를 안전한 추상화 내에 있도록 감싸서 안전한 API를 제공하는 것이 최상인데, 이는 우리가 이 장의 뒷편에서 안전하지 않은 함수와 메소드를 시험해 볼 때 다루겠습니다. 표준 라이브러리의 일부분은 검사가 수행된 안전하지 않은 코드 위에 안전한 추상화로 구현되어 있습니다. 안전한 추상화로 안전하지 않은 코드를 감싸는 것은 여러분 혹은 여러분의 사용자가 unsafe 코드로 구현된 기능을 이용하고자 하는 모든 장소에 unsafe라고 쓰는 것을 방지할 수 있는데, 안전한 추상화 코드를 사용하는 것은 안전하기 때문입니다.

네 가지 안전하지 않은 슈퍼파워 각각을 차례로 살펴봅시다: 또한 안전하지 않은 코드에 대한 안전한 인터페이스를 제공하는 몇몇 추상화도 살펴볼 것입니다.

로우 포인터를 역참조하기

4장의 “댕글링 참조자”절에서 우리는 참조자들이 언제나 유효함을 컴파일러가 보장한다고 언급했었습니다. 안전하지 않은 러스트는 로우 포인터 (raw pointer) 라고 불리는 참조자와 유사한 두가지 새로운 타입을 갖습니다. 참조자를 이용하는 것처럼 로우 포인터도 불변 혹은 가변이 될 수 있으며 각각 *const T*mut T라고 씁니다. 이 애스터리스크는 역참조 연산자가 아닙니다; 이것은 타입 이름의 일부입니다. 로우 포인터의 맥락 내에서 “불변”이란 해당 포인터가 역참조된 후에 직접 대입될 수 없음을 의미합니다.

참조자나 스마트 포인터와는 다르게, 아래와 같은 로우 포인터의 성질을 명심하세요:

  • 로우 포인터는 빌림 규칙 무시가 허용되어 불변 및 가변 포인터 양쪽 모두를 갖거나 같은 위치에 여러 개의 가변 포인터를 갖을 수 있습니다.
  • 로우 포인터는 유효한 메모리를 가리키고 있음을 보장하지 않습니다.
  • 로우 포인터는 널이 될 수 있습니다.
  • 로우 포인터는 자동 메모리 정리가 구현되어 있지 않습니다.

러스트가 이러한 보장을 강제하도록 하는 것으로부터 손을 떼도록 함으로써, 우리는 보장된 안전성을 포기하고, 개선된 성능이나 러스트의 보장이 적용되지 않는 타 언어 혹은 하드웨어와의 상호작용 능력을 얻는 기회비용을 얻을 수 있습니다.

Listing 19-1은 참조자로부터 불변 및 가변 로우 포인터를 만드는 방법을 보여줍니다.


# #![allow(unused_variables)]
#fn main() {
let mut num = 5;

let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
#}

Listing 19-1: 참조자로부터 로우 포인터 생성하기

이 코드에서 unsafe 키워드를 포함하지 않았음을 주목하세요. 우리는 로우 포인터를 안전한 코드 내에서 생성할 수 있습니다; 여러분이 잠시 후에 보게될 것처럼, 우리는 그저 안전하지 않은 블록 밖에서는 로우 포인터를 역참조할 수 없을 뿐입니다.

우리는 불변 및 가변 참조자를 관련된 로우 포인터 타입으로 캐스팅하기 위해 as를 사용함으로써 로우 포인터를 생성하였습니다. 우리가 유효성이 보장된 참조자로부터 직접 이것들을 만들었기 때문에, 우리는 이 특정한 로우 포인터가 유효함을 알지만, 임의의 로우 포인터에 대해서는 이러한 가정을 내릴 수 없습니다.

다음으로, 우리가 유효성을 특정할 수 없는 로우 포인터를 만들어 보겠습니다. Listing 19-2는 메모리 내에 임의의 위치를 가리키는 로우 포인터를 만드는 방법을 보여줍니다. 임의의 메모리를 사용 시도하는 것은 정의되어 있지 않습니다: 해당 주소에 데이터가 있을 수도 있고 없을 수도 있으며, 컴파일러가 코드를 최적화해서 메모리 접근이 없을 수도, 혹은 프로그램이 세그먼테이션 폴트 (segmentation fault) 에러를 일으킬지도 모릅니다. 보통은 이러한 코드를 작성할 어떠한 좋은 이유도 없지만, 가능은 합니다:


# #![allow(unused_variables)]
#fn main() {
let address = 0x012345usize;
let r = address as *const i32;
#}

Listing 19-2: 임의의 메모리 주소를 가리키는 로우 포인터 생성하기

우리가 안전한 코드 내에서 로우 포인터를 생성할 수는 있지만, 로우 포인터를 역참조하여 해당 포인터가 가리키고 있는 데이터를 읽지는 못함을 상기하세요. Listing 19-3에서는 unsafe 블록을 필요로 하는 로우 포인터에 상에서의 역참조 연산자 *를 사용합니다.


# #![allow(unused_variables)]
#fn main() {
let mut num = 5;

let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;

unsafe {
    println!("r1 is: {}", *r1);
    println!("r2 is: {}", *r2);
}
#}

Listing 19-3: unsafe 블록 내에서 로우 포인터 역참조하기

포인터를 생성하는 것은 어떠한 해도 끼지지 않습니다; 문제는 우리가 이 포인터가 가리키는 값에 접근을 시도하여 유효하지 않은 값을 다루는 상황에 처할지도 모를 때입니다.

또한 Listing 19-1과 19-3에서 우리가 num이 저장되어 있는 동일한 메모리 장소를 가리키고 있는 *const i32*mut i32 로우 포인터를 생성했음을 주목하세요. 만일 우리가 대신 num에 대한 불변 및 가변 참조자를 생성 시도했다면, 러스트의 소유권 규칙이 가변 참조자 와 불변 참조자를 동시에 허용하지 않기 때문에 코드는 컴파일 되지 않을 것입니다. 로우 포인터를 이용하면, 우리는 동일한 위치를 가리키는 가변 포인터 및 불변 포인터를 만들 수 있고, 가변 포인터를 통해 데이터를 바꿀수 있는데, 이는 데이터 레이스를 야기할 가능성이 있습니다. 조심하세요!

이러한 모든 위험을 가지고, 왜 우리는 로우 포인터를 사용하게 될까요? 한가지 주요 사용례는 여러분이 다음 절에 “안전하지 않은 함수 혹은 메소드 호출하기”에서 보실 것과 같이, C 코드와의 상호작용을 할 때입니다. 또다른 경우는 빌림 검사기가 이해하지 못하는 안전한 추상화를 만들 때입니다. 우리는 안전하지 않은 함수를 소개한 다음 안전하지 않은 코드를 사용하는 안전한 주상화의 예를 살펴보겠습니다.

안전하지 않은 함수 혹은 메소드 호출하기

안전하지 않은 블록을 필요로하는 연산의 두번째 타입은 안전하지 않은 함수의 호출입니다. 안전하지 않은 함수와 메소드는 보통의 함수와 메소드와 똑같이 생겼지만, 함수 정의의 앞부분에 추가적으로 unsafe가 붙어있습니다. 이 맥락 내에서의 unsafe 키워드는 우리가 이 함수를 호출할 때 우리가 유지시키고 싶어하는 요구사항을 가지고 있음을 나타내는데, 이는 우리가 이러한 요구사항을 만족시키는지를 러스트가 보장할 수 없기 때문입니다. unsafe 블록 내에서 안전하지 않은 함수를 호출함으로써, 우리가 이 함수의 문서를 읽었고 함수의 계약서를 준수할 책임을 가지고 있다고 말하는 것입니다.

아래는 본체에서 아무것도 하지 않는 dangerous라는 이름의 안전하지 않은 함수입니다:


# #![allow(unused_variables)]
#fn main() {
unsafe fn dangerous() {}

unsafe {
    dangerous();
}
#}

우리는 반드시 분리된 unsafe 블록 내에서 dangerous를 호출해야 합니다. 만일 unsafe 블록 없이 danugerous의 호출을 시도하면, 다음과 같은 에러를 얻게 됩니다:

error[E0133]: call to unsafe function requires unsafe function or block
 -->
  |
4 |     dangerous();
  |     ^^^^^^^^^^^ call to unsafe function

우리의 dangerous 호출 주변에 unsafe 블록을 집어넣음으로서, 우리는 이 함수의 문서를 읽었고, 이를 어떻게 적절히 이용하는지 이해했으며, 이 함수의 개약서에 서명하는 것임을 확인했음을 러스트에게 단언하는 중입니다.

안전하지 않은 함수의 본체는 사실상 unsafe 블록이므로, 안전하지 않은 함수 내에서 다른 안전하지 않은 연산을 수행하기 위해서 별도의 unsafe 블록을 추가할 필요는 없습니다.

안전하지 않은 코드 상에 안전한 추상화 생성하기

어떤 함수가 단지 안전하지 않은 코드를 담고 있다는 것이 함수 전체를 안전하지 않은 것으로 표시할 필요가 있음을 뜻하지는 않습니다. 사실, 안전한 함수 내에 안전하지 않은 코드를 감싸는 것은 일반적인 추상화입니다. 한가지 예로, 표준 라이브러리가 제공하는 함수 split_at_mut를 공부해봅시다. 이 함수는 몇몇 안전하지 않은 코드를 필요로 하고 우리가 어떻게 구현할 수 있을지 탐구해볼만 합니다. 이 안전한 메소드는 가변 슬라이스 상에서 정의됩니다: 이것은 하나의 슬라이스를 취해서 인자로 주어진 인덱스에서 슬라이스를 쪼개서 둘로 만들어줍니다. Listing 19-4는 split_at_mut를 사용하는 방법을 보여줍니다.


# #![allow(unused_variables)]
#fn main() {
let mut v = vec![1, 2, 3, 4, 5, 6];

let r = &mut v[..];

let (a, b) = r.split_at_mut(3);

assert_eq!(a, &mut [1, 2, 3]);
assert_eq!(b, &mut [4, 5, 6]);
#}

Listing 19-4: 안전한 split_at_mut 함수의 사용

안전한 러스트만 사용해서는 이 함수를 구현할 수 없습니다. 그 시도는 Listing 19-5와 같은 형태처럼 되겠으나, 컴파일되지 않을 것입니다. 단순하게 하기 위해서, 우리는 split_at_mut를 메소드가 아닌 함수로서 구현하고 제네릭 타입 T의 슬라이스를 위한 것보다는 i32 값의 슬라이스를 위한 것으로 구현하겠습니다.

fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = slice.len();

    assert!(mid <= len);

    (&mut slice[..mid],
     &mut slice[mid..])
}

Listing 19-5: 안전한 러스트만 사용하여 split_at_mut를 구현하는 시도

이 함수는 먼저 슬라이스의 총 길이를 얻은 다음, 매개변수로 주어진 인덱스가 총 길이보다 작거나 같음을 검사함으로서 슬라이스 내에 있음을 단언(assert)합니다. 이 단언은 우리가 넘긴 인덱스가 슬라이스를 쪼개기 위한 인덱스보다 클 경우, 이 함수가 그 인덱스의 사용 시도를 하기 전에 패닉을 일으킬 것임을 의미합니다.

그 다음 우리는 두 개의 가변 슬라이스를 튜플 안에 넣어 반환합니다: 하나는 원본 슬라이스의 시작부터 mid 인덱스까지이고 다른 하나는 mid부터 원본 슬라이스의 끝까지입니다.

Listing 19-5의 코드의 컴파일을 시도하면, 다음과 같은 에러를 얻습니다:

error[E0499]: cannot borrow `*slice` as mutable more than once at a time
 -->
  |
6 |     (&mut slice[..mid],
  |           ----- first mutable borrow occurs here
7 |      &mut slice[mid..])
  |           ^^^^^ second mutable borrow occurs here
8 | }
  | - first borrow ends here

러스트의 빌림 검사기는 우리가 슬라이스의 서로 다른 부분을 빌리는 중임을 이해할 수 없습니다; 러스트는 우리가 같은 슬라이스로부터 두번 빌리는 중인것만을 알고 있습니다. 슬라이스의 서로 다른 부분을 빌리는 것은 이 두 슬라이스가 서로 겹치지 않기 때문에 근본적으로 괜찮지만, 러스트는 이를 알 정도로 똑똑하진 않습니다. 우리가 이 코드가 괜찮은 것임을 알지만 러스트는 그렇지 못하므로, 안전하지 않은 코드를 이용할 시간입니다.

Listing 19-6은 split_at_mut의 구현체가 동작하도록 만들기 위해서 unsafe 블록, 로우 포인터, 그리고 몇몇 안전하지 않은 함수의 호출을 사용하는 방법을 보여줍니다.


# #![allow(unused_variables)]
#fn main() {
use std::slice;

fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = slice.len();
    let ptr = slice.as_mut_ptr();

    assert!(mid <= len);

    unsafe {
        (slice::from_raw_parts_mut(ptr, mid),
         slice::from_raw_parts_mut(ptr.offset(mid as isize), len - mid))
    }
}
#}

Listing 19-6: split_at_mut 함수의 구현체 내에서 안전하지 않은 코드 사용하기

4장의 “슬라이스 타입”절에서 슬라이스는 어떤 데이터를 가리키는 포인터와 슬라이스의 길이로 되어있음을 상기하세요. 우리는 len 메소드를 사용하여 슬라이스의 길이를 얻고 as_mut_ptr 메소드를 사용하여 슬라이스의 로우 포인터에 접근합니다. 위의 경우, 우리가 i32 값들의 가변 슬라이스를 갖고 있으므로, as_mut_ptr*mut i32 타입을 갖는 로우 포인터를 반환하는데, 이는 ptr 변수에 저장됩니다.

mid 인덱스가 슬라이스 내에 있다는 단어는 유지합니다. 그 다음 안전하지 않은 코드에 왔습니다: slice::from_raw_parts_mut 함수는 로우 포인터와 길이를 받아서 슬라이스를 생성합니다. 이 함수를 이용하여 ptr로 시작하고 mid 만큼의 아이템을 가진 슬라이스를 생성합니다. 그다음 우리는 ptr 상에서 offset 메소드를 인자 mid와 함꼐 호출하여 mid에서부터 시작하는 로우 포인터를 얻고, 이 포인터와 mid 뒤에 남은 아이템의 개수를 길이로 하는 슬라이스를 생성합니다.

함수 slice::from_raw_parts_mut는 로우 포인터를 인자로 사용하고 이 포인터가 유효함을 반드시 믿어야 하므로 안전하지 않습니다. 로우 포인터의 offset 메소드 또한 안전하지 않은데, 그 이유는 오프셋 위치 또한 유효한 포인터임을 반드시 믿어야 하기 때문입니다. 따라서, 이들을 호출할 수 있도록 하기 위해 우리의 slice::from_raw_parts_mutoffset 호출 주변에 unsafe 블록을 넣어야 했습니다. 이 코드를 살펴보고 mid가 반드시 len보다 작거나 같다는 단언을 추가함으로써, 우리는 unsafe 블록 내에서 사용된 모든 로우 포인터들이 슬라이스 내의 데이터를 가리키는 유요한 포인터가 될 것입을 말할 수 있습니다. 이는 받아들일만 하고 unsafe의 적절한 사용입니다.

결과적으로 나온 split_at_mut 함수를 unsafe로 표시할 필요가 없으며, 이 코드를 안전한 러스트로부터 호출할 수 있음을 주목하세요. 우리는 unsafe 코드를 안전한 방법으로 사용하는 함수의 구현체를 가지고 안전하지 않은 코드에 대한 안전한 추상화를 만들었는데, 이는 이 함수가 접근하는 데이터로부터 오직 유효한 포인터만을 생성하기 때문입니다.

반면, Listing 19-7의 slice::from_raw_parts_mut 사용은 슬라이스에 사용될 때 크래시를 일으키기 쉽습니다. 이 코드는 임의의 메모리 위치를 얻어서 만개의 아이템 길이를 갖는 슬라이스를 생성합니다:


# #![allow(unused_variables)]
#fn main() {
use std::slice;

let address = 0x012345usize;
let r = address as *mut i32;

let slice = unsafe {
    slice::from_raw_parts_mut(r, 10000)
};
#}

Listing 19-7: 임의의 메모리 위치로부터 슬라이스 생성하기

우리는 이 임의의 위치에서 메모리를 소유하지 않았으며, 이 코드가 만들어낸 슬라이스가 유효한 i32 값들을 담고 있음에 대한 보장은 없습니다. slice를 마치 유효한 슬라이스인 것처럼 사용하는 시도는 정의하지 않은 동작 (undefined behaviour) 을 야기합니다.

extern 함수를 사용하여 외부 코드 호출하기

가끔, 여러분의 러스트 코드는 다른 언어로 작성된 코드와 상호작용하고 싶어할지도 모릅니다. 이를 위해서 러스트는 외국 함수 인터페이스 (Foreign Function Interface, FFI) 의 생성과 사용을 가능케 하는 extern 키워드를 가지고 있습니다. FFI는 프로그래밍 언어가 함수를 정의하고 다른 (외국의) 프로그래밍 언어가 해당 함수를 호출 가능하게 하는 방법입니다.

Listing 19-8은 C 표준 라이브러리의 abs 함수와의 통합을 설정하는 방법을 보여줍니다. extern 블록 내에 선언된 함수는 언제나 러스트 코드로부터 호출하기에 안전하지 않습니다. 그 이유는 타 언어들이 러스트의 규칙과 보장들을 강제하지 않으며, 러스트가 이들을 검사할 수도 없으므로, 따라서 안전성을 보장하기 위한 책임은 프로그래머에게 떨어집니다.

Filename: src/main.rs

extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    unsafe {
        println!("Absolute value of -3 according to C: {}", abs(-3));
    }
}

Listing 19-8: 다른 언어에 정의된 extern 함수의 선언 및 호출

extern "C" 블록 내에서, 우리가 호출하고 싶은 다른 언어로부터 온 외부 함수의 이름과 시그니처를 나열합니다. "C" 부분은 해당 외부 함수가 어떤 ABI (application binary interface) 를 사용하는지를 정의합니다: API는 어셈플리 수준에서 함수를 어떻게 호출하는지를 정의합니다. "C" ABI는 가장 흔하며 C 프로그래밍 언어의 ABI를 준수합니다.

다른 언어로부터 러스트 함수 호출하기

우리는 또한 extern을 사용하여 다른 언어들이 러스트 함수를 호출할 수 있도록 하는 인터페이스를 만들 수 있습니다. extern 블록 대신, fn 키워드 전에 extern 키워드를 추가하고 사용할 ABI를 명시합니다. 우리는 또한 #[no_mangle] 어노테이션을 추가하여 러스트 컴파일러가 이 함수의 이름을 맹글링하지 않도록 할 필요가 있습니다. 맹글링 (mangling) 이란 우리가 함수에게 준 이름을 컴파일 과정의 다른 부분에서 사용하기 위한 더 많은 정보를 담고 있지만 사람이 읽기엔 별로 안좋은 이름으로 컴파일러가 바꾸는 과정입니다. 모든 프로그래밍 언어 컴파일러가 약간씩 다르게 이름을 맹글링하므로, 러스트 함수가 다른 언어에 의해 이름을 불릴 수 있도록 하기 위해, 우리는 반드시 러스트 컴파일러의 이름 맹글링 기능을 꺼야 합니다.

아래의 예제에서, 우리는 call_from_c를 공유 라이브러리로 컴파일하고 C로 링크한 다음, 이 함수를 C 코드에서 접근 가능하게 만들었습니다:


# #![allow(unused_variables)]
#fn main() {
#[no_mangle]
pub extern "C" fn call_from_c() {
    println!("Just called a Rust function from C!");
}
#}

이러한 extern의 사용에는 unsafe가 필요 없습니다.

가변 정적 변수의 접근 혹은 수정하기

지금까지 우리는 전역 변수 (global variable) 에 대하여 이야기한 적이 없는데, 이는 러스트가 지원하기는 하지만 러스트의 소유권 규칙에 문제를 일으킬 수 있습니다. 만일 두 스레드가 동일한 가변 전역 변수에 접근하는 중이라면, 이는 데이터 레이스를 야기할 수 있습니다.

러스트에서 전역 변수는 정적 (static) 변수라고 불립니다. Listing 19-9는 스트링 슬라이스를 값으로 갖는 정적 변수의 정의 및 사용의 예를 보여줍니다.

Filename: src/main.rs

static HELLO_WORLD: &str = "Hello, world!";

fn main() {
    println!("name is: {}", HELLO_WORLD);
}

Listing 19-9: 불변 정적 변수의 정의 및 사용

정적 변수는 상수와 유사한데, 이는 우리가 3장의 “변수와 상수의 차이점” 절에서 논의했었습니다. 정적 변수의 이름은 관례에 따라 SCREAMING_SNAKE_CASE 형식을 따르며, 우리는 반드시 변수의 타입을 명시해야 하는데, 위의 예제에서는 &'static str입니다. 정적 변수는 'static 라이프타임을 갖는 참조자만을 저장할 수 있는데, 이는 러스트 컴파일러가 라이프 타임을 알아낼 수 있음을 의미합니다; 우리는 이를 명시적으로 작성할 필요가 없습니다. 불변 정적 변수에의 접근은 안전합니다.

상수와 불변 정적 변수는 비슷해 보일지도 모르겠으나, 정적 변수의 값이 메모리 내의 고정된 주소값을 갖는다는 점에서 미묘한 차이점이 있습니다. 값을 사용하면 언제나 동일한 데이터에 접근하게 될 것입니다. 반면 상수는 사용될 때마다 데이터가 복사되는 것이 허용됩니다.

상수와 정적 변수 간의 또다른 차이점은 정적 변수가 가변일 수 있다는 점입니다. 가변 정적 변수에 접근하고 수정하는 것은 안전하지 않습니다. Listing 19-10는 COUNTER라는 이름의 가변 정적 변수를 선언하고, 접근하고, 수정하는 방법을 보여줍니다.

Filename: src/main.rs

static mut COUNTER: u32 = 0;

fn add_to_count(inc: u32) {
    unsafe {
        COUNTER += inc;
    }
}

fn main() {
    add_to_count(3);

    unsafe {
        println!("COUNTER: {}", COUNTER);
    }
}

Listing 19-10: 가변 정적 변수를 읽거나 쓰는 것은 안전하지 않습니다

보통의 변수때처럼, 우리는 mut 키워드를 사용하여 가변성을 명시합니다. COUNTER를 읽거나 쓰는 어떠한 코드라도 unsafe 블록 내에 있어야 합니다. 이 코드는 컴파일 되고 우리가 기대한 바와 같이 COUNTER: 3을 출력하는데, 그 이유는 이 프로그램이 단일 스레드이기 때문입니다. 여러 수래드가 COUNTER에 접근하도록 하는 것은 데이터 레이스를 일으키기 쉽습니다.

전역적으로 접근 가능한 가변 데이터를 이용하는 것은 데이터 레이서가 없음을 확신하기 힘들게 만드는데, 이것이 러스트가 가변 정적 변수를 안전하지 않은 것으로 간주하는 이유입니다. 가능하다면 우리가 16장에서 논의했던 동시성 기술과 스레드-안전한 스마트 포인터를 이용하여, 컴파일러가 서로 다른 스레드로부터 접근되는 데이터가 안전하게 사용됨을 검사하도록 하는 편이 좋습니다.

안전하지 않은 트레잇 구현하기

unsafe에서만 동작하는 마지막 기능은 안전하지 않은 트레잇 구현하기 입니다. 트레잇은 적어도 메소드 중 하나가 컴파일러가 검사할 수 없는 몇몇 불변성 (invariant) 을 갖고 있을 때 안전하지 않게 됩니다. 우리는 trait 전에 unsafe를 추가함으로써 어떤 트레잇이 unsafe함을 선언할 수 있습니다; 그 다음 트레잇의 구현체 또한 Listing 19-11에서 보는 바와 같이 unsafe로 표시되어야 합니다.


# #![allow(unused_variables)]
#fn main() {
unsafe trait Foo {
    // methods go here
}

unsafe impl Foo for i32 {
    // method implementations go here
}
#}

Listing 19-11: 안전하지 않은 트레잇의 정의 및 구현

unsafe impl을 이용함으로써 우리는 컴파일러가 검증할 수 없는 불변성을 우리가 유지할 것임을 약속하고 있습니다.

한 가지 예로서, 16장의 “SyncSend 트레잇을 이용한 확장 가능한 동시성” 절에서 논했던 SyncSend 마커 트레잇을 상기해보세요: 우리의 타입이 전체적으로 Send되고 Sync한 타입으로 구성되어 있다면 컴파일러는 이 트레잇을 자동적으로 구현합니다. 만일 우리가 로우 포인터와 같이 Send되지 않거나 Sync하지 않은 타입을 포함한 타입을 구현하고, 이 타입을 Send되거나 Sync한 것으로 표시하고 싶다면, 우리는 unsafe를 이용해야 합니다. 러스트는 우리의 타입이 스레드 사이로 안전하게 보내지거나 여러 스레드로부터 안전하게 접근되는 것에 대한 보장을 유지하는 것을 검사할 수 없습니다; 따라서, 우리는 손수 이를 검사하고 unsafe를 이용하여 이러한 사항을 나타낼 필요가 있습니다.

언제 안전하지 않은 코드를 이용할까요?

방금까지 논했던 네 가지 행동 (슈퍼파워) 을 얻기 위해 unsafe를 사용하는 것은 잘못된 것도 아니고, 심지어 눈살을 찌푸릴 일도 아닙니다. 하지만 unsafe 코드를 올바르게 이용하는 것은 좀 더 힘든데 그 이유는 컴파일러가 메모리 안전성응 유지하는데 도움을 줄 수 없기 때문입니다. 여러분이 unsafe 코드를 사용할 이유를 갖게 될 때, 여러분은 그렇게 할 수 있고, 명시적인 unsafe 어노테이션을 갖는 것이 문제가 일어났을 때 그 근원을 추적해 나가는 것을 더 수월하게 만들어 줍니다.