Rust의 가장 중요한 개념 중 하나는 소유권(Ownership)입니다. 이 개념은 메모리 안전성을 보장하면서도 가비지 컬렉션 없이 성능을 유지하는 핵심 메커니즘입니다. 소유권 개념은 힙에 저장된 데이터에 대해서만 적용되며, 힙의 데이터는 다른 변수에 대입하거나 함수의 인수로 전달 시 이동이 원칙입니다.
스택(Stack)과 힙(heap)

구분 | 스택(Stack) | 힙(Heap) |
---|---|---|
저장 방식 | 선입후출 (LIFO) | 필요할 때 동적으로 저장 |
속도 | 빠름 | 느림 |
용도 | 크기가 고정된 데이터 (ex. 정수, 포인터 등) | 크기가 가변적인 데이터 (ex. String, Vec) |
할당 방식 | 컴파일 타임에 크기 결정 | 런타임에 크기 결정 |
메모리 해제 | 자동 (블록 종료 시) | 직접 해제 필요 (Rust는 drop) |
🔹 기본 규칙
- 러스트의 각각의 값은 해당값의 오너(owner)라고 불리우는 변수를 갖고 있다.
- 한번에 딱 하나의 오너만 존재할 수 있다.
- 오너가 스코프 밖으로 벗어나는 때, 값은 버려진다(dropped).
🔸 문자열 이동(Move)
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s1); // 에러! s1은 더 이상 유효하지 않음
}
- String은 힙(heap)에 저장되는 데이터입니다.
- s2 = s1은 복사가 아니고, 소유권이 s1에서 s2로 이동(move)됩니다.
- 따라서, s1은 더 이상 유효하지 않으며, 컴파일 에러가 발생합니다. 그러나, s2를 화면 출력하면 문제 없습니다.

빨간 색으로 표시된 error만 문제가 되며, 노란 색으로 표시된 warning은 실행에 영향이 없기에 무시하거나 참고만해도 됩니다.
✅ 클론(Clone) 사용
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1: {}, s2: {}", s1, s2);
}
- .clone()을 사용하면 깊은 복사가 수행됩니다.
- 깊은 복사는 값(포인터, 길이, 용량)뿐만 아니라 (포인터가 가리키는) 참조된 데이터까지 복사하여 완전히 독립적인 객체를 생성하는 것입니다.
🔸 스택 값은 복사(Copy)
fn main() {
let x = 5;
let y = x;
println!("x: {}, y: {}", x, y);
}
- 정수형(i32 등)은 스택에 저장되며, Copy 트레잇이 적용되어 자동 복사됩니다.
- String은 Copy가 아니라 Clone이 필요합니다.
🛠️ 함수와 소유권
fn main() {
let s = String::from("Rust");
take_ownership(s);
// println!("{}", s); // 에러! 소유권이 함수로 이동
}
fn take_ownership(s: String) {
println!("받은 문자열: {}", s);
}
- 함수로 전달하면 소유권이 이동되어 원래 변수는 더 이상 사용할 수 없습니다.
- 위에서 println! 앞의 주석을 ctrl + / 키를 눌러 해제한 후 실행하면 소유권 이동으로 컴파일 오류가 발생합니다.
소유권을 되돌리기
fn main() {
let s1 = String::from("hi");
let s2 = return_ownership(s1);
println!("{}", s2);
}
fn return_ownership(s: String) -> String {
s
}
- 위와 같이 하면 return_ownership 함수가 실행 후 String type으로 s를 반환하고, 그 값을 s2가 받기때문에 문제없이 s2를 화면에 출력할 수 있습니다.
🛠️ 스코프와 Drop
스코프를 벗어나면 스택이든 힙이든 저장된 데이터가 메모리에서 해제(Drop)됩니다.
> 힙(heap)
fn main() {
let s = String::from("Hello"); // s는 문자열 "Hello"를 소유합니다.
{
let s1 = s; // s의 소유권이 s1으로 이동합니다. s는 더 이상 값을 소유하지 않습니다.
println!("s1 = {}", s1);
} // s1이 스코프를 벗어나고, 값이 해제됩니다.
println!("s = {}", s); // 에러 발생: s, s1은 더 이상 유효하지 않습니다.
// println!("s1 = {}", s1); // 에러 발생: s, s1은 더 이상 유효하지 않습니다.
}
- 중괄호로 둘어쌓인 영역인 Scope입니다. 위 main 함수는 바깥쪽에 스코프가 하나 있고, 그 안에 스코프가 있는 이중 구조입니다. 바깥쪽 스코프의 변수는 안쪽 스코프에도 유효하게 적용되지만, 안쪽 스코프의 변수는 바깥쪽 스코프에서 유효하지 않습니다.
- 힙의 데이터는 다른 변수에 대입하면 이동됩니다.
- 안쪽 스코프에서 s1으로 데이터가 이동되었으므로 바깥쪽 스코프의 println!문에 주석을 하고 실행하면 안쪽 println!문은 정상적으로 출력됩니다.

- 안쪽 스코프를 벗어나면 s1의 데이터가 메모리에서 해제되므로 s1은 더 이상 존재하지 않게 되므로, println!(“s1 = {}”, s1);에서 에러가 발생합니다.

- 또한 s는 s1으로 이동되었으므로 println!(“s = {}”, s); 실행 시 에러가 발생합니다.

> 스택(stack)
- 그러나, 스택 데이터인 정수를 s에 대입하면 이동이 아니라 s1으로 복사되고, s1은 스크프를 벗어나면 메모리에서 해제되므로 바깥쪽 스코프의 println!(“s1 = {}”, s1);이 오류가 발생하는 것은 힙 데이터인 String일 때와 같습니다.

그러나, s는 바깥쪽 스코프의 변수이므로 스코프를 벗어나도 해제되지 않고 s를 출력하는 안쪽, 바깥쪽 println!문 모두 정상적으로 작동합니다. 이 때 에러가 발생하는 s1 출력문은 주석으로 처리해야 합니다.

🧠 요약
개념 | 설명 |
---|---|
소유권 | 값의 유일한 소유자가 존재 |
Move | 소유권이 다른 변수로 이동 |
Clone | 깊은 복사 (스택과 힙 데이터를 모두 복사) |
Copy | 스택의 값은 자동 복사 가능 |
함수 전달 | 인자로 주면 소유권도 함께 전달됨 |