4. 소유권 이해하기
https://rinthel.github.io/rust-lang-book-ko/ch04-00-understanding-ownership.html
소유권 이해하기 - The Rust Programming Language
소유권(Ownership)은 러스트의 가장 유니크한 특성이며, 러스트가 가비지 콜렉터 없이 메모리 안정성 보장을 하게 해줍니다. 그러므로, 소유권이 러스트 내에서 어떻게 동작하는지 이해하는 것은
rinthel.github.io
1. 소유권
1.1 소유권 규칙
- 러스트가 다루는 각각의 값은 소유자(owner) 변수를 가지고 있다.
- 특정 시점에 값의 소유자는 단 하나뿐이다.
- 소유자가 범위를 벗어나면 그 값은 제거된다.
기본적인 타입들은 주로 스택(Stack)에 저장되며, 범위(Scope)를 벗어나면 스택에서 자연적으로 제거됩니다.이것이 가능한 이유는 사용할 메모리의 크기를 미리 알 수 있다는 점(예측가능한) 때문입니다.
이것과 반대로 복잡한 타입들(예를 들면 String)은 컴파일 시점에서 알 수 없는 크기를 지니며, 이로인해 사용되는 메모리가 바이너리 형태로 미리 변환될 수 없습니다. 따라서 소유권 개념을 이용하여 메모리를 제어(회수)하고, 이에 따라 잘못된 메모리 해제로 인해 발생될 수 있는 오류들(Double Free같은)을 미리 예방할 수 있습니다.
1.1.1 복잡한 타입에 대한 러스트의 메모리 해제 방식
{
let s = String::from("hello"); // 변수 s는 이 지점부터 유효하다.
// s를 이용한 동작 수행
} // 여기서부터는 범위를 벗어나게 되므로 변수 s는 유효하지 않다.
복잡한 타입의 일부 중 하나인 String 타입의 변수 s는 자신의 범위(Scope)를 벗어나게 되면 Drop이라는 함수의 호출로 인하여 유효하지 않은 상태(메모리의 해제)가 됩니다.
2. 변수와 데이터의 상호작용: 이동(Move)
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1);
println!("{}, world!", s2);
복잡한 타입들은 값의 복사가 이루어질 때, 할당된 메모리를 복사하는 대신 기존의 변수가 더 이상 유효하지 않다고 판단합니다. 따라서 첫 번째 변수를 무효화해버리기 때문에 값이 복사(Copy)된 것이 아니라 이동(Move)했다고 표현합니다. (무효화된 변수는 따로 해제할 필요가 없습니다.)
따라서 위의 코드는 변수 s1이 s2에 이동한 이후 s1이 유효하지 않은 상태에서 s1을 사용하였기 때문에 컴파일 에러가 발생합니다.
이 모든건 메모리 안정성 버그(UAF, Double Free 등)를 해결하기 위한 러스트의 노력입니다.
3. 변수와 데이터의 상호작용: 복제(Clone)
만약 복잡한 타입에 대하여 값의 이동이 아닌 복사를 하길 원한다면 clone 메소드를 사용할 수 있습니다.
이는 타 프로그래밍 언어에서의 깊은 복사와 비슷하게 스택뿐만 아니라 힙 메모리까지 복사하여 완전히 다른 포인터를 가지게합니다.
let s1 = String::from("hello");
let s2 = s1.clone();
println!("{}, world!", s1); // s1은 clone 메소드를 이용하여 값을 복제했기 때문에 유효합니다.
println!("{}, world!", s2);
4. 스택 전용 데이터: 복사(copy)
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);
위의 코드는 clone 메소드 없이도 변수 x와 y가 모두 유효합니다. 정수형 같은 타입은 컴파일 시점에서 그 크기를 알 수 있으며, 온전히 스택에 저장되기 때문입니다.
이처럼 스택에 저장되는 정수형 같은 타입에는 Copy trait이 적용되어 있는데, 이 Copy trait은 이전 변수를 새 변수에 할당해도 무효되지 않는 특성을 제공합니다.
5. 소유권과 함수
값을 함수에 전달하는 것과 함수에서 값을 받는 리턴값의 경우는 모두 변수의 소유권과 같은 패턴입니다.
즉, 값을 다른 변수에 할당하면 소유권이 옮겨진다. 힙 메모리에 저장된 변수의 데이터는 소유권이 다른 변수로 옮겨지지 않았다면 범위를 벗어날 때 Drop 함수를 호출합니다.

