소유권(Ownership)

소유권의 개념 - 스택과 힙

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


스택(Stack)과 힙(heap)

구분스택(Stack)힙(Heap)
저장 방식선입후출 (LIFO)필요할 때 동적으로 저장
속도빠름느림
용도크기가 고정된 데이터 (ex. 정수, 포인터 등)크기가 가변적인 데이터 (ex. String, Vec)
할당 방식컴파일 타임에 크기 결정런타임에 크기 결정
메모리 해제자동 (블록 종료 시)직접 해제 필요 (Rust는 drop)

🔹 기본 규칙

  1. 러스트의 각각의 값은 해당값의 오너(owner)라고 불리우는 변수를 갖고 있다.
  2. 한번에 딱 하나의 오너만 존재할 수 있다.
  3. 오너가 스코프 밖으로 벗어나는 때, 값은 버려진다(dropped).

🔸 문자열 이동(Move)

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;

    println!("{}", s1); // 에러! s1은 더 이상 유효하지 않음
}
  • String은 힙(heap)에 저장되는 데이터입니다.
  • s2 = s1은 복사가 아니고, 소유권이 s1에서 s2로 이동(move)됩니다.
  • 따라서, s1은 더 이상 유효하지 않으며, 컴파일 에러가 발생합니다. 그러나, s2를 화면 출력하면 문제 없습니다.
String 타입은 복사가 아니라 이동이 되기 때문에 "이동 후 빌림"오류가 발행합니다.

빨간 색으로 표시된 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스택의 값은 자동 복사 가능
함수 전달인자로 주면 소유권도 함께 전달됨

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다