만자의 개발일지

[Rust] 소유권과 메모리 모델 본문

Rust

[Rust] 소유권과 메모리 모델

박만자 2025. 6. 17. 22:32

저번 포스팅에서는 패턴 매칭에 대해 알아보았습니다. 이번 포스팅에서는 소유권과 메모리 모델에 대해 알아보도록 하겠습니다.

메모리 모델(Memory Model)

Rust에서 런타임에 사용되는 메모리 영역으로는 스택이 있습니다.

스택은 데이터를 들어온 순서대로 저장하고, 역순으로 제거하는 LIFO(last in, first out) 방식입니다. 스택에는 기본 타입(i32, f64, char 등)커스텀 타입(enum, struct 등) 같이 크기가 정해져있는 데이터가 저장됩니다. 또한 스택은 데이터가 순차적으로 추가되고 제거되기 때문에 메모리 관련 오버헤드가 거의 없어 속도가 빠르다는 장점이 있습니다. 단, 컴파일 타임에 크기를 알 수 없거나, 변경될 수 있는 데이터들은 스택이 아닌 힙 메모리에 저장해야합니다.

 

힙은 스택과 달리 런타임에 크기가 변할 수 있는 동적 데이터를 저장하는 메모리입니다. 힙에는 벡터나 스마트 포인터등 컴파일 타임에 크기를 알 수 없는 데이터들이 저장됩니다. 데이터를 힙에 할당할 때 실제 값은 힙에 저장하고, 시작 주소(포인터), 길이, 용량 같은 메타데이터는 스택에 저장합니다. 힙에 저장된 데이터에 접근할 때 스택에 저장된 포인터 값을 통해 실제 데이터가 있는 곳을 찾아가는데, 이 과정으로 인해 속도가 느려진다는 단점이 있습니다. 소유권의 주요 목적은 이러한 힙의 중복된 값을 최소화하고, 쓰지 않는 힙 공간을 정리하여 힙에 저장된 값을 관리하기 위함입니다.

소유권(Ownership)

Rust는 메모리를 관리하기 위해 소유권이라는 독특한 방식을 사용합니다. 소유권이란 Rust 프로그램의 메모리를 관리하기 위한 시스템입니다. 소유권에는 프로그램의 메모리를 관리하기 위한 규칙들이 있고, 컴파일러는 컴파일 타임에 소유권 규칙을 검사하여 메모리 안정성을 보장합니다.

소유권 시스템의 가장 큰 장점은 런타임에 어떠한 영향도 미치지 않는다는 것입니다. 가비지 컬렉터(GC)를 사용하는 프로그램들은 프로그램 성능에 영향을 끼치지만, 소유권과 관련된 모든 검사는 컴파일 시점에 이루어지기 때문에 프로그램 성능에 어떠한 영향도 미치지 않습니다.

 

소유권에는 다음 세 가지 핵심 규칙이 있습니다.

  1. 각각의 값은 소유자(owner)가 정해져 있습니다.
    • 어떤 값이 메모리에 할당되면, 그 값은 특정 변수에 의해 "소유"됩니다. 해당 변수가 바로 그 값의 소유자입니다.
  2. 한 값의 소유자는 동시에 여럿 존재할 수 없습니다.
    • 하나의 값은 오직 하나의 변수만 소유할 수 있습니다. 
  3. 소유자가 스코프 밖으로 벗어날 때, 값은 해제됩니다.
    • 변수가 유요한 범위(스코프)를 벗어나면, 해당 변수가 소유하던 값은 자동으로 메모리에서 해제(dropped)됩니다.

아래 예시를 보겠습니다.

fn main() {
    {
        let s = "hello";
    }
    
    println!("{s}"); // error: cannot find value `s` in this scope
}

s를 스코프 밖에서 접근하려 했더니, 찾을 수 없는 값이라고 에러를 띄웁니다. Rust는 변수가 스코프 밖으로 벗어나면 drop 이라는 함수를 호출합니다. 위 예시에서는 중괄호가 닫힌 시점에서 drop 함수가 자동을 호출됩니다. 

