본문 바로가기

Rust

[Rust] 소유권, 참조와 빌림, 슬라이스

안녕하세요.

Rust의 소유권과 참조에 대해서 알아보도록 하겠습니다.


서론

Rust의 가장 중요한 특징은 메모리 안정성을 보장하면서도 GC(가비지 컬렉터) 없이 동작합니다.

이를 가능하게 하는 핵심 개념이 소유권, 참조와 빌림, 슬라이스 입니다.


💿 소유권 (Ownership)

🔹 개념
Rust에서는 heap에 저장되는 값은 특정한 변수 하나만이 소유권을 가집니다.
소유권을 가진 변수는 해당 값이 유효한 범위를 결정하며, 소유권이 사라지면 값도 메모리에서 해제됩니다.
🔹 소유권 규칙
- 한 번에 딱 하나의 owner만 존재할 수 있습니다.
- owner가 scope 밖으로 벗어나는 때, 값은 버려집니다. (자동 반납 지점)
- 값을 참조하는 경우, 원본 값을 변경할 수 없습니다. (기본적으로 불변 참조)
fn main() { // s는 유효하지 않음 (선언 전이므로)
    let s = "Hello world!"; // s는 이 지점부터 유효함

    println!("{}", s); // s를 가지고 무언가를 함
} // 이 스코프는 이제 끝이므로, s는 더이상 유효하지 않음

ex) 소유권 이동

소유권이 s1에서 s2로 이동했기에 더 이상 s1을 사용할 수 없습니다. (컴파일 에러 발생)

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

    // s2로 소유권을 이동
    let s2 = s1;

    // s1은 소유권을 상실했기 때문에 s1에 접근하는 순간 컴파일 에러 발생
    println!("{}", s1);
}

 

Rust의 String은 힙 메모리를 사용합니다.

기본적으로 복사가 아니라, 소유권이 새로운 변수로 이동하여 중복 해제 오류를 방지합니다.


ex) 값 복사

Clone을 사용하면 소유권 이동 없이, 값 복사를 할 수 있습니다.

fn main() {
    let s1 = String::from("hello world");
    let s2 = s1.clone(); // 깊은 복사 (독립적인 인스턴스)

    println!("s1 = {}, s2 = {}", s1, s2);
}

ex) 스택에 저장되는 타입들

이러한 타입들은 자동으로 copy 가능하여 소유권이 이동하지 않습니다.

  • u32와 같은 정수형 타입
  • bool 타입
  • f64와 같은 부동 소수점 타입
  • copy가 가능한 타입만으로 구성된 튜플
    • (i32, i32)는 가능
    • (i32, String)은 불가능
fn main() {
    let x = 5;
    let y = x; // copy가 일어남

    println!("{}", x); // 컴파일 에러가 안남
    println!("{}", y);
}

소유권과 함수

함수에 parameter로 넘겨주는 것도 소유권을 잃습니다.

fn main() {
    let s = String::from("hello world"); // s가 스코프 안으로 들어옴

    takes_ownership(s); // s의 값이 함수 안으로 이동

    println!("{}", s); // 더 이상 유효하지 않음 -> 컴파일 에러
}

fn takes_ownership(some_string: String) {
    println!("{}", some_string);
} // some_string이 스코프 밖으로 벗어남 -> 메모리 해제됨

 

이럴 때 참조를 넘겨서 소유권을 빌려주면 main에서 다시 사용가능합니다.


💿 참조와 빌림 (Reference & Borrowing)

🔹 개념
소유권을 넘기지 않고도 값을 사용할 수 있는 방법
🔹 참조의 두 가지 종류 
1) 불변 참조 (&T) → 원본 데이터를 변경할 수 없음 (여러 개 가능)
2) 가변 참조 (&mut T) → 원본 데이터를 변경 가능 (단, 동시에 하나만 존재 가능)

1) 불변 참조

s1의 소유권은 이동하지 않기 때문에 main 함수에서 계속 사용할 수 있습니다.

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

    let len = calculate_length(&s1);

    println!("The length of '{}' is {}.", s1, len); // 사용 가능
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

2) 가변 참조

참조로 넘긴 변수를 수정하고 싶을 때 사용합니다.

fn main() {
    let mut s = String::from("hello");

    change(&mut s);

    println!("{}", s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world"); // 변경 가능
}

가변 참조의 제약

Rust에서는 데이터 레이스를 방지하기 위해 다음 규칙이 적용됩니다.

  1. 하나의 스코프 안에서 여러 개의 가변 참조(&mut T)를 만들 수 없음
  2. 하나의 스코프 안에서 불변 참조(&T)와 가변 참조(&mut T)를 동시에 사용할 수 없음

 

아래 코드는 동시에 두 개의 가변 참조를 가지므로 컴파일 오류가 납니다.

fn main() {
    let mut s = String::from("hello");

    let r1 = &mut s;
    let r2 = &mut s; // 컴파일 에러

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

 

아래 코드는 불변, 가변 참조자를 동시에 가지므로 컴파일 오류가 납니다.

fn main() {
    let mut s = String::from("hello");

    let r1 = &s;
    let r2 = &s;
    let r3 = &mut s; // 컴파일 에러

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


💿 슬라이스 (Slice)

🔹 개념
컬렉션의 일부를 참조하는 데이터 타입입니다.
소유권을 가지지 않으면서 데이터의 일부분을 안전하게 사용할 수 있도록 합니다.
fn main() {
    let s = String::from("hello world");

    let hello = &s[0..5]; // 부분 문자열 참조
    let world = &s[6..11];

    println!("{}, {}", hello, world);
    println!("{}", s); // 사용 가능
}

 

슬라이스는 원본 데이터의 일부를 참조하지만, 소유권을 가지지 않습니다.


 

🔹 정리

개념 설명 특징
소유권 (Ownership) 모든 값은 하나의 소유자가 있음 소유권이 이동하면 원래 변수는 사용할 수 없음
참조 & 빌림 (Borrowing) 값을 소유권 이동 없이 사용할 수 있음 불변 참조(&T): 여러 개 가능,
가변 참조(&mut T): 동시에 하나만 가능
슬라이스 (Slice) 데이터의 일부를 참조하는 타입 &str, &[T] 등, 원본을 변경하지 않음