소유권, 스코프 및 이동

RustBeginner
지금 연습하기

소개

Rust는 소유권을 사용해 프로그램의 어느 부분이 값을 책임지는지, 그리고 해당 값을 언제 정리할 수 있는지를 결정합니다. 소유권은 하나의 변수 이름에 영구적으로 붙어 있는 레이블이 아니라 책임과 권한이라고 생각해야 합니다. 이 책임은 새로운 바인딩으로 이동할 수 있습니다. 이를 통해 가비지 컬렉터 없이도 여러 메모리 오류를 방지합니다.

이 실습에서는 먼저 작은 정수를 복사할 수 있다는 것을 확인합니다. 그런 다음 소유권이 이동된 뒤에도 소유한 String을 사용하도록 의도적으로 요청하고, 그 결과로 표시되는 컴파일러 진단 메시지를 읽습니다. 마지막으로 함수에서 반환된 소유권을 사용해 데이터 흐름을 수정합니다. 코드 프레임워크는 이미 준비되어 있으므로 각 수정은 작고 명확하게 유지됩니다.

스코프와 복사되는 정수 확인

이 단계에서는 작은 정수를 내부 스코프에 복사하고, 원래 바인딩을 계속 사용할 수 있는지 확인합니다.

준비된 프로젝트로 이동한 다음 소스 파일을 엽니다.

cd /home/labex/project/ownership-demo
nano src/main.rs

**스코프(scope)**는 바인딩이 유효한 영역입니다. 중괄호는 새로운 내부 스코프를 만듭니다. local_count 바인딩은 내부 중괄호 쌍 사이에서만 존재하고, warehouse_count는 바깥쪽 main 스코프에 속합니다.

Step 1 주석이 있는 줄에서 0만 warehouse_count로 바꿉니다.

        let local_count = warehouse_count; // Step 1: replace the placeholder.

정수 타입 i32는 Rust의 Copy 동작을 구현합니다. 따라서 값을 local_count에 할당하면 warehouse_count가 무효화되는 대신 작은 값이 복사됩니다.

Ctrl+O를 눌러 저장하고, Enter를 누른 다음 Ctrl+X로 종료합니다. 프로그램을 실행합니다.

cargo run --quiet

처음 두 줄은 두 바인딩을 모두 사용할 수 있음을 보여 줍니다.

Local count: 3
Warehouse count: 3

내부 스코프가 끝나면 local_count는 더 이상 유효하지 않습니다. 바깥쪽의 warehouse_count는 main이 끝날 때까지 계속 유효합니다.

의도적인 이동 오류 읽기

이 단계에서는 이동된 뒤에도 String을 의도적으로 사용하고, Rust 컴파일러의 진단 메시지를 해석합니다. 검사가 실패하는 것은 예상된 결과이며, 이 단계에서 학습해야 할 내용입니다.

String::from("PX-104")는 소유한 텍스트를 생성합니다. 작은 i32와 달리 String은 크기를 변경할 수 있는 별도의 할당 텍스트 버퍼를 관리합니다. Rust는 이 버퍼를 정리할 수 있도록 한 명의 소유자에게 책임을 부여합니다.

record_parcel(parcel_id) 호출은 String을 값으로 전달합니다. 소유권은 parcel_id 바인딩에서 함수 내부의 매개변수로 이동합니다. 함수는 값을 반환하고, 호출부에서는 반환된 소유자를 _returned_id에 저장합니다. 앞의 밑줄은 이 임시 바인딩을 현재 의도적으로 사용하지 않는다는 것을 Rust에 알립니다.

값 자체가 이동될 때 소멸하는 것은 아닙니다. 기존 바인딩만 해당 값을 사용할 권한을 잃습니다. 이 프로그램에서 소유권이 이동하는 경로는 다음과 같습니다.

parcel_id in main → parcel_id parameter in record_parcel → _returned_id in main

각 시점에는 동일한 텍스트에 대해 책임을 지는 현재 소유자가 한 명씩 존재합니다.

파일을 엽니다.

nano src/main.rs

Step 2 주석 아래의 줄을 찾아 앞에 있는 //만 삭제해 다음과 같이 만듭니다.

    println!("Original after move: {parcel_id}");

저장하고 종료합니다. 다음 명령을 실행하기 전에 결과를 예상해 봅니다. parcel_id는 더 이상 소유자가 아니므로 컴파일러가 이 사용을 거부해야 합니다.

검사를 실행합니다. 이 검사는 실패하는 것이 정상입니다.

cargo check

진단 메시지에서 다음과 같은 변하지 않는 핵심 부분을 찾습니다.

error[E0382]: borrow of moved value: `parcel_id`
...
value moved here
...
value borrowed here after move

borrow라는 단어가 나타나는 이유는 println!이 값을 표시하기 위해 임시 접근만 필요로 하기 때문입니다. 그러나 소유권이 이미 이동했으므로 parcel_id를 통한 접근은 여전히 불가능합니다. 다음 단계에서는 함수에서 소유권을 돌려받은 바인딩을 사용합니다.

반환된 소유자 계속 사용하기

이 단계에서는 record_parcel에서 반환된 값을 이름을 붙여 사용함으로써 이동 오류를 수정합니다.

함수 시그니처 fn record_parcel(parcel_id: String) -> String은 함수가 String의 소유권을 받고 String을 반환한다는 뜻입니다. 함수의 마지막 parcel_id 표현식은 소유권을 호출자에게 다시 넘깁니다.

파일을 엽니다.

nano src/main.rs

두 곳을 간단히 수정합니다. 먼저 함수 호출 줄에서 _returned_id를 returned_id로 바꿉니다. 이제 이 값을 사용할 것이므로 임시 밑줄 이름을 제거하는 것입니다.

그런 다음 오류가 발생한 출력 줄을 다음 줄로 바꿉니다.

    println!("Returned owner: {returned_id}");

기존 parcel_id 바인딩은 이동된 후에도 계속 무효 상태입니다. _returned_id를 returned_id로 바꾼다고 해서 기존 바인딩이 복구되는 것은 아닙니다. 이미 소유권을 돌려받은 바인딩에 일반적인 이름을 부여하는 것입니다. 이제 returned_id가 현재 소유자이므로 사용해야 할 올바른 이름입니다.

저장하고 종료한 다음, 수정된 프로그램을 검사합니다.

cargo check

E0382 오류 없이 Finished 결과가 표시되면 소유권 흐름이 유효하다는 뜻입니다. 프로그램을 실행합니다.

cargo run --quiet

전체 출력은 다음과 같아야 합니다.

Local count: 3
Warehouse count: 3
Recorded parcel: PX-104
Returned owner: PX-104

main이 끝나면 returned_id가 스코프를 벗어나고, Rust가 소유한 String을 정리합니다. 이 데이터에 책임을 지는 소유자는 여전히 한 명뿐입니다.

요약

렉시컬 스코프와 값의 수명을 연결해 확인하고, 원래 바인딩을 무효화하지 않는 i32 복사를 살펴보았습니다. 또한 String이 함수로 이동하는 과정을 따라가고 E0382를 해석한 뒤, 호출자에게 반환된 소유권을 안전하게 계속 사용하는 방법을 확인했습니다.