Intro
프로그래밍을 처음 접할 때 C/C++를 배우게 된다되면 가장 큰 벽 중 하나는 바로 포인터(pointer)입니다. Rust에서는 Ownership(소유권) 개념이 이와 비슷한 벽일 수 있습니다. 다른 Programming에서는 나오지 않는 규칙이기 때문이죠.
Ownership : 하나의 데이터는 하나의 변수가 소유한다.
하지만 Rust의 소유권 개념은 새로운 규칙일 뿐 그렇게 어렵지는 않습니다. 만약 여러분이 소유권 규칙을 어긴 코드를 작성했을 경우 rust에 내장된 Linter가 소유권을 위반한 위치와 대처법에 대해서 상세하게 알려주기 때문에 큰 부담감을 가지지 않아도 됩니다. 애초에 소유권 규칙을 어기고 build를 수행하게 된다면 컴파일 타임 에러(Compiler time error)가 발생하기 때문에 소유권 규칙을 어김으로 생길 런타임 에러(runtime error)는 걱정할 필요는 없습니다. 반면, C++은 포인터에 대한 실수가 런타임 에러를 발생시킬 수 있기 때문에, 소유권과는 큰 차이라 할 수 있습니다
이러한 Rust의 소유권 규칙은 왜 도입되었을까요? Rust의 소유권 규칙은 메모리 안전성을 보장하고, 동시에 효율적인 메모리 관리를 가능하게 하기 위해 도입되었습니다. Rust는 전통적인 가비지 컬렉션 방식 대신, 컴파일 타임에 메모리 관리를 수행하는 시스템을 채택하여, 성능을 최적화하고, 런타임 비용을 최소화하려고 했습니다. 주요 이유는 아래와 같습니다.
- 메모리 안전성:
- Rust는 프로그램의 메모리 오류(예: 더 이상 사용되지 않는 메모리 접근, 댕글링 포인터, 데이터 경쟁 등)를 컴파일 타임에 방지하고자 했습니다. 이를 위해 소유권(ownership), 대여(borrowing), 생애(lifetime) 등의 개념을 도입하여, 프로그램이 실행되기 전에 이러한 오류를 잡을 수 있도록 했습니다.
- 예를 들어, move와 borrow 규칙을 통해 하나의 데이터가 동시에 두 곳에서 수정되는 문제를 피할 수 있습니다.
- 성능 최적화:
- Rust는 가비지 컬렉션을 사용하지 않기 때문에, 메모리 할당과 해제를 수동으로 하지 않으면서도 메모리 관리의 오버헤드를 없앴습니다. 소유권 규칙은 데이터가 명확하게 "소유자"를 가지므로, 메모리 해제를 자동으로 할 수 있고, 불필요한 복사를 최소화할 수 있습니다.
- 소유권과 빌림 규칙을 통해, 데이터의 복사를 최소화하고, 메모리를 더 효율적으로 사용할 수 있습니다.
- 동시성 및 데이터 경쟁 방지:
- Rust의 소유권 규칙은 동시에 여러 스레드에서 데이터를 안전하게 공유할 수 있도록 돕습니다. mutable borrow가 하나만 허용되기 때문에, 동시에 여러 스레드에서 데이터를 변경하는 위험을 줄이고, 데이터 경쟁(race condition)을 방지할 수 있습니다.
- 컴파일 타임 오류 처리:
- Rust의 소유권 규칙은 컴파일 타임에 오류를 잡을 수 있게 해줍니다. 예를 들어, 객체의 생애주기가 명확하게 정의되므로, 런타임 오류를 줄이고, 프로그램이 실행되기 전에 많은 잠재적인 버그를 잡을 수 있습니다.
결론적으로, Rust의 소유권 규칙은 메모리 관리의 안전성과 성능을 동시에 만족시키고, 개발자가 런타임 중에 발생할 수 있는 많은 오류를 미리 방지할 수 있도록 설계되었습니다. 이는 Rust가 고성능 시스템 프로그래밍 언어로 자리잡을 수 있도록 한 핵심 요소입니다.
Ownership 도입 예제
Rust 1.79.0 (released 2024-06-13)
이번 예제는 rust와 C++을 비교하면서 설명하겠습니다.
- C++ : delete를 통해 동적으로 할당된 메모리를 해제한 후, 해당 메모리를 참조하려고 합니다. 이로 인해 **댕글링 포인터(dangling pointer)**가 발생하며, 정의되지 않은 동작(UB, Undefined Behavior)이 일어날 수 있습니다.
#include <iostream>
void create_and_use_pointer() {
int* ptr = new int(42); // 동적으로 메모리 할당
delete ptr; // 메모리 해제
std::cout << *ptr << std::endl; // 메모리 해제 후 포인터 접근 - 댕글링 포인터
}
int main() {
create_and_use_pointer();
return 0;
}
- Rust : Rust에서 Box를 사용하여 메모리를 할당하면, 해당 메모리는 소유권을 가지는 변수(ptr)가 범위를 벗어나면 자동으로 해제됩니다. Rust는 이 메모리 해제 후에 해당 메모리에 접근하려고 할 경우 컴파일 타임에 오류를 발생시켜, 댕글링 포인터 문제를 방지합니다.
fn create_and_use_pointer() {
let ptr = Box::new(42); // 동적으로 메모리 할당
// 메모리 해제는 Box가 범위를 벗어날 때 자동으로 처리됨
drop(ptr); // 수동으로 삭제도 가능
// println!("{}", *ptr); // 이 줄을 주석 처리해야 컴파일됨
}
fn main() {
create_and_use_pointer();
}
이러한 문제를 Compiler가 잡아줄 수 있도록 설계된 규칙이 바로 소유권 규칙입니다. 다음 글에서 다양한 예제를 살펴보도록 하겠습니다.
Code Example
Rust 1.79.0 (released 2024-06-13)
fn main(){
/* 1. scope */
// scope를 벗어난 변수는 해제된다.
{
let _x1 = 2;
}
// println!("{_x1}"); // [Error] _x1는 더이상 유효하기 않음
/* 2. 기본Type 값 복사 */
// Primary Type은 대입했을 떄 값이 Copy된다. Copy Trait이 있기 때문이다.
let x1 = 1;
let x2 = x1; // x1 copied. = 연산자는 "복사" 작용을 했다.
println!("{x1}, {x2}");
/* 3. 소유권 이동 */
// Primary 외 Type은 대입했을 때 소유권이 move 된다. Copy Trait이 없기 때문이다.
let x1 = String::from("test");
let x2 = x1; // x1 moved. = 연산자는 "이동" 작용을 했다.
// println!("{x1}, {x2}"); // [Error] x2는 더이상 유효하지 않음
println!("{x2}");
/* 4. clone을 활용한 값 복사 */
// Primary Type 외의는 Clone으로 값을 Copy함을 명시하면 Copy된다.
// Clone Trait이 있는 경우에만 해당된다.
let x1 = String::from("test");
let x2 = x1.clone(); // clone()으로 값 Copy가 동작
println!("{x1}, {x2}");
/* 5. 불변 참조 복사 */
// 불변 참조는 소유권 이동 없이 다수 만들 수 있다.
// 단 원본 값의 소유권이 이동되면 기존 참조 또한 유효하지 않는다.
let x1 = String::from("test");
let x1_ref = &x1; // 값의 참조를 복사.
let x1_ref2 = x1_ref; // 참조를 복사
// 소유권을 잃지 않았으며 모든 불변 참조 복사는 유효하다.
println!("{}, {}, {}", x1, *x1_ref, *x1_ref2);
// x1의 소유권은 _x2로 이동. 따라서 x1을 참조하던 것들도 모두 소유권 상실
let _x2 = x1;
// println!("{}", x1); // [Error] x1 소유권 소모로 유효하지 않음
// println!("{}", *x1_ref); // [Error] x1 소유권 소모로 x1_ref도 유효하지 않음
// println!("{}", *x1_ref2); // [Error] x1 소유권 소모로 x1_ref2도 유효하지 않음
/* 6. 가변 참조 빌림 */
// 가변 참조는 소유권을 "빌려"준다 (Borrowing).
// 빌려준 동안에는 소유권이 없기 때문에 다른 변수에 소유권 관련 동작을 할 수 없다.
// 빌려줬던 변수가 소유권 관련 사용이 끝나면 다시 소유권이 되돌아 온다.
let mut x1_mut = String::from("test");
// 가변 참조 빌림. 이 시점에서 x1_mut 소유권은 소멸된 것과 같음
let _x1_ref = &mut x1_mut;
// let _x1_ref2 = &mut x1_mut; // [Error] x1_mut의 소유권은 이미 빌려줬으므로 불가능
// 빌린 소유권으로 string data 변경
_x1_ref.push_str(" add1");
println!("{}", *_x1_ref); // test add1
// 이 이후로 _x1_mut_ref 코드를 작성하지 않으면 소유권은 반납한다.
// 가변 참조 빌림. 소유권이 반납되어 x1_mut이 소유권을 가게 되어 _x1_ref2에게 소유권을 빌려줄 수 있음
let _x1_ref2 = &mut x1_mut;
// _x1_ref2를 이 이후 사용하지 않아 소유권이 즉시 반납됨.
// 불변 참조 복사 후 가변 참조 빌림.
// x1_mut은 소유권이 다시 생겼으므로 불변 참조 복사, 가변 참조 빌림을 수행할 수 있음.
let _ref1 = &x1_mut; // 불변 참조 복사
let _ref2 = &x1_mut; // 불변 참조 복사
let _ref3 = &x1_mut; // 불변 참조 복사
let _x1_ref3 = &mut x1_mut; // 가변 참조 빌림. 위에서 선언한 불변 참조들은 전부 무효화 된다.
// let _x2_ref4 = &x1_mut; // [Error] 가변 참조를 빌려준 상태로, 불변 참조 복사를 수행할 수 없음
// println!("{_ref3}"); // [Error] _ref3은 무효화 되어 사용할 수 없음.
_x1_ref3.push_str(" add2"); // 가변 참조 사용 종료. 소유권 반납
println!("{x1_mut}"); // test add1 add2
// println!("{_ref3}"); // [Error] _x1_ref3의 사용이 끝나서 x1_mut에 소유권이 돌아와도 이미 무효화 된 참조는 사용할 수 없음
/* 7. function example */
// 소유권을 빌리는 함수
fn modify_string(s: &mut String) {
s.push_str(" world!");
}
// 소유권을 소모하는 함수
fn consume_string(s: String) {
println!("Consumed string: {}", s);
}
// 소유권 빌려서 변경
let mut my_string = String::from("Hello");
modify_string(&mut my_string);
println!("After modify: {}", my_string); // Hello world!
// 소유권 가져와서 소모
let my_string2 = String::from("Hello");
consume_string(my_string2);
// println!("{}", my_string2); // [Error] 소유권이 이전됨
// 값을 복사해서 가져오기
let my_string3 = String::from("Hello");
consume_string(my_string3.clone()); // 값을 복사했기 때문에 소모되지 않음
println!("After clone: {}", my_string3); // Hello
}
* reference
- https://doc.rust-lang.org/book/
'Rust > Basic' 카테고리의 다른 글
[Rust Tutorial] 9 - mod, use (2) | 2024.11.20 |
---|---|
[Rust Tutorial] 8 - Struct, Impl, Trait (0) | 2024.11.18 |
[Rust Tutorial] 6 - Control Flow (0) | 2024.11.17 |
[Rust Tutorial] 5 - Function (0) | 2024.11.17 |
[Rust Tutorial] 4 - Data Type (0) | 2024.11.14 |