6. 참조와 대여
참조(Refernces, &)는 소유권을 넘기지 않고도 값을 이용하게 해주는 개념입니다. 참조의 반대는 역참조(dereferencing, *)입니다.
또한 참조를 이용한 매개변수 전달을 '대여(burrowing)'이라고 부릅니다.

기본적으로 참조하고 있는 값은 불변입니다.
가변 참조는 이 제약을 없애주는 기능입니다. 가변 참조는 참조하려하는 변수에 mut 키워드를 붙이고, 함수의 인자로 넘길 때 &mut 키워드를 붙이면됩니다.
let mut s = String::from("hello");
change(&mut s);
함수의 매개변수 또한 &mut 키워드를 붙인다.
fn change(some_string: &mut String) {
...
}
가변참조는 한 범위 안에 하나의 가변참조만이 존재할 수 있습니다. 즉, 둘 이상의 변수가 같은 값을 가변 참조할 수는 없습니다. 이런 제약은 Data races를 컴파일 시점에 방지할 수 있다는 장점이 있습니다.
또한 이미 불변 참조를 사용중일 때, 같은 값을 가변 참조할 수 없다. 불변인 값을 바꾸어선 안되기 때문입니다.
같은 이유에서 불변 참조는 여러 개를 생성해도 무방합니다. 데이터를 읽는 동작은 다른 사용자의 같은 동작에 영향이 없기 때문입니다.

하지만 위와 같은 상황에서는 가변 변수 s에 대해서 불변 참조 r1과 가변 참조 r2가 모두 이상없이 컴파일에 성공했습니다. 이는 기존 불변 참조 r1을 사용하지 않았기 때문에 가능한 상태입니다.
실제로는 r1은 불변성을 기대하지만, r2는 가변성을 기대한 참조이기 때문에, r1과 r2의 수명을 고려하지 않고서 코딩을 한다면 두 값은 언젠가 충돌합니다.
예를 들면 불변참조 r1의 선언 이후, 가변참조 r2를 이용하여 s의 값을 함수 인수로 쓴다던가, 아니면 값을 아예 바꾼다던가 하는 동작이 있을 경우 에러가 발생합니다.
이러한 충돌을 저지하기 위해서는 각각의 참조 수명을 고려하여 코드를 진행시켜야 할 것입니다.
7. 슬라이스
소유권을 갖지 않는 또다른 데이터 패턴으로 슬라이스(slice)가 존재합니다. 이는 컬렉션 타입의 데이터 묶음으로부터 연속된 일련의 요소들을 참조할 수 있도록 하는 패턴입니다.
만약 우리가 어떤 문자열의 일부만을 리턴하고 싶다면, 색인(Index)을 먼저 찾아낸 뒤에 해당 색인을 이용하여 스트링을 일부 출력할 수 있을 것 입니다.
하지만 만약 색인을 찾아낸 뒤에 해당 문자열의 내용을 바꿔야하는 상황이 생긴다면 어떻게 처리해야 할까요? 기껏 찾아낸 색인은 무의미해지고 이 다음에 문자열의 일부를 획득하는 것은 더더욱 어려워질 것입니다.
러스트는 이러한 문제에 대한 해결책으로 슬라이스(String Slice)를 제공합니다.
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
스트링뿐만이 아니라 대부분의 타입은 슬라이스로 작성될 수 있습니다.
해당 타입의 일부를 참조하는 것이기 때문에 참조(&)표시를 사용합니다. 또한 [starting_index..ending_index는 starting_index를 포함하고 ending_index를 포함하지 않은 범위까지를 기술합니다.
여기서 첫 번째 색인부터 시작하길 원할 때 이를 생략해서 쓸 수도 있고, 반대로 마지막 색인까지 포함하길 원하면 이것 또한 생략하여 쓸 수 있습니다.
let s = String::from("hello");
let slice = &s[3..]; // 이는 &s[3..len]와 동일합니다.
let slice = &s[..2]; // 이는 &s[0..2]와 동일합니다.
이러한 스트링 슬라이스는 &str로 그 타입을 나타냅니다.
fn first_word(s: &str) -> &str {
...
}
스트링 타입이 아닌 다른 컬렉션들에 대해서도 슬라이스를 적용시킬 수 있습니다.
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
이런 슬라이스는 &[i32]로 그 타입을 나타냅니다.