조용한 담장

Rust Understanding Ownership - Quick Reference 본문

Rust

Rust Understanding Ownership - Quick Reference

iosroid 2025. 12. 29. 22:37

이 글은 Rust 공식 문서 4장(Understanding Ownership)의 내용을 기반으로 빠르게 훑어볼 수 있도록 정리한 요약본입니다.

4. Understanding Ownership

Rust가 가비지 컬렉터 없이 메모리 안전성을 보장하는 핵심 메커니즘입니다.
"누가 메모리를 해제할 책임이 있는가?"를 결정하는 규칙들의 집합입니다.


4.1. What Is Ownership?

https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html

The 3 Rules

  1. Rust의 각각의 값은 해당 값의 Owner(소유자)라고 불리는 변수가 있다.
  2. 한 번에 딱 하나의 Owner만 존재할 수 있다.
  3. Owner가 스코프 밖으로 벗어나면, 값은 버려진다(Dropped).

메모리 동작: Move vs Copy

데이터 타입이 힙(Heap)을 쓰느냐 스택(Stack)을 쓰느냐에 따라 대입 연산의 동작이 다릅니다.

Case 1: Move (힙 데이터 - String 등)

힙에 저장되는 데이터는 변수 대입 시 얕은 복사(포인터 복사)가 일어나며, 기존 변수는 무효화됩니다. 이를 Move라고 합니다.

#[test]
fn test_ownership_move() {
    let s1 = String::from("hello");
    let s2 = s1; // 소유권이 s2로 이동(Move)됨.

    // println!("{}, world!", s1); // 컴파일 에러! s1은 이제 유효하지 않음 (Use of moved value)
    println!("{}, world!", s2); // 정상 작동
}

Case 2: Copy (스택 데이터 - 정수, 불리언 등)

크기가 고정된 스택 데이터는 Copy 트레이트가 구현되어 있어, 대입 시 값이 자동 복사됩니다. 기존 변수도 계속 사용 가능합니다.

#[test]
fn test_stack_copy() {
    let x = 5;
    let y = x; // x의 값이 복사되어 y에 할당됨.

    println!("x = {}, y = {}", x, y); // 둘 다 사용 가능
}

Case 3: Clone (깊은 복사)

힙 데이터를 그대로 유지하면서 새로 하나 더 만들고 싶다면 .clone()을 명시적으로 호출해야 합니다.

#[test]
fn test_deep_copy() {
    let s1 = String::from("hello");
    let s2 = s1.clone(); // 힙 메모리 데이터까지 완전히 복사 (비용 발생)

    println!("s1 = {}, s2 = {}", s1, s2); // 둘 다 사용 가능
}

Quick Tip:

  • 함수에 값을 전달할 때도 Move가 발생합니다. 값을 함수에 넘겨주고 다시 쓰고 싶다면 반환값으로 돌려받거나, 아래의 '참조(Reference)'를 써야 합니다.

4.1. What Is Ownership? - 테스트 코드(Rust Playground)
Gist


4.2 References and Borrowing

https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html

 

소유권을 넘기지(Move) 않고 값에 접근하고 싶을 때 사용합니다. & 기호를 사용합니다.

참조의 규칙 (The Rules of References)

  1. 어느 한 시점에 코드는 하나의 가변 참조자(&mut) 또는 여러 개의 불변 참조자(&) 중 하나만 가질 수 있다. (동시 존재 불가)
  2. 참조자는 항상 유효해야 한다. (Dangling Reference 불가)

불변 참조 (&T) vs 가변 참조 (&mut T)

1. 여러 개의 불변 참조 (Read-Only)

읽기만 한다면 몇 명이든 상관없습니다.

#[test]
fn test_immutable_borrow() {
    let s = String::from("hello");
    let r1 = &s; // 불변 참조 1
    let r2 = &s; // 불변 참조 2

    println!("{}, {}", r1, r2); // 문제 없음
}

2. 단 하나의 가변 참조 (Read-Write)

