kakasoo

6. 소유권 (Ownership) 본문

프로그래밍/Rust

6. 소유권 (Ownership)

카카수(kakasoo) 2024. 3. 30. 18:48
반응형

러스트에는 소유권(Ownership)이라는 개념이 있고, 이는 다른 언어에서는 보기 힘든 특성이다.

일단 소유권의 세 가지 규칙을 먼저 보자.

  1. 러스트에서는 변수(variable)가 각 값(value)에 대한 소유권을 가진다.
  2. 소유자(owner = variable)가 범위(scope)를 벗어나면 그 값은 해제(deallocate)된다.
  3. 특정 시점에 소유자는 단 하나만이 존재할 수 있다.

 

지난 번에 작성한 코드

use std::io; // 어떤 외부 크레이트를 사용할지는 use로 명시 가능하다.

fn main() {
	let mut input = String::new(); // 구조체를 사용하여 빈 문자열을 생성한다.
	io::stdin().read_line(&mut input); // 주소를 참조할 수 있도록 &mut로 명시한다.
	let mars_weight = calcuate_weight_on_mars(100.0);

	println!("weight on Mars: {}kg", mars_weight);
}

fn calcuate_weight_on_mars (weight: f32) -> f32 {
 	(weight / 9.81) * 3.711
}

발생한 에러는 위와 같은데, 생각보다 컴파일러가 친절하게 알려주고 있다.

cargo build를 하면 발생하는 에러이지만 IDE에서도 린트 에러를 보여주기 때문에 빠르게 확인할 수 있다.

러스트는 기본적으로 불변변수가 되게끔 동작하기 때문에 ‘mut’ 이라는 키워드를 사용할 것을 고려하라고 한다.

 

저번 것을 우선 복습하면, 우리는 일단 let mut 키워드로 변수 input을 선언했고, 빈 문자열로 정의했다.

let mut 키워드는 변경 가능한 변수를 선언할 때 사용하는 키워드로, input의 Strting이 바뀔 수 있음을 뜻한다.

이제부터 이 코드로부터 소유권을 확인할 것이다.

 

소유권, 규칙1과 규칙2에 대하여

use std::io;

fn main() {
	let mut input = String::new(); // (1)
	io::stdin().read_line(&mut input); // (2)
	let mars_weight = calcuate_weight_on_mars(100.0);

	println!("weight on Mars: {}kg", mars_weight);
} // (3) input의 범위(scope)

fn calcuate_weight_on_mars (weight: f32) -> f32 {
 	(weight / 9.81) * 3.711
}

 

(1) 에서는 문자열 값에 대한 소유권은 input 이라는 변수에게 있다.

 

컴파일 시점에서는 이 변수 값 ( = 문자열 ) 의 크기를 알 방법이 없기 때문에 힙 영역에 저장한다.

대신 스택에서는 이 힙 영역을 가리키는 주소, 즉 포인터를 저장한다.

 

(3) 이 때, input(scope)의 범위를 벗어나면 힙에 할당되어 있는 문자열들을 해제(deallocate)된다.

 

이는 앞서 설명한 스마트포인터로 문자열은 스마트포인터의 한 유형이라고 볼 수 있다.

이런 해제는 컴파일러의 drop 함수로, 다른 언어에서는 소멸자라는 개념과 비슷하다.

여기까지로 러스트가 가지고 있던 두 가지 규칙에 대해서 모두 설명할 수 있다.

 

소유권, 규칙 3에 대하여

use std::io;

fn main() {
	let mut input = String::new(); // (1)
	let mut s = input; // (4)
	io::stdin().read_line(&mut input); // (2)
	let mars_weight = calcuate_weight_on_mars(100.0);

	println!("weight on Mars: {}kg", mars_weight);
} // (3) input의 범위(scope)

fn calcuate_weight_on_mars (weight: f32) -> f32 {
 	(weight / 9.81) * 3.711
}

(4) 만약 동일한 힙 영역에 대해서 참조할 수 있게 새 변수 s가 input을 대입하면 어떻게 될까?

 

C++에서는 이럴 경우 소멸자에 의한 해제가 2번 반복되어 일어나며 메모리를 손상시키는 원인이 된다.

의외로 이런 버그는 C++에서 쉽게 볼 수 있는 버그인데, 러스트에서는 3번 규칙을 통해서 이를 제거한다.

애초에 소유권을 특정 시점마다 소유권을 하나의 변수만 가질 수 있게 제한을 해버린다면 이중 해제 문제는 없다.

위와 같이 코드를 작성한다면,

 

use std::io;

fn main() {
	let mut input = String::new(); // (1)
	let mut s = input; // (4)
	io::stdin().read_line(&mut input); // (2), (5)
	let mars_weight = calcuate_weight_on_mars(100.0);

	println!("weight on Mars: {}kg", mars_weight);
} // (3) input의 범위(scope)

fn calcuate_weight_on_mars (weight: f32) -> f32 {
 	(weight / 9.81) * 3.711
}

(5) 에서는 이제 컴파일러가 불평을 뱉기 시작한다.

 

input을 s에 대입한 시점에서 input은 힙 영역의 데이터에 대한 소유권을 상실했다.

따라서 (2), (5) 에서 &mut input 파라미터를 대입하는 것은 불가능해졌기 때문에 컴파일 에러가 나는 것이다.

에러는 ‘value borrowed here after move’ 인데, 이는 번역하면 ‘차용’ 이라고 한다.

값이 이동된 후에 차용되었다는 에러 메시지 위에는 정확히 ‘s’에게 그 값이 이동해있다고 알려주는 문구도 있다.

 

소유권 이동에 따른 에러 메시지

  • value moved here
  • move occurs because ‘input’ has type ‘String’, which does not implement the ‘Copy' trait

우리가 새로운 소유자를 할당했을 때 값은 변수 s 쪽으로 이동했다.

이로 인한 두번째 메시지는 이동은 할 수 있지만 복사하는 것이 불가능하다는 에러이다.

type이 String ( = 문자열 )인 input은 복사될 수 없기 때문에 값이 이동이 발생했다는 얘기인데,

이는 값이 힙에 저장되어 있는 복합적인 유형이기 때문이다.

그렇다면 복사될 수 있는 유형은 무엇일까?

 

복사될 수 있는 유형

fn main () {
  let a = 5; // 5를 a에 저장한다.
  let b = a; // b에도 5가 저장된다. 즉, 5가 2개가 되었다.
}

 

정답은, 컴파일 시점에 그 크기를 알 수 있어 힙이 아닌 스택에 저장된 값들만이 복사가 가능하다.

위와 같은 코드에서는 a와 b는 서로 다른 주소에 저장된 5를 가지고 있기 때문에 5는 2개가 존재하는 것이다.

이를 복합 유형이 아닌, 단순 유형이라 부른다.

반응형