러스트의 소유권이란?
- 말 그대로 변수의 소유권이 누구에게 있냐 를 따져보는 것이다.
- 소유권이 필요한 이유는 소유권이 붕 떠버린 변수에 할당된 메모리를 반납하기 위해서이다.
- 변수는 컴퓨터의 메모리를 사용하고, 메모리는 한정적 자원이라 사용이 끝나면 반납되어야 한다.
스택과 힙
소유권을 공부하기 위해 스택과 힙 개념을 알아야 한다.
스택
- 먼저 들어간 데이터가 가장 나중에 나오는 방식으로 구현된다.
- 모든 데이터의 크기가 고정된 크기를 가져야 한다.
- 위의 특징 덕에 안으로 깊숙히 들어간 데이터 위에 중간부터 다른 데이터를 쌓는 것은 불가능하다.
- 계속 위로만 쌓고, 위에 있는 것 먼저 꺼내오기에 속도가 빠르다
- 정적으로 관리되기 때문에 할당과 해제가 물흐르듯 자연스럽다.
힙
- 메모리의 특정 지점을 선정하여 사용 중이라고 표기한다. 이 곳에 데이터를 저장할 준비를 한다. 이를 힙 공간 할당이라 부른다.
- 동적으로 공간을 할당하기 때문에, 크기가 미리 결정되어 있지 않아 크기가 변하는 데이터의 경우에 힙 메모리로 할당하기에 적합하다.
- 스택에 비해서 데이터 접근이 느리다. 포인터가 가리키는 곳을 계속 따라가야 하기 때문이다.
- 동적으로 관리되기 때문에 메모리 할당과 해제에 문제가 발생하는 경우가 많다.
- 할당된 메모리를 해제하지 않아 메모리가 모자라는 경우도 있고, 해제된 메모리를 한번 더 해제하는 것도 문제가 된다.
소유권 규칙
- 러스트에서의 값은 해당 값의 오너(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 타입bool
f64
와 같은 부동 소수점 타입들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 |