또한 이 drop 함수는 후에 알아볼 트레이트 라는 문법을 이용해 개발자가 직접 커스터마이징할 수 있습니다.

 

메모리를 직접 관리해야한다면 개발자의 실수로 해제된 메모리에 접근하거나, 메모리를 두번 해제하는 등의 문제가 발생할 수도 있습니다. 하지만 Rust는 스코프를 벗어나는 순간 자동으로 메모리를 해제하는 방식을 사용함으로써 할당과 해제가 자연스럽게 1대1로 짝지어져 이러한 문제를 해결하였습니다.

 

Rust에서 소유권을 다루는 방식에는 Move(이동), Clone(복제) Copy(복사) 세 가지 방식이 있습니다.

 

Move

Move소유권을 다루는 가장 기본적인 방식입니다.

 

아래 코드를 보겠습니다.

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

s1에 문자열 데이터를 할당해주었습니다. s1hello 라는 값의 소유자가 됩니다. s1은 실제 데이터를 저장하고 있지 않습니다. 아래 이미지와 같이 실제 데이터에 대한 메타 데이터들을 스택에 저장합니다.

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

여기서 소유자는 값에 대한 주소를 직접적으로 저장하고 있는 변수를 의미합니다.

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
}

s2s1을 대입하였습니다. 이때 데이터의 복사가 일어나는데 힙에 있는 실제 데이터가 아닌 스택에 있는 메타데이터들이 복사됩니다. 그후 아래 이미지와 같이 컴파일러는 원본 변수를 유요하지 않다고 판단하고 무효화 시킵니다.

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

이는 shallow copy 방식과 유사하지만 원본 변수를 무효화 시키기 때문에, Rust에서는 shallow copy가 아닌 Move라고 표현합니다. 이처럼 Rust는 Move 방식을 통해 값에 대한 소유자를 하나로 유지하면서 이중 해제와 같은 문제를 컴파일 시점에 해결하였습니다.

 

Clone

Clone힙 데이터까지 복사(deep copy)하여 원본 변수의 소유권을 유지하면서 새로운 데이터를 만드는 방식입니다.

Clone 트레이트를 구현한 타입에 대해서만 사용 가능하며, clone() 메서드를 호출해 사용할 수 있습니다.

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();
}

Clone된 데이터는 아래 이미지와 같이 새롭게 할당된 힙 데이터를 가르키고 있습니다. 따라서 s1s2각각 다른 데이터의 소유자가 되는 것입니다.

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

Clone은 힙 메모리 할당 및 데이터 복사를 수반하기 때문에 성능적인 비용이 발생할 수 있습니다. 따라서 꼭 필요한 경우에만 사용하는 것이 좋습니다.

 

Copy

Clone이 힙에 저장된 데이터를 복사하는 거라면 Copy스택에 저장되는 데이터를 복사하는 방식입니다. Copy 역시 Copy 트레이트를 구현한 타입에 대해서만 사용 가능합니다. 정수형, 부동소수점, 문자, 불리언 등과 같은 기본타입들은 기본적으로 Copy 트레이트가 구현되어있습니다.

Copy는 별다른 메서드 호출 없이 자동으로 이루어집니다.

fn main() {
    let x = 5;
    let y = x;

    println!("x = {}, y = {}", x, y);
}

Copy데이터를 스택에 복사하여 저장합니다. 데이터를 직접 메모리에 저장하기 때문에 원본 변수를 무효화할 필요가 없습니다. Clone의 메모리 구조처럼  xy 역시 각각 다른 데이터의 소유자가 됩니다.


지금까지 소유권과 메모리 모델에 대해 알아보았습니다. 다음 포스팅에서는 소유권의 이동 없이 값을 사용할 수 있는 참조와 대여에 대해 알아보도록 하겠습니다.

 

참고

'Rust' 카테고리의 다른 글

[Rust] 제네릭  (5) 2025.07.29
[Rust] 참조와 대여  (1) 2025.07.03
[Rust] 패턴 매칭  (0) 2025.05.31
[Rust] 열거형  (0) 2025.05.18
[Rust] 구조체와 메서드 & 연관 함수  (0) 2025.05.04
Comments