프로그래밍 PROGRAMMING/기타 ETC

[RUST] Rust 소유권 규칙(Ownership Rules)

매운할라피뇨 2026. 2. 28. 08:46
반응형

Rust 소유권 규칙(Ownership Rules)

 

개요

Rust의 소유권(Ownership) 은 "각 값에는 단 하나의 소유자가 있다"는 규칙입니다. 소유자가 스코프를 벗어나면 값은 자동으로 정리(drop)되고, 대입·함수 전달 시 이동(move) 이 기본이라 C/C++에서 흔한 이중 해제(double free)나 use-after-free를 원천적으로 막을 수 있습니다. 이 글에서는 소유권의 세 가지 규칙, 이동과 복사(Copy) 의 차이, 함수에 값 넘기기·반환하기까지 긴 글로 풀어서 정리합니다. 다음 두 글을 읽었다면 소유권 → 빌림 → 라이프타임 순서로 이어져 이해하기 쉽습니다.

 

2026.02.22 - [프로그래밍 PROGRAMMING/기타 ETC] - [RUST] Rust란 무엇인가: 메모리 안전성과 성능을 동시에 잡는 시스템 언어

2026.02.22 - [프로그래밍 PROGRAMMING/기타 ETC] - [RUST] Rust Borrow Checker와 라이프타임: 메모리 안전성


소유권의 세 가지 규칙

Rust 공식 문서에서 소유권은 다음 세 가지로 요약됩니다.

  1. 각 값에는 그 값을 소유한 변수가 정확히 하나 있습니다.
    한 값이 두 변수의 "소유"가 되지 않습니다. 대입이나 함수 인자로 넘기면 소유권이 이동합니다.
  2. 소유자가 스코프를 벗어나면, 그 값은 drop됩니다.
    별도의 freedelete 호출이 없어도, 컴파일러가 스코프 끝에서 자동으로 정리 코드를 넣습니다.
  3. 값의 "종류"에 따라 대입이 이동(move)이 되거나 복사(copy)가 됩니다.
    힙에 데이터를 두는 타입(String, Vec 등)은 기본이 이동이고, 스택에만 있는 작은 타입(i32, bool 등)은 Copy 트레이트가 있어 복사됩니다.

이 세 규칙이 지켜지기 때문에 "누가 언제 메모리를 해제할지"가 컴파일 타임에 정해지고, 이중 해제나 use-after-free가 나올 구멍이 없습니다.


이동(Move)과 복사(Copy): 한 번에 하나의 소유자

변경전: 다른 언어처럼 "복사"라고 생각했을 때

C++이나 Java에 익숙하다면 "대입하면 복사가 된다"고 생각하기 쉽습니다. Rust에서는 힙 데이터를 다루는 타입은 대입 시 복사가 아니라 소유권 이동이 일어납니다. 이동 후에는 이전 변수를 사용할 수 없습니다.

// 변경전(의도): s1을 s2에 "복사"한 뒤 s1도 쓰고 싶음 — Rust에서는 이동이라 에러
fn main() {
    let s1 = String::from("hello");
    let s2 = s1;   // s1의 소유권이 s2로 이동. s1은 더 이상 유효하지 않음
    println!("{}", s1);  // 컴파일 에러: use of moved value: `s1`
}

컴파일러 메시지(요지): s1의 소유권이 s2로 이동했기 때문에 s1은 "값을 잃은" 상태입니다. 이 상태에서 s1을 쓰면 이중 해제나 use-after-free 위험이 있으므로, Rust는 컴파일 단계에서 사용을 막습니다.


변경후: 이동을 인정하고, 필요하면 복제(clone) 또는 참조(빌림)

진짜 복사가 필요하면 clone()을 씁니다. 참조만 넘기고 싶으면 &s1처럼 빌림을 사용합니다.

// 변경후(1): 복사가 필요할 때만 clone() 사용
fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();  // 힙 데이터를 복제. s1과 s2가 각각 소유권을 가짐
    println!("{} {}", s1, s2);
}

// 변경후(2): 소유권을 넘기지 않고 읽기만 할 때는 참조(&) 사용
fn print(s: &String) {
    println!("{}", s);
}
fn main() {
    let s1 = String::from("hello");
    print(&s1);   // 소유권은 s1에 그대로, print는 빌려서 읽기만 함
    println!("{}", s1);
}

