러스트의 소유권이란?
- 말 그대로 변수의 소유권이 누구에게 있냐 를 따져보는 것이다.
- 소유권이 필요한 이유는 소유권이 붕 떠버린 변수에 할당된 메모리를 반납하기 위해서이다.
- 변수는 컴퓨터의 메모리를 사용하고, 메모리는 한정적 자원이라 사용이 끝나면 반납되어야 한다.
스택과 힙
소유권을 공부하기 위해 스택과 힙 개념을 알아야 한다.
스택
- 먼저 들어간 데이터가 가장 나중에 나오는 방식으로 구현된다.
- 모든 데이터의 크기가 고정된 크기를 가져야 한다.
- 위의 특징 덕에 안으로 깊숙히 들어간 데이터 위에 중간부터 다른 데이터를 쌓는 것은 불가능하다.
- 계속 위로만 쌓고, 위에 있는 것 먼저 꺼내오기에 속도가 빠르다
- 정적으로 관리되기 때문에 할당과 해제가 물흐르듯 자연스럽다.
힙
- 메모리의 특정 지점을 선정하여 사용 중이라고 표기한다. 이 곳에 데이터를 저장할 준비를 한다. 이를 힙 공간 할당이라 부른다.
- 동적으로 공간을 할당하기 때문에, 크기가 미리 결정되어 있지 않아 크기가 변하는 데이터의 경우에 힙 메모리로 할당하기에 적합하다.
- 스택에 비해서 데이터 접근이 느리다. 포인터가 가리키는 곳을 계속 따라가야 하기 때문이다.
- 동적으로 관리되기 때문에 메모리 할당과 해제에 문제가 발생하는 경우가 많다.
- 할당된 메모리를 해제하지 않아 메모리가 모자라는 경우도 있고, 해제된 메모리를 한번 더 해제하는 것도 문제가 된다.
소유권 규칙
- 러스트에서의 값은 해당 값의 오너(owner)라고 불리우는 변수를 가지고 있다.
- 한번에 딱 하나의 오너만 존재 가능하다.
- 오너가 스코프 밖으로 벗어나면, 값은 버려진다. (dropped)
변수 스코프 코드 예제
{ // s는 유효하지 않습니다. 아직 선언이 안됐거든요.
let s = "hello"; // s는 이 지점부터 유효합니다.
// s를 가지고 뭔가 합니다.
} // 이 스코프는 이제 끝이므로, s는 더이상 유효하지 않습니다.
- 블록이 끝나면서 변수에 할당된 공간이 회수된다.
- 우리의 눈엔 보이지 않지만 마지막에
drop이라는 것이 일어나며, 소유권이 반환된다.
String 타입의 소유권 이동
String타입에 들어가는 값은 동적이라 힙 메모리를 이용한다.- 힙 메모리를 사용하는 변수는 '다른 변수로 옮겨지거나', '함수의 인자로 이용될 때' 소유권 이동이 발생한다.
힙 메모리를 사용하는 변수에 소유권 이동이 발생하는 이유는
두번 해제(double free)오류를 범하지 않기 위함이다. 이로 인해 메모리 손상과 보안 취약성 문제를 일으킬 수 있다.
위키피디아 Memory safety 문서 의 Types of memory errors 를 보면,Double free문제에 대한 설명이 있다.
소유권 이동 코드 예제: 변수 이동
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1);
위의 코드는 다른 고수준 프로그래밍 언어에서는 문제가 없는 코드겠지만, 러스트에서는 아래와 같은 에러가 발생한다. 소유권이 이동하면서 s1 은 더이상 유효하지 않은 변수가 되기 때문이다.
소유권 이동이 발생하지 않아 같은 값을 참조한다면,
s1과s2의 블록 종료 시점이 달라졌을 때,s1이 먼저 메모리 해제된다면,s2는 해제된 메모리를 가리키고 있게 된다.
자동으로 깊은 복사를 하거나, 두 개의 변수가 하나의 메모리 주소를 참조하지 않는다.
error[E0382]: use of moved value: `s1`
--> src/main.rs:4:27
|
3 | let s2 = s1;
| -- value moved here
4 | println!("{}, world!", s1);
| ^^ value used here after move
|
= note: move occurs because `s1` has type `std::string::String`,
which does not implement the `Copy` trait

