[Rust] _는 bind하지 않는다

RustRAII idiom을 사용하는 언어로, 객체가 소멸하는 시점에 따라 코드의 의미가 달라진다. 예를 들어 아래 코드를 보자.

이 코드는 Service의 객체를 생성하고 종료하기를 기다리는 코드다. 이 코드는 문법적으로는 아무 문제가 없다. 하지만 종료할 때까지 Service가 어떤 동작을 수행하기를 원했다면 이는 틀린 코드다. Service 객체는 아무 변수에도 bind 되지 않았기 때문에 이 객체는 두 번째 줄에 있는 문장이 끝나면 소멸한다.

Service 객체가 wait_for_exit이 수행될 때까지 살아있기를 원한다면, 아래와 같이 변경해야 한다. 위의 코드에서 Service의 객체는 변수 service에 bind 된다. 따라서 두 번째 라인이 끝나도 소멸하지 않고 wait_for_exit이 종료되는 것을 기다리고, run 함수가 종료되면서 stack이 unwind 될 때 소멸한다.

하지만 위의 코드를 컴파일하면 server가 unused variable이라는 경고가 보일 것이다. Rust 컴파일러는 선언된 변수가 사용되지 않으면 경고를 내기 때문이다. 그렇다면 사용하지 않는 변수의 소유권만 가지고 있고 싶을 때는 어떻게 해야 할까? 이 경우 _(underscore)로 시작하는 변수 이름을 사용하면 된다. Rust 컴파일러는 _로 시작하는 변수를 특별 취급하기 때문에, _로 시작하는 변수는 사용하지 않아도 컴파일러 경고가 나지 않는다.

그렇다면 사용하지 않는 변수에 아무런 이름을 주지 않으면 어떻게 될까? 어차피 사용하는 것이 목적이 아니고, 객체를 bind만 해서 가지고 있는 것이 목적이라면 아래 코드처럼 아무 이름 없이 _를 변수로 사용해도 되지 않을까?

아쉽게도 위 코드는 예상대로 동작하지 않는다. 이는 _가 객체의 소유권을 가지지 않기 때문이다. 이를 Rust의 용어로는 _는 value를 bind 하지 않는다고 말한다. 즉, 위의 코드는 Service 객체를 생성하고 소멸시킨 뒤 wait_for_exit 함수를 실행하는 service_run1.rs와 같은 코드다.

사실 이는 처음부터 의도된 동작은 아니었다. 2013년에 _에 넣은 값이 바로 drop 된다는 것이 버그로 보고됐고, _를 일반적인 변수와 같이 bind 하도록 고치자는 제안도 있었다. 사실, bind 하는 것이 더 직관적이고 일반적인 접근 방법이다. 하지만, Rust 개발자들은 _의 의미를 bind 하지 않는 것으로 스펙에 규정했다. 이는 어떤 값을 받아서 bind 하지 않는 방법이 기존에 존재하지 않았기 때문이다.

값을 bind 하지 않는 _의 특성은 다음과 같은 경우에 유용하게 사용된다. 위 코드는 어떤 tuple을 받아서 Option<A>를 먼저 사용하고, 그 뒤 B를 사용한다. 만약 _가 값을 bind 하였다면, 네 번째 줄과 다섯 번째 줄에서 B가 move 되었을 것이다. 따라서 여덟 번째 줄은 이미 move 된 값을 사용하였다는 경고와 함께 컴파일되지 않았을 것이다. 하지만 _는 값을 bind 하지 않기 때문에 위의 코드는 성공적으로 컴파일된다.

여기서 _로 받은 값을 statement가 끝난 뒤 drop 하는 것이 아닌 값을 bind 하지 않는 것으로 정의했다는 것이 중요하다. 위의 service_run4.rs 예제에서 Service 객체가 drop 된 것은 temporary value를 bind 하지 않았기 때문이지 _가 값을 drop 시키기 때문이 아니다. 만약 _가 값을 drop 한다면, non_bind.rs 예제에서 match 문이 끝난 뒤 B가 drop 되기 때문에 여덟 번째 줄에서 B를 사용할 수 없었을 것이다.

개인적으로 이것 때문에 발생한 버그가 Rust를 사용하면서 겪었던 버그 중 해결에 가장 오랜 시간이 걸린 버그였다. 현재 개발 중인 CodeChain은 멀티스레드 프로그램이기 때문에 main thread에서 여러 개의 서비스를 생성하고 각 서비스는 종료될 때까지 자신이 해야 할 일을 한다. 이때 대부분 서비스는 자기 완결적인 일을 하므로 외부에서 사용하지 않고 객체를 유지하기만 하면 된다. 따라서 변수의 이름을 _로 사용해도, 컴파일러 입장에서는 틀린 코드가 아니기 때문에 성공적으로 컴파일된다. 게다가 다른 언어에서는 _를 변수로 사용하는 것이 아무 문제 없기 때문에 코드를 읽어도 의심 없이 넘어갔었다.

이런 부류의 버그는 모르고 보면 잡기 어려운 문제지만, 알면 매우 쉬운 문제다. 하지만 Rust 공식 문서인 “The Rust Programming Language”에서도 “However, using the underscore by itself doesn’t ever bind to the value. Listing 18-22 will compile without any errors because s doesn’t get moved into _.”라고 단 두 줄 나오고 넘어가기 때문에 Rust의 _는 값을 bind 하지 않는다는 것을 모르기 쉽다. 그래서 앞으로 Rust를 쓰는 사람이 같은 버그를 만나지 않았으면 하는 마음으로 이 글을 쓴다.

댓글

  1. 와 이런건.. 정말 모르면 아프게 삽뜰듯..

    답글삭제
  2. 굉장히 유용한 글입니다. 모르고 개발하면 굉장히 힘든 시간을 보낼 것 같은 내용이네요.

    답글삭제
  3. 이걸로 고생좀 하고 있었는데... 블로그 보니 이해가 잘되는군요. 감사합니다!

    답글삭제

댓글 쓰기

이 블로그의 인기 게시물

USB 2.0 케이블의 내부 구조

[C++] enum class - 안전하고 쓰기 쉬운 enum

Log Aggregator 비교 - Scribe, Flume, Fluentd, logstash

[Web] SpeechSynthesis - TTS API

[Python] cache 데코레이터로 최적화하기