DaleSchool

소유권의 기본 규칙

중급20분

학습 목표

  • 소유권의 세 가지 규칙을 말할 수 있다
  • String 타입에서 값이 이동(move)되는 것을 확인할 수 있다
  • 이동 후 원래 변수를 사용하면 발생하는 에러를 이해한다
  • Copy 트레이트가 있는 타입(정수, bool 등)은 이동이 아닌 복사됨을 안다

동작하는 코드

먼저 이 코드를 Rust Playground에서 실행해보세요.

fn main() {
    let name = String::from("민수");
    let greeting = format!("안녕, {name}!");
    println!("{greeting}");
    println!("이름: {name}");
}

잘 돌아가죠? 이제 살짝 바꿔볼게요.

fn main() {
    let name = String::from("민수");
    let name2 = name;           // name을 name2에 대입
    println!("이름: {name}");   // 에러!
}
error[E0382]: borrow of moved value: `name`
 --> src/main.rs:4:24
  |
2 |     let name = String::from("민수");
  |         ---- move occurs because `name` has type `String`
3 |     let name2 = name;
  |                 ---- value moved here
4 |     println!("이름: {name}");
  |                      ^^^^ value borrowed here after move

해석: "name의 값이 name2로 이동했기 때문에, 더 이상 name을 사용할 수 없어요."

해결: 이동 후에는 새 주인(name2)을 사용하거나, clone()으로 복사본을 만드세요.

직접 수정하기

  1. 에러가 나는 코드에서 println!{name}{name2}로 바꿔보세요. 에러가 사라지나요?
  2. let name2 = name; 대신 let name2 = name.clone();으로 바꿔보세요. 이번에는 namename2도 사용할 수 있나요?

"왜?" — 소유권의 3대 규칙

Rust의 소유권 규칙은 딱 세 가지예요.

  1. 모든 값에는 주인(owner)이 있다
  2. 주인은 한 번에 하나만 존재한다
  3. 주인이 스코프를 벗어나면 값은 버려진다(drop)

"물건의 주인" 비유로 생각해보세요.

  • 여러분의 노트북은 주인이 한 명이에요 (규칙 2)
  • 노트북을 친구에게 주면(이동), 이제 그 친구가 주인이에요. 여러분은 더 이상 사용할 수 없어요
  • 주인이 방을 나가면(스코프 종료), 노트북도 함께 치워져요 (규칙 3)

이동(move) — 택배 보내기

String 같은 힙 데이터를 다른 변수에 대입하면, 값이 이동(move) 해요. 택배를 보내는 것과 같아요 — 보낸 사람 손에는 더 이상 물건이 없어요.

fn main() {
    let original = String::from("택배 내용물");
    let moved = original;  // 택배를 보냄!

    // original은 이제 빈 손
    // moved가 새 주인
    println!("{moved}");
}

모듈 06에서 벡터를 함수에 넘긴 후 다시 쓰려고 했을 때 에러가 났던 거 기억나세요? 그때 clone()으로 해결했었죠. 이제 그 에러가 왜 발생했는지 보이시죠?

fn print_fruits(fruits: Vec<&str>) {
    for f in fruits {
        println!("{f}");
    }
}

fn main() {
    let fruits = vec!["사과", "바나나"];
    print_fruits(fruits);       // fruits의 소유권이 함수로 이동!
    // println!("{:?}", fruits); // 에러! 이미 이동됨
}

함수에 값을 넘기는 것도 이동이에요. fruits의 주인이 main에서 print_fruits 함수의 매개변수로 바뀐 거예요.

Copy 타입 — 복사되는 값들

그런데 정수나 bool은 다르게 동작해요.

fn main() {
    let x = 42;
    let y = x;     // 복사됨! (이동이 아님)
    println!("x = {x}, y = {y}");  // 둘 다 사용 가능!
}

에러가 안 나요! 왜일까요?

스택에 저장되는 작은 값들은 복사해도 비용이 거의 없어요. 그래서 Rust는 이런 타입에 Copy 트레이트를 부여해서 이동 대신 복사가 일어나게 해요.

Copy 타입 (복사됨)Copy 아닌 타입 (이동됨)
i32, f64, bool, charString, Vec, HashMap
스택에 저장, 크기 고정힙에 데이터 저장, 크기 가변
더 알아보기: clone()은 왜 항상 쓰면 안 되나요?

clone()은 힙에 있는 데이터 전체를 복사해요. 작은 문자열이면 괜찮지만, 수만 개의 요소가 있는 벡터를 clone()하면 메모리와 시간이 많이 들어요.

fn main() {
    // 10,000개의 숫자를 복사 — 비용이 크다!
    let big_data = vec![0; 10_000];
    let copy = big_data.clone();
    println!("원본: {}, 복사본: {}", big_data.len(), copy.len());
}

다음 모듈에서 배울 빌림(borrowing) 을 쓰면, 복사 없이도 값을 사용할 수 있어요. clone()보다 훨씬 효율적이에요!

더 알아보기: 스코프와 drop

주인이 스코프를 벗어나면 Rust가 자동으로 값을 정리해요. 이걸 drop이라고 해요.

fn main() {
    {
        let s = String::from("잠깐!");
        println!("{s}");
    } // <- 여기서 s가 drop됨. 메모리 자동 정리!

    // println!("{s}"); // 에러! s는 이미 사라졌어요
}

C에서는 free()를 직접 호출해야 했지만, Rust는 스코프가 끝나면 알아서 해줘요. 깜빡할 일이 없어요!

연습 1. 아래 코드가 컴파일되도록 수정해보세요. clone()을 사용하지 않고 해결해보세요.

fn main() {
    let message = String::from("안녕하세요");
    let message2 = message;
    println!("{message}");
}

힌트: 이동 후에 원래 변수는 사용할 수 없어요. 어떤 변수를 출력해야 할까요?

연습 2. 아래 코드는 왜 에러가 나지 않을까요? 이유를 설명해보세요.

fn main() {
    let a = 10;
    let b = a;
    println!("a = {a}, b = {b}");
}

힌트: i32는 어떤 특별한 성질이 있나요?

소유권의 3대 규칙에 포함되지 않는 것은?