[Rust 시리즈 5편] ownership 기초

English version

1~4편에서 문법과 흐름을 익혔다면, 이제 Rust가 왜 까다롭게 느껴지는지 직접 마주할 차례입니다. ownership은 "값이 누구에게 속하고, scope이 끝나면 누가 정리하는가"를 추적하는 규칙으로, 러스트 컴파일러가 메모리 안전성을 보장하는 핵심입니다. 5편에서는 move, scope, drop을 단순한 예제와 함께 살펴보고, 6편에서 다룰 borrowing에 대한 기대감을 만들어 둡니다.

이번 글에서 새로 나오는 용어

  1. scope: 중괄호 블록 등에서 변수가 살아 있는 범위를 의미합니다.
  2. move: 값의 소유권이 한 변수에서 다른 변수로 이동하는 행위로, 원래 소유자는 더 이상 값을 사용할 수 없습니다.
  3. drop: 변수가 scope을 벗어날 때 Rust가 자동으로 호출해 자원을 정리하는 동작입니다.
  4. Copy 트레이트: 정수처럼 가벼운 타입이 move 대신 비트 단위 복사를 허용하도록 표시하는 특성입니다.

핵심 개념

  • Rust의 모든 값은 정확히 하나의 owner를 가지며, owner가 scope을 벗어나면 값이 정리됩니다.
  • String 같은 힙 데이터를 다루는 타입은 기본적으로 move semantics를 따릅니다. 따라서 다른 변수로 대입하면 기존 변수를 더 이상 사용할 수 없습니다.
  • Copy 트레이트를 구현한 타입은 move 대신 복사가 일어나므로, 정수·불리언·문자 등은 자유롭게 대입해도 됩니다.
  • 함수에 인자를 전달하거나 반환할 때도 ownership 이동이 발생하므로, 함수 경계에서 값의 생명주기를 추적해야 합니다.

여기서 초보자가 가장 많이 묻는 질문은 "왜 i32는 괜찮고 String은 안 되지?"입니다. 지금 단계에서는 아래처럼 이해하면 충분합니다.

  • i32 같은 값은 크기가 고정되어 있어 복사 비용이 작고 단순합니다.
  • String은 힙 메모리를 가리키는 데이터를 포함해 더 조심스럽게 다뤄야 합니다.
  • 그래서 Rust는 String을 기본적으로 move하고, 정수처럼 가벼운 타입은 Copy를 허용합니다.

코드로 따라하기

1. scope과 drop

fn main() {
    {
        let name = String::from("Mathbong");
        println!("안녕, {name}");
    } // 여기서 name이 drop되어 메모리가 해제됩니다.

    // println!("{name}"); // 컴파일 오류: scope 밖에서 사용 불가
}

블록이 끝나는 순간 name은 더 이상 유효하지 않습니다. Rust는 drop을 자동 호출해 힙 메모리를 정리합니다.

2. move와 Copy

fn main() {
    let original = String::from("ownership");
    let moved = original; // move 발생

    // println!("{original}"); // 컴파일 오류
    println!("moved = {moved}");

    let x = 10;
    let y = x; // Copy 발생 (i32는 Copy)
    println!("x = {x}, y = {y}");
}

String은 힙 데이터를 가리키는 포인터를 내부에 포함하므로, 단순 복사가 아니라 소유권 이동이 일어납니다. 반면 i32Copy라서 두 변수 모두 안전하게 사용할 수 있습니다.

3. 함수 경계에서의 ownership

fn main() {
    let title = String::from("Rust 101");
    takes_ownership(title);
    // println!("{title}"); // 이미 move됨

    let score = 95;
    makes_copy(score);
    println!("score 여전히 사용 가능: {score}");

    let returned = gives_back();
    println!("돌아온 값: {returned}");
}

fn takes_ownership(text: String) {
    println!("소유권을 받은 함수: {text}");
} // text가 drop됨

fn makes_copy(number: i32) {
    println!("Copy 받은 함수: {number}");
} // number는 Copy라서 원본 사용 가능

fn gives_back() -> String {
    let note = String::from("return으로 소유권 반환");
    note
}

함수에 String을 전달하면 소유권이 함수 매개변수로 이동합니다. 따라서 함수 호출 뒤 원래 변수는 더 이상 사용할 수 없습니다. 값을 다시 사용하려면 반환하거나, 다음 편에서 배울 borrowing을 사용해야 합니다.

4. move를 피하기 위한 조합

fn main() {
    let text = String::from("hello");
    let (len, text) = calculate_length(text);
    println!("{text}의 길이는 {len}");
}

fn calculate_length(input: String) -> (usize, String) {
    let length = input.len();
    (length, input)
}

밑바닥 방법으로는 튜플을 사용해 길이와 문자열을 함께 반환할 수 있습니다. 하지만 이 방법은 번거롭기에, 6편에서 참조(&)로 빌려쓰는 패턴을 배울 예정입니다.

왜 중요한가

  • ownership은 Rust가 가비지 컬렉터 없이도 메모리를 안전하게 관리하는 이유입니다.
  • move와 scope을 이해하면 컴파일 오류가 발생했을 때 메시지를 빠르게 해석하고, 어떻게 소유권을 다시 확보할지 전략을 세울 수 있습니다.
  • 함수 경계에서 ownership을 추적할 수 있어야 borrowing, 슬라이스, 구조체, 컬렉션을 다룰 때 실수하지 않습니다.

CodeSandbox로 이어서 실습하기

아래 샌드박스는 CodeSandbox의 Rust starter입니다. 이번 글의 핵심 코드를 src/main.rs에 옮기고, cargo checkcargo run 결과를 나란히 보면서 컴파일 메시지와 실행 출력을 비교해 보세요.

Live Practice

Rust Practice Sandbox

CodeSandbox

Run the starter project in CodeSandbox, compare it with the lesson code, and keep experimenting.

Rust startercargoterminal
  1. starter를 fork한 뒤 src/main.rs를 연다
  2. 본문 예제를 붙여 넣고 cargo check와 cargo run을 차례로 실행한다
  3. 타입, 값, 참조 흐름을 바꿔 컴파일 피드백과 출력 차이를 비교한다

Rust 실습은 브라우저 미리보기보다 터미널 피드백이 더 중요합니다. 여러 파일 구조나 추가 crate가 필요한 예제는 파일 배치를 조금 더 손봐야 할 수 있습니다.

실습

  1. String을 두 번 출력하려다 실패하는 코드를 직접 작성하고, 컴파일러 오류 메시지를 기록해 어떤 ownership 규칙이 깨졌는지 설명해 봅니다.
  2. fn takes_and_returns(value: String) -> String 함수를 만들어, 소유권을 가져갔다가 다시 돌려주는 패턴을 연습합니다.
  3. Copy가 적용되지 않는 타입(Vec<i32>, String)과 적용되는 타입(u8, bool)을 비교하는 짧은 프로그램을 작성합니다.
  4. 블록 내부에서 생성한 값이 drop되는 시점을 로그로 출력하기 위해 println!을 배치해, scope 종료 시점을 시각적으로 확인합니다.

마무리

ownership의 기본 규칙은 "값은 하나의 owner만 가진다"와 "scope이 끝나면 drop된다"는 두 문장으로 정리됩니다. 이번 편에서 move와 함수 경계의 동작을 충분히 실험해 봤다면, 다음 6편에서 다룰 borrowing과 참조 규칙이 훨씬 쉽게 다가올 것입니다.

💬 댓글

이 글에 대한 의견을 남겨주세요