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, char | String, 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는 어떤 특별한 성질이 있나요?

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

  • A) 모든 값에는 주인이 있다
  • B) 주인은 한 번에 하나만 존재한다
  • C) 주인이 스코프를 벗어나면 값이 버려진다
  • D) 모든 값은 자동으로 복사된다

Q2. 아래 코드의 결과는?

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
    println!("{s2}");
}
  • A) 컴파일 에러
  • B) "hello" 출력
  • C) 빈 문자열 출력
  • D) 런타임 에러

Q3. i32 타입의 값을 다른 변수에 대입하면 어떻게 되나요?

  • A) 이동(move)되어 원래 변수를 사용할 수 없다
  • B) 복사(copy)되어 두 변수 모두 사용할 수 있다
  • C) 참조(reference)가 생긴다
  • D) 컴파일 에러가 발생한다