💻 프로그래밍

Ownership

소유권

Rust의 핵심 메모리 관리 개념. 각 값은 하나의 소유자만 가지며, 스코프를 벗어나면 자동 해제. GC 없이 메모리 안전성 보장.

📖 상세 설명

Ownership(소유권)은 Rust 프로그래밍 언어의 가장 핵심적이고 독창적인 개념입니다. 메모리 관리를 컴파일 타임에 검증하여 런타임 오버헤드 없이 메모리 안전성을 보장합니다. C/C++의 수동 메모리 관리로 인한 버그(dangling pointer, double free, use-after-free)와 GC 언어의 성능 문제를 동시에 해결하는 혁신적인 접근 방식입니다.

Ownership의 세 가지 규칙

  1. Each value has a single owner - Rust의 모든 값은 단 하나의 변수(소유자)만 가질 수 있습니다.
  2. Scope determines lifetime - 소유자가 스코프(scope)를 벗어나면 값은 자동으로 drop(해제)됩니다.
  3. Move semantics by default - 값을 다른 변수에 할당하면 소유권이 이동(move)하고, 원래 변수는 더 이상 사용할 수 없습니다.

Borrowing(빌림)과 References(참조)는 Ownership을 보완하는 개념입니다. 소유권을 이전하지 않고 값을 임시로 빌려 사용할 수 있습니다. 불변 참조(&T)는 여러 개가 동시에 존재할 수 있지만, 가변 참조(&mut T)는 한 번에 하나만 허용됩니다. 이 규칙은 data race를 컴파일 타임에 방지합니다.

Ownership 시스템의 가장 큰 장점은 Garbage Collector 없이 메모리 안전성을 보장한다는 것입니다. GC가 없어 예측 가능한 성능과 낮은 메모리 오버헤드를 제공하며, 시스템 프로그래밍, 임베디드, 게임 엔진 등 성능이 중요한 분야에서 각광받고 있습니다. Borrow checker가 컴파일 시점에 모든 메모리 접근을 검증하므로 런타임 크래시가 발생하지 않습니다.

💻 코드 예제

// === Ownership 기본 예제 ===

fn main() {
    // 1. 기본 Ownership: String은 힙에 할당됨
    let s1 = String::from("hello");
    let s2 = s1;  // s1의 소유권이 s2로 이동(move)
    // println!("{}", s1);  // 컴파일 에러! s1은 더 이상 유효하지 않음
    println!("{}", s2);  // OK: "hello"

    // 2. Clone: 깊은 복사로 소유권 유지
    let s3 = String::from("world");
    let s4 = s3.clone();  // 힙 데이터까지 복사
    println!("s3: {}, s4: {}", s3, s4);  // 둘 다 유효

    // 3. Copy trait: 스택 데이터는 자동 복사
    let x = 5;
    let y = x;  // i32는 Copy trait 구현, x도 여전히 유효
    println!("x: {}, y: {}", x, y);  // 둘 다 유효

    // 4. 함수와 Ownership
    let s5 = String::from("rust");
    takes_ownership(s5);  // s5의 소유권이 함수로 이동
    // println!("{}", s5);  // 컴파일 에러!

    let s6 = gives_ownership();  // 함수로부터 소유권 받음
    println!("받은 값: {}", s6);
}

fn takes_ownership(s: String) {
    println!("소유권 받음: {}", s);
}  // s가 drop됨

fn gives_ownership() -> String {
    String::from("새로운 문자열")  // 소유권 반환
}

// === Borrowing과 References ===

fn borrowing_example() {
    let s1 = String::from("hello");

    // 불변 참조 (Immutable borrow)
    let len = calculate_length(&s1);  // 빌려주기만 함
    println!("'{}' 길이: {}", s1, len);  // s1 여전히 유효

    // 가변 참조 (Mutable borrow)
    let mut s2 = String::from("hello");
    change(&mut s2);
    println!("변경됨: {}", s2);  // "hello, world"

    // 참조 규칙: 불변 참조 여러 개 OK
    let r1 = &s1;
    let r2 = &s1;
    println!("{} {}", r1, r2);

    // 가변 참조는 단 하나만!
    let mut s3 = String::from("test");
    let r3 = &mut s3;
    // let r4 = &mut s3;  // 컴파일 에러! 동시에 두 개의 가변 참조 불가
    println!("{}", r3);
}

fn calculate_length(s: &String) -> usize {
    s.len()  // 참조로 받았으므로 소유권 이동 없음
}

fn change(s: &mut String) {
    s.push_str(", world");  // 가변 참조로 수정 가능
}

🗣️ 실무 대화 예시

Rust 개발자 기술 면접에서

"Ownership은 Rust가 GC 없이 메모리 안전성을 보장하는 핵심 메커니즘입니다. 각 값은 단 하나의 소유자만 가지고, 소유자가 스코프를 벗어나면 자동으로 drop됩니다. Borrowing을 통해 소유권 이전 없이 값을 참조할 수 있는데, 불변 참조는 여러 개가 가능하지만 가변 참조는 동시에 하나만 허용됩니다. 이 규칙으로 data race를 컴파일 타임에 방지합니다."

GC vs Ownership 기술 토론에서

"GC는 개발 편의성은 높지만 Stop-the-world 이슈로 실시간 시스템에 부적합합니다. Rust의 Ownership은 컴파일 타임에 메모리를 추적하니까 런타임 오버헤드가 없어요. 물론 borrow checker와 싸우느라 초기 러닝 커브가 높지만, 메모리 버그 없이 C++ 수준 성능을 얻을 수 있으니 시스템 프로그래밍에는 최적이죠. 저희 팀은 성능 크리티컬한 마이크로서비스를 Rust로 재작성해서 메모리 사용량을 60% 줄였습니다."

팀 코드 리뷰 중

"여기 String을 함수에 넘길 때 clone() 쓰셨는데, 읽기만 하면 &str 참조로 빌려주세요. 불필요한 힙 할당이 줄어들어요. 그리고 이 루프에서 Vec을 반환할 때 소유권이 이동하니까 다음 이터레이션에서 에러 날 거예요. iter() 대신 into_iter() 쓰거나, 참조로 처리하시는 게 맞습니다."

⚠️ 주의사항

1
Move vs Copy 구분

정수, 부동소수점, bool, char 등 스택에 저장되는 타입은 Copy trait을 구현하여 자동 복사됩니다. 그러나 String, Vec, Box 등 힙 데이터를 가진 타입은 Move됩니다. Copy 가능 여부를 혼동하면 예상치 못한 컴파일 에러가 발생합니다.

2
Lifetime 이해 필수

참조를 반환하거나 구조체에 저장할 때 Lifetime annotation('a)이 필요합니다. Lifetime은 참조가 유효한 범위를 명시하여 dangling reference를 방지합니다. 처음에는 복잡해 보이지만, 컴파일러 메시지를 따라가면 점차 이해할 수 있습니다.

3
Borrow Checker 에러 대처법

borrow checker 에러가 발생하면 당황하지 말고 에러 메시지를 자세히 읽으세요. 대부분 소유권을 clone()으로 복사하거나, 참조 범위를 조정하거나, RefCell/Rc로 런타임 빌림 검사로 전환하면 해결됩니다. 무조건 clone()보다는 설계를 다시 검토하는 것이 좋습니다.

🔗 관련 용어

📚 더 배우기