소유권(OwnerShip)이란?

  • 러스트에서 프로그램의 메모리 관리법을 지배하는 규칙 모음
    • 규칙 1. 소유자가 스코프에서 벗어날 때, 값은 버려짐(dropped)
    • 규칙 2. 한 값의 소유자는 동시에 여럿 존재할 수 없음
    • 규칙 3. 각각의 값은 소유자(owner)가 정해져 있음
  • 소유권 규칙에 위배되면 컴파일 에러를 발생시킴으로써 안정성 확보
  • 러스트가 메모리 안정성을 확보하기 위한 핵심적인 문법 요소
    • 다른 언어들과 비교되는 메모리 관리 방식 (가바지 컬렉션 및 직접 관리)
  • 런타임에 동적으로 할당되는 힙 데이터를 관리하는 것이 소유권 규칙의 주 목표

소유권 해제 - 스코프

  • 사용자 입력과 같이 런타임에 크기가 결정되는 값을 다루려면 변수를 동적으로 메모리에 할당하고 해제해야 함
  • 메모리 해제를 위해 GC에 의존하거나 개발자가 직접 변수가 유효하지 않은 시점을 찾아 해제해야 함
  • 직접 메모리를 제어하는 경우, 메모리 해제 시점을 너무 일찍 잡으면 유효하지 않은 변수가 생기고 늦게 잡으면 메모리 낭비가 발생
  • Rust는 변수가 소속된 스코프를 벗어나면 자동으로 메모리를 해제하는 방식으로 이 문제를 해결
    • 변수가 스코프를 벗어날 때, 내부적으로 drop메서드를 호출함으로써 메모리 해제되며 이것을 소유권이 해제되었다고 표현
    • 즉, 스코프를 벗어난 변수를 사용하려하면 컴파일 에러 발생
fn main() {
    {   // 변수 x는 이 블럭 내에서만 유효
        let x = String::from("Hello");
    }   // 변수 x가 이 블럭을 벗어나면 메모리 해제, 내부적으로 drop 메서드 호출

    x.str_push("world"); // 컴파일 에러 발생!
}

소유권의 이동

  • 어떤 변수의 값을 다른 변수에 할당할 때, 레퍼런스가 복사 될 뿐만 아니라 '소유권'이 이동한다.
  • 소유권이 이동하면 소유권이 사라진 변수는 해당 변수가 참조하는 데이터에 접근할 수 없다.
  • 스택 영역의 레퍼런스만 복사할 뿐, 힙 영역의 실제 데이터는 복사하지 않음
    • 효율적인 메모리 사용과 런타임 성능을 가져가면서 의도치 않은 데이터 공유를 방지할 수 있음
  • 즉, Rust의 소유권 이동 규칙은 레퍼런스로 접근하는 값을 대상으로 적용됨
  • 레퍼런스를 복사할 뿐만 아니라 기존 변수를 무효화하기 때문에 '얕은 복사'와는 다르다.
fn main() {
    let x = String::from("hello");
    let y = x;

    println!("x -> {x}"); // 컴파일 에러 발생, x에 할당된 값의 소유권이 이동했기 때문
    println!("y -> {y}");
}

소유권 이동 규칙에 영향을 받지 않는 경우

  • 기본 데이터 타입이나 고정 크기 배열과 같은 Copy가 가능한 타입
    • 모든 정수형, 부동 소수점 타입
    • 논리 자료형(bool), 문자형(char)
    • Copy 가능 타입으로만 구성된 튜플 및 고정 크기 배열
  • 이러한 타입은 컴파일 타임에 크기를 알 수 있으므로 값 자체가 스택 영역에 저장되기 때문이다
  • 스택 영역의 데이터는 레퍼런스로 접근하지 않기 때문에 해당 값이 저장된 변수가 다른 변수에 할당될 때 값 그 자체가 '복사'된다
  • 따라서, 소유권 이동 규칙의 대상이 되지 않는다

소유권 이동 규칙이 적용되지 않는다면?

  • xy에 할당되면, 두 변수의 레퍼런스는 같은 곳("hello")을 참조한다.
  • 두 변수가 스코프를 벗어날 때, 각각 메모리를 해제하면 중복 해제(double free)에러가 발생
  • 이는 메모리 안정성 버그 중 하나이며, 보안을 취약하게 만드는 메모리 손상의 원인

소유권 이동이 아닌, 값 자체를 복사하고 싶다면?

  • 레퍼런스 뿐만 아니라 힙 영역의 데이터까지 복사하고 싶다면 clone() 메서드를 사용하면 된다

    fn main() {
      let x = String::from("hello");
      let y = x.clone();
    
      println!("x -> {x}"); // 컴파일 성공
      println!("y -> {y}");
    }

함수와 소유권

  • 변수가 함수의 인자로 전달되면 소유권이 이동한다.
  • 함수의 반환값이 외부 변수에 저장되면 소유권이 이동한다.
  • 힙 영역에 데이터를 갖는 변수가 스코프를 벗어나면 사전에 해당 데이터에 대한 소유권이 다른 변수로 이동하지 않는 이상drop에 의해 데이터는 제거된다.
fn main() {
    let str_1 = String::from("yjk");      
    let str_2 = gives_and_takes_ownership(str_1); // 1. str_1의 소유권이 이 함수의 매개변수인 name으로 이동한다. 이 라인 이후로 str_1은 무효화된다.
    // 5. gives_and_takes_ownership() 함수로부터 반환된 result의 소유권이 str_2로 이동했다.

    println!("{str_2}"); // 6. 변수 str_2는 소유권을 갖고 있으므로 유효한 변수다.
}

fn gives_and_takes_ownership(name: String) -> String { // 2. 이 함수의 스코프를 벗어나면 name의 소유권이 해제된다.
    let result = format!("{name}, Hello"); // 3. 변수 result를 선언함으로써 소유권이 발생하고 이 스코프 내에서 유효하다.

    result // 4. 변수 result를 반환함으로써 이 함수의 호출자쪽으로 result의 소유권이 이동한다.
}

+ Recent posts