RUST 소유권

Alpha, Orderly·2024년 6월 2일

RUST

목록 보기
2/11
post-thumbnail

러스트의 소유권과 메모리 관리

Rust는 가비지 컬렉터 없이도 메모리 안전성을 보장하기 위해 **소유권(ownership)**이라는 개념을 사용합니다. 이는 Rust의 핵심 개념 중 하나로, 값이 언제 생성되고 언제 해제되는지를 명확히 정의합니다.


소유권 규칙

  1. 모든 값에는 소유자가 존재한다.
  2. 값은 동시에 여러 소유자를 가질 수 없다.
  3. 소유자가 스코프를 벗어나면 값은 메모리에서 해제된다.
fn main() {
    let s = String::from("hello"); // s가 소유자
    let t = s;                      // 소유권이 t로 이동
    // println!("{}", s);           // 컴파일 에러: s는 더 이상 유효하지 않음
    println!("{}", t);
}

변수와 스코프

{
    let s = "hello"; // 이 시점부터 s는 유효합니다
    // s로 작업 가능
} // 스코프가 끝나면 s는 더 이상 유효하지 않습니다
  • 변수는 스코프를 벗어나면 자동으로 무효화됩니다.

String 타입

Rust의 String은 힙(heap)에 데이터를 저장합니다.

let s = String::from("hello");
  • 스코프를 벗어나면 drop 함수가 자동 호출되어 메모리가 해제됩니다.

이동 (Move)

let s1 = String::from("hello");
let s2 = s1; // s1 → s2로 소유권 이동
  • s1은 더 이상 사용할 수 없습니다. (얕은 복사가 아닌 이동)

복제 (Clone)

let s1 = String::from("hello");
let s2 = s1.clone();
  • s1s2 모두 사용 가능. (깊은 복사)

Copy 트레이트

  • 정수, 불리언, 부동소수점 등 스택에만 저장되는 단순 타입은 자동으로 복사됩니다.

소유권과 함수

함수로 값을 전달하는 것도 이동이 발생할 수 있습니다.

fn main() {
    let s = String::from("hello");
    takes_ownership(s); // 소유권 이동

    let x = 5;
    makes_copy(x); // Copy 타입이므로 여전히 x 사용 가능
}

fn takes_ownership(s: String) {
    println!("{}", s);
}

fn makes_copy(x: i32) {
    println!("{}", x);
}
  • 함수 반환도 소유권 이동을 발생시킬 수 있습니다.

참조와 대여 (Borrowing)

값을 이동시키지 않고 참조할 수 있습니다.

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()
}
  • &는 참조를 의미하며, 데이터 소유권을 이동시키지 않습니다.
  • 참조는 기본적으로 불변입니다.

가변 참조

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

fn change(s: &mut String) {
    s.push_str(", world");
}
  • 단, 하나의 가변 참조 또는 여러 개의 불변 참조만 동시에 존재할 수 있습니다.
  • 이는 데이터 경쟁을 방지하기 위함입니다.

Dangling Pointer (댕글링 포인터)

Rust는 해제된 메모리를 참조하는 것을 원천적으로 방지합니다.

fn dangle() -> &String {
    let s = String::from("hello");
    &s // 컴파일 에러: s는 함수 종료 시 해제됨
}

슬라이스 (Slice)

컬렉션의 일부를 참조하는 방법입니다.

let s = String::from("hello world");
let hello = &s[..5];   // "hello"
let world = &s[6..];   // "world"
  • 슬라이스는 소유권을 갖지 않고 참조만 합니다.
  • UTF-8 문자열은 반드시 문자 경계에서 슬라이스해야 합니다.

배열 슬라이스

let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
assert_eq!(slice, &[2, 3]);

예시: 아나그램 함수와 소유권

아래의 함수는 소유권과 대여 개념이 코드에 어떻게 반영되는지를 잘 보여줍니다.

use std::collections::HashSet;

pub fn anagrams_for<'a>(word: &str, possible_anagrams: &[&str]) -> HashSet<&'a str> {
    let mut result = HashSet::new();
    // 구현 내용은 생략
    result
}

소유권과의 관계

  1. 입력 인자: word: &strpossible_anagrams: &[&str]는 모두 참조 타입입니다. 이 말은 함수가 입력 문자열들의 소유권을 가져오지 않고 빌려오기만 한다는 뜻입니다. 따라서 호출자는 이 함수를 실행한 뒤에도 원래 데이터를 자유롭게 사용할 수 있습니다.

  2. 반환 값: HashSet<&'a str>는 입력된 문자열들에 대한 참조를 반환합니다. 여기서 'a 라이프타임 파라미터는 반환된 참조들이 여전히 유효한 메모리를 가리킨다는 것을 컴파일러가 보장하도록 합니다. 즉, 함수가 새로운 소유권을 만들지 않고, 기존의 데이터를 안전하게 참조만 한다는 점을 명시합니다.

## 라이프타임 'a

'a는 제네릭 라이프타임 파라미터입니다. 간단히 말하면, "이 참조는 최소한 어떤 입력 값이 살아있는 동안만 유효하다"라는 약속을 컴파일러에게 알려주는 역할을 합니다.

즉, 'a는 함수의 입력 참조와 반환 참조가 같은 생존 기간을 공유한다는 뜻입니다. 반환된 참조가 입력 데이터보다 오래 살 수 없게 보장해 주므로 안전합니다.

쉬운 예시

fn first<'a>(s: &'a str) -> &'a str {
    &s[0..1]
}

fn main() {
    let word = String::from("hello");
    let ch = first(&word); // ch는 word가 유효한 동안만 유효
    println!("{}", ch);
} // 여기서 word와 ch 모두 스코프 종료와 함께 해제

여기서 chword의 일부를 참조하기 때문에 word가 끝나면 같이 끝나야 합니다. 라이프타임 'a가 바로 이 규칙을 표현하는 도구입니다.

만약 함수 내부에서 새로운 String을 만들고 참조를 반환한다면, 그 String은 함수가 끝나는 순간 사라지므로 참조는 더 이상 유효하지 않습니다. Rust는 이런 코드를 컴파일 단계에서 막아주며, 결과적으로 댕글링 포인터 문제를 원천적으로 방지합니다.


핵심 요약

  1. 하나의 가변 참조 또는 여러 개의 불변 참조만 가능하다.
  2. 불변 참조는 마지막 사용 시점까지만 유효하다.
  3. 참조는 항상 유효한 메모리를 가리켜야 한다.
  4. 슬라이스는 컬렉션의 일부분을 소유권 없이 참조할 수 있다.
  5. anagrams_for와 같은 함수는 소유권을 넘기지 않고 참조와 라이프타임을 통해 안전하게 데이터를 활용한다.
profile
만능 컴덕후 겸 번지 팬

0개의 댓글