수정 권한은 오직 한 명에게만 주어집니다. 데이터 레이스(Data Race)를 원천 차단합니다.

#[test]
fn test_mutable_borrow() {
    let mut s = String::from("hello");

    let r1 = &mut s; 
    // let r2 = &mut s; // 컴파일 에러! (Second mutable borrow occurs here)

    r1.push_str(", world");
    println!("{}", r1);
}

3. 불변과 가변의 혼용 불가

"보고 있는데 누가 수정하면 안 된다"는 원칙입니다.

#[test]
fn test_mixed_borrow() {
    let mut s = String::from("hello");

    let r1 = &s; // 불변 참조
    let r2 = &s; // 불변 참조
    // let r3 = &mut s; // 컴파일 에러! (불변 참조가 살아있는 동안 가변 참조 불가)

    println!("{}, {}", r1, r2); 

    // NLL (Non-Lexical Lifetimes):
    // r1, r2가 여기서 마지막으로 사용되었으므로, 이 시점 이후부터는 가변 참조 가능
    let r3 = &mut s; 
    r3.push_str(" world");
    println!("{}", r3); // 최신 Rust 컴파일러는 여기서 문제 없음
}

** Quick Tip (Dangling Reference):**
함수 내에서 생성한 로컬 변수의 참조(reference)를 리턴하면 안 됩니다. 함수가 끝나면 로컬 변수는 Drop되는데, 포인터만 리턴하려 하기 때문입니다. 이럴 땐 값을 직접 리턴(Move)하세요.

4.2 References and Borrowing - 테스트 코드(Rust Playground)
Gist


4.3 The Slice Type

https://doc.rust-lang.org/book/ch04-03-slices.html

 

소유권을 갖지 않고(Borrow), 컬렉션(배열, 문자열 등)의 연속된 일부분을 참조하는 뷰(View)입니다.

문자열 슬라이스 (&str)

String의 일부분을 가리킵니다. 내부적으로 ptr(시작 위치)와 len(길이)만 저장합니다.

#[test]
fn test_string_slices() {
    let s = String::from("Hello World");

    let hello = &s[0..5]; // 인덱스 0부터 4까지
    let world = &s[6..11]; // 인덱스 6부터 10까지

    assert_eq!(hello, "Hello");
    assert_eq!(world, "World");

    // 전체 슬라이스 문법
    let slice = &s[..]; 
}

문자열 리터럴은 슬라이스다

코드에 박혀있는 문자열은 바이너리 어딘가를 가리키는 슬라이스입니다.

let s: &str = "Hello"; // s는 바이너리 데이터 영역의 특정 위치를 가리키는 슬라이스

배열 슬라이스 (&[T])

문자열뿐만 아니라 일반 배열도 슬라이싱 가능합니다.

#[test]
fn test_array_slices() {
    let a = [1, 2, 3, 4, 5];
    let slice = &a[1..3]; // [2, 3]

    assert_eq!(slice, &[2, 3]);
}

실전 팁: 함수 파라미터 설계

함수 파라미터로 문자열을 받을 때 &String보다 &str을 사용하는 것이 훨씬 유연합니다.

// Bad: String 객체의 참조만 받을 수 있음
fn bad_greet(name: &String) { /* ... */ }

// Good: String의 참조도 받고, 문자열 리터럴("...")도 받을 수 있음
fn good_greet(name: &str) { 
    println!("Hello, {}!", name);
}

#[test]
fn test_api_design() {
    let my_name = String::from("Rust");

    // bad_greet("Rust"); // 컴파일 에러 (타입 불일치)
    bad_greet(&my_name); // 가능

    good_greet("Rust");   // 리터럴 가능 (&str)
    good_greet(&my_name); // String 참조 가능 (Deref Coercion에 의해 &String -> &str 자동 변환)
    good_greet(&my_name[0..1]); // 슬라이스 가능
}

4.3 The Slice Type - 테스트 코드(Rust Playground)
Gist

Comments