Output (변경후 1):

hello hello

Output (변경후 2):

hello
hello

 

요점: 대입·함수 인자로 넘길 때 "이동"이 기본이므로, 복사가 필요하면 clone(), 읽기만 하면 &T로 빌림을 사용합니다. 이렇게 하면 소유자가 항상 하나로 유지됩니다.


스택 타입과 Copy: 이동이 아니라 복사되는 경우

i32, u64, bool, char처럼 스택에만 있고 크기가 작은 타입은 Copy 트레이트를 구현하고 있습니다. 이런 타입은 대입해도 이동이 아니라 복사가 되어, 대입 후에도 이전 변수를 계속 쓸 수 있습니다.

// Copy 타입: 대입 후에도 x, y 둘 다 사용 가능
fn main() {
    let x: i32 = 42;
    let y = x;   // 복사. x의 소유권이 y로 이동하는 것이 아님
    println!("{} {}", x, y);
}

Output:

42 42

 

요점: "한 값에 한 소유자"는 그대로이지만, Copy 타입은 값 자체가 비트 단위로 복사되기 때문에 xy가 각각 독립된 값을 갖습니다. 따라서 이동 후 사용 금지 규칙이 적용되지 않습니다.


소유권과 함수: 전달과 반환

함수에 값을 넘기면 소유권이 함수 쪽으로 이동합니다. 함수가 값을 반환하면 소유권이 호출자에게 넘어갑니다. 이동된 변수는 그 이후로 사용할 수 없습니다.

fn take_ownership(s: String) {
    println!("함수 안: {}", s);
}  // s가 스코프를 벗어나며 drop

fn give_ownership() -> String {
    String::from("반환값")
}

fn main() {
    let s1 = String::from("hello");
    take_ownership(s1);  // s1의 소유권이 함수로 이동
    // println!("{}", s1);  // 에러: use of moved value

    let s2 = give_ownership();  // 반환된 String의 소유권이 s2로 이동
    println!("{}", s2);
}

Output:

함수 안: hello
반환값

 

요점: "넘길 때 이동, 반환할 때 이동"만 기억하면 됩니다. 읽기만 하려면 인자를 &String처럼 참조로 받으면 소유권 이동이 일어나지 않습니다.


소유권과 컬렉션·구조체

Vec, HashMap 같은 컬렉션도 소유권 규칙을 그대로 따릅니다. 컬렉션을 다른 변수에 대입하거나 함수에 넘기면 전체 컬렉션의 소유권이 이동합니다. 구조체가 다른 타입을 필드로 소유할 때도, 그 구조체를 이동하면 안에 있는 소유 데이터가 함께 이동합니다.

struct Person {
    name: String,  // Person이 name의 소유자
}

fn main() {
    let v = vec![String::from("a"), String::from("b")];
    let w = v;  // v의 소유권이 w로 이동. v는 더 이상 사용 불가

    let p = Person { name: String::from("Alice") };
    let name = p.name;  // p.name의 소유권이 name으로 이동. p.name은 사용 불가
    println!("{}", name);
}

Output:

Alice

 

요점: 컬렉션·구조체 안에 들어 있는 소유 데이터도 "한 값에 한 소유자" 규칙을 따릅니다. 부분만 빼 쓰고 싶다면 clone()이나 참조(&, &mut)를 사용하면 됩니다.


맺음말

Rust 소유권 규칙(Ownership Rules) 은 (1) 한 값에 한 소유자, (2) 소유자가 스코프를 벗어나면 값 drop, (3) 대입·전달 시 이동이 기본(단, Copy 타입은 복사)으로 요약됩니다. 이 규칙 때문에 메모리 해제 시점이 컴파일 타임에 정해지고, 이중 해제·use-after-free가 발생하지 않습니다. 값을 "복사해서 둘 다 쓰고 싶을 때"는 clone(), "읽기만 할 때"는 참조(&T, &mut T)를 쓰면 되고, 참조 사용 시에는 Borrow Checker와 라이프타임 규칙이 이어서 적용됩니다.

반응형