.clone() 을 이용한 깊은 복사 코드 예제
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
- 위 코드는 깊은 복사가 일어나고, 정상적으로 수행된다.
- 다만, 복사하는 비용이 커지면 퍼포먼스가 떨어질 수 있다.
스택 데이터 복사 코드 예제
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);
- 위 코드는 따로
.clone()과 같은 메서드를 이용하지 않아도 정상적으로 수행된다. - 정수형 타입은 정적인 크기를 가진 채로 스택에 저장되어 있기 때문에 메모리 할당, 해제에 대한 문제가 없기 때문이다.
- 런타임에 크기가 커지거나 작아질 일이 없다.
스택에 저장되는, 복사가 가능한 타입들
u32와 같은 모든 정수형 타입들true,false와 같은 boolean 타입boolf64와 같은 부동 소수점 타입들Copy가 가능한 타입만으로 구성된 튜플들- ex)
(i32, i32), 단(i32, String)은 되지 않는다.
- ex)
함수와 소유권
- 함수의 인자로 변수를 넘기는 것은 소유권을 넘기는 것과 동일한 효과를 갖는다.
- 함수의 반환 값으로 다시 소유권을 가져올 수 있다.
함수의 소유권 이동 예제 코드
fn main() {
let s = String::from("hello"); // s가 스코프 안으로 들어왔습니다.
takes_ownership(s); // s의 값이 함수 안으로 이동했습니다...
// ... 그리고 이제 더이상 유효하지 않습니다.
let x = 5; // x가 스코프 안으로 들어왔습니다.
makes_copy(x); // x가 함수 안으로 이동했습니다만,
// i32는 Copy가 되므로, x를 이후에 계속
// 사용해도 됩니다.
} // 여기서 x는 스코프 밖으로 나가고, s도 그 후 나갑니다. 하지만 s는 이미 이동되었으므로,
// 별다른 일이 발생하지 않습니다.
fn takes_ownership(some_string: String) { // some_string이 스코프 안으로 들어왔습니다.
println!("{}", some_string);
} // 여기서 some_string이 스코프 밖으로 벗어났고 `drop`이 호출됩니다. 메모리는
// 해제되었습니다.
fn makes_copy(some_integer: i32) { // some_integer이 스코프 안으로 들어왔습니다.
println!("{}", some_integer);
} // 여기서 some_integer가 스코프 밖으로 벗어났습니다. 별다른 일은 발생하지 않습니다.
위의 코드에서는 소유권이 이동하지만, 힙 영역을 사용하는
s는 함수에 소유권을 뺏겨 함수 이후부터 이용이 불가능하고, 스택 영역을 사용하는x는 여전히 이용이 가능하다.
함수의 반환 값을 이용한 소유권 반환 예제 코드
fn main() {
let s1 = gives_ownership(); // gives_ownership은 반환값을 s1에게
// 이동시킵니다.
let s2 = String::from("hello"); // s2가 스코프 안에 들어왔습니다.
let s3 = takes_and_gives_back(s2); // s2는 takes_and_gives_back 안으로
// 이동되었고, 이 함수가 반환값을 s3으로도
// 이동시켰습니다.
} // 여기서 s3는 스코프 밖으로 벗어났으며 drop이 호출됩니다. s2는 스코프 밖으로
// 벗어났지만 이동되었으므로 아무 일도 일어나지 않습니다. s1은 스코프 밖으로
// 벗어나서 drop이 호출됩니다.
fn gives_ownership() -> String { // gives_ownership 함수가 반환 값을
// 호출한 쪽으로 이동시킵니다.
let some_string = String::from("hello"); // some_string이 스코프 안에 들어왔습니다.
some_string // some_string이 반환되고, 호출한 쪽의
// 함수로 이동됩니다.
}
// takes_and_gives_back 함수는 String을 하나 받아서 다른 하나를 반환합니다.
fn takes_and_gives_back(a_string: String) -> String { // a_string이 스코프
// 안으로 들어왔습니다.
a_string // a_string은 반환되고, 호출한 쪽의 함수로 이동됩니다.
}
튜플을 이용해 여러 값을 반환받는 예제
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
println!("The length of '{}' is {}.", s2, len);
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len(); // len()함수는 문자열의 길이를 반환합니다.
(s, length)
}
참조자
사실 참조자라는 것을 이용하여, 소유권을 함수로 넘기지 않고 변수를 이용하는 방법도 있다.
이는 다음 포스팅인 러스트 참조자와 빌림 개념 에서 알아볼 수 있다.
레퍼런스
'러스트 (Rust)' 카테고리의 다른 글
| 러스트 (Rust) 의 슬라이스 (Slice) 개념 (0) | 2022.11.02 |
|---|---|
| 러스트 (Rust) 의 참조자 (References) 와 빌림 (Borrowing) 개념 (0) | 2022.11.02 |
| 러스트 (Rust) 의 반복문 정리 (0) | 2022.11.01 |
| 러스트 (Rust) 의 제어문 문법 정리 (0) | 2022.11.01 |
| 러스트 (Rust) 함수 사용법 핵심 정리 (0) | 2022.11.01 |