컨텐츠 바로가기

05.18 (토)

“입문자부터 숙련자까지” 주의해야 할 러스트 프로그래밍 실수 6가지

댓글 첫 댓글을 작성해보세요
주소복사가 완료되었습니다
러스트는 프로그래머에게 가비지 수집 없이, 머신 네이티브 속도로 실행되는 메모리 안전 소프트웨어를 작성할 수 있는 방법을 제공한다. 그러나 마스터하기 복잡한 언어이기도 하고, 초기 학습 난이도도 상당히 높다. 러스트에 입문하는 개발자는 물론 숙련된 러스트 개발자도 숙지해야 하는 6가지 주의 사항을 살펴보자.
ITWorld

ⓒ Getty Images Bank

<이미지를 클릭하시면 크게 보실 수 있습니다>



러스트 코드 작성 시 알아야 할 6가지 주의 사항

  • 빌림 검사기를 끌 수 없음
  • 바인딩하려는 변수에 '_'를 사용하지 말 것
  • 클로저의 수명 규칙은 함수와 다름
  • 빌림이 만료될 때 항상 소멸자가 실행되는 것은 아님
  • 안전하지 않은 것과 한계 없는 수명에 주의
  • .unwrap()은 오류 처리 제어를 포기함


빌림 검사기를 끌 수 없음

소유권, 빌림, 수명은 러스트에 내장된 요소로, 러스트 언어가 가비지 수집 없이 메모리 안전을 유지하는 방법에 있어 필수적인 부분이다.

몇몇 다른 언어는 안전 또는 메모리 문제를 개발자에게 알리되 코드 컴파일은 그대로 허용하는 코드 검사 툴을 제공하지만 러스트의 작동 방식은 다르다. 러스트 컴파일러의 일부분으로 모든 소유권 연산이 유효한지 확인하는 빌림 검사기(borrow checker)는 끌 수 있는 선택적 유틸리티가 아니다. 빌림 검사기에서 유효한 것으로 확인되지 않은 코드는 무슨 일이 있어도 컴파일되지 않는다.

빌림 검사기를 붙잡고 씨름하지 않는 방법에 대한 내용으로만 따로 기사 하나를 써도 될 정도다. 예제로 보는 러스트(Rust by Example)의 범위 섹션에서 이 규칙이 많은 일반적인 동작에서 어떻게 작용하는지 읽어볼 것을 권한다.

러스트를 처음 시작하는 단계에서는 언제든 .clone()을 사용해 복사본을 만드는 방법으로 소유권 문제를 피할 수 있음을 기억하라. 프로그램에서 성능 집약적인 부분이 아니라면 복사본을 만든다 해도 측정될 정도의 영향은 거의 없다. 이렇게 하고, 최대한의 성능이 실제로 필요한 부분에 집중하면서 프로그램의 이런 부분에서 빌림과 수명의 효율성을 더 높일 방법을 알아낼 수 있다.


바인딩하려는 변수에 '_'를 사용하지 말 것

_(밑줄 하나) 변수 이름은 러스트에서 특수하게 동작한다. 이 이름은 변수가 받는 값이 변수에 바인딩되지 않음을 의미하므로 일반적으로 즉시 폐기될 값을 받는 용도로 사용된다. 예를 들어, must_use 경고를 띄우는 항목이 있을 때 이 경고가 나오지 않도록 하기 위해 흔히 _을 할당한다.

값이 사용되는 문보다 더 오래 지속되는 값에는 밑줄을 사용하면 안 된다. 범위(scope)가 아니라 문(statement)에 관한 이야기임에 유의하라.

주의해야 할 시나리오는 범위를 벗어날 때까지 무언가를 유지하고자 하는 경우다. 다음과 같은 코드 블록을 보자.
let _ = String::from(" Hello World ").trim();

생성된 문자열은 문 이후 즉시 범위를 벗어나게 된다. 즉, 블록 끝까지 유지되지 않는다(메서드 호출은 컴파일에서 결과가 생략되지 않도록 하기 위한 것).

이 함정을 피하는 쉬운 방법은 범위의 끝까지 유지하되, 그 외에는 그다지 사용할 계획이 없는 할당에 대해 _user 또는 _item과 같은 이름만 사용하는 것이다.


클로저의 수명 규칙은 함수와 다름

이 함수를 보자.
fn function(x: &i32) -> &i32 {
x
}

함수의 반환 값에 대해 이 함수를 클로저로 표현하려고 시도할 수 있다.

fn main() {
let closure = |x: &i32| x;
}

문제는 이렇게 하면 작동하지 않는다는 것이다. 클로저의 입력과 출력 수명이 다르기 때문에 컴파일러에서 lifetime may not live long enough 오류가 발생한다.

이 문제를 피하는 방법 중 하나는 다음과 같이 정적 참조를 사용하는 것이다.

fn main() {
let _closure: &dyn Fn(&i32) -> &i32 = &|x: &i32| x;
}

별도의 함수를 사용하면 더 장황해지지만 이와 같은 문제는 피할 수 있다. 범위가 더 명확해지고 시각적으로 파싱하기가 더 쉬워진다. (참고 : 이 섹션의 예제는 러스트 수명에 대한 일반적인 오해에 관한 깃허브의 유용한 참조 자료를 수정한 것이다.)


빌림이 만료될 때 항상 소멸자가 실행되는 것은 아님

러스트에서도 C++와 마찬가지로 형식에 대한 소멸자를 생성할 수 있으며 소멸자는 객체가 범위를 벗어날 때 실행될 수 있다. 그러나 실행이 보장되는 것은 아니다.

빌림이 특정 객체에 대해 만료될 때 역시 마찬가지다. 뭔가에 대해 빌림이 만료된다고 해서 소멸자가 항상 실행되지는 않는다. 사실 단지 빌림이 만료되었다고 해서 소멸자가 실행되는 것을 프로그래머가 원하지 않는 경우도 있다(예를 들어 뭔가에 대한 포인터를 유지하고 있는 경우).

러스트 문서에는 소멸자 실행을 보장하는 방법, 그리고 소멸자 실행이 보장되는 경우를 아는 방법에 대한 가이드라인이 있다.


안전하지 않은 것과 한계 없는 수명에 주의

unsafe라는 키워드는 원시 포인터 역참조와 같은 동작을 수행할 수 있는 러스트 코드에 태그를 붙이기 위해 존재하는 키워드다. 러스트에서 자주 할 필요가 없는 종류의 작업이지만(하지 않는 것이 좋음!) 꼭 해야 하는 경우 많은 잠재적 문제가 따라온다.

예를 들어, unsafe 연산에 의해 생성된 원시 포인터 역참조는 한계가 없는 수명으로 이어진다. 안전하지 않은 러스트에 관한 책인 러스토노미콘(Rustonomicon)은 한계 없는 수명이 "컨텍스트가 요구하는 만큼 커진다"라고 경고한다. 즉, 원래 필요했거나 의도했던 것 이상으로, 예상치 못하게 커질 수 있다.

한계 없는 참조를 사용해서 수행되는 작업을 세심하게 살핀다면 문제는 없을 것이다. 그러나 안전을 위해서는 함수 범위 내에서 멋대로 움직이도록 두는 것보다는 역참조된 포인터를 함수에 넣고 함수 경계에서 수명을 사용하는 편이 더 좋다.


.unwrap()은 오류 처리 제어를 포기함

연산이 Result를 반환할 때마다 이를 처리하는 두 가지 기본적인 방법이 있다. 하나는 .unwrap() 또는 그 사촌 중 하나(예를 들어 .unwrap_or())를 사용하는 방법, 다른 하나는 완전한 match 문을 사용해서 Err 결과를 처리하는 방법이다.

.unwrap()의 큰 장점은 편리함이다. 현재 코드 경로에 오류 조건이 발생할 일이 없거나, 오류 조건이 발생해도 어차피 처리하는 것이 불가능한 경우 .unwrap()을 사용해서 필요한 값을 얻고 할 일을 계속 하면 된다.

이 편리함에는 대가가 따른다. 모든 오류 조건은 패닉(panic)을 일으키고 프로그램을 중단시킨다. 러스트의 패닉은 복구할 수 없는데, 패닉은 프로그램의 실제 버그를 가리킬 만큼 잘못된 부분이 있다는 신호이기 때문이다.

.unwrap() 또는.unwrap()의 변형 중 하나인 .unwrap_or() 등을 사용하는 경우 제한적인 오류 처리 역량만 갖게 된다는 점에 유의해야 한다. OK 값이 생성할 만한 형식에 부합하는 종류의 값을 전달해야 한다. match를 사용하는 경우 단순히 적절한 형식을 생산하는 것보다 훨씬 더 높은 동작 유연성을 확보할 수 있다.

주어진 프로그램 경로에서 그러한 유연성이 필요할 일이 없다고 판단된다면 .unwrap()도 괜찮다. 그렇다 해도, 먼저 전체 match 문을 작성해서 처리 작업에서 간과한 측면이 있는지 확인해볼 것을 권한다.
editor@itworld.co.kr

Serdar Yegulalp editor@itworld.co.kr
저작권자 한국IDG & ITWorld, 무단 전재 및 재배포 금지
기사가 속한 카테고리는 언론사가 분류합니다.
언론사는 한 기사를 두 개 이상의 카테고리로 분류할 수 있습니다.