소유권

전지현·2024년 8월 13일
0

Rust

목록 보기
5/17

소유권

  • 러스트 프로그램 메모리 관리법

    러스트는 '소유권(ownership)' 이라는 시스템을 만들고, 컴파일러가 컴파일 중에 검사할 여러 규칙을 정해 메모리를 관리하는 방식을 채택했다.

  • 규칙 중 하나라도 위반하면 프로그램은 컴파일되지 않으며, 소유권의 어떠한 특성도 프로그램 실행 속도를 느리게 하지는 않는다.

  • 소유권의 주요 목표가 힙 데이터의 관리




소유권 규칙

  • 러스트에서 각각의 값은 소유자 (owner) 가 정해져 있다.
  • 한 값의 소유자는 동시에 여럿 존재할 수 없다.
  • 소유자가 스코프 밖으로 벗어날 때, 값은 버려진다. (dropped)

변수의 스코프

스코프란?
프로그램 내에서 아이템이 유효한 범위
1. 변수가 스코프 내에 나타나면 유효하다.
2. 유효기간은 스코프 밖으로 벗어나기 전까지이다.

  • 러스트는 문자열 타입 String을 제공한다. (String은 힙에 할당된다)
let s = String::from("hello");
  • 문자열 리터럴과 다르게 String은 가변이라 값을 변경할 수 있다.
let mut s = String::from("hello");
s.push_str(", world!");  // push_str()이 문자열에 리터럴을 추가한다.
pritnln!("{}", s); //'hello, world!'를 출력.

=> 문자열 리터럴은 컴파일 타임에 내용을 알 수 있으므로 텍스트가 최종 실행파일에 하드코딩 된다. 이 방식은 빠르고 효율적이지만, 문자열이 변하지 않을 경우에만 사용할 수 있다.
반면 String 타입은 힙에 메모리를 할당하는 방식을 사용하기 때문에 텍스트 내용 및 크기를 변경할 수 있다.

하지만 이는 다음을 의미하기도 한다:

1. 실행 중 메모리 할당자로부터 메모리를 요청해야 한다.
2. String 사용을 마쳤을 때 메모리를 해제할 (즉, 할당자에게 메모리를 반납할) 방법이 필요하다.

  1. String::from 호출 시 필요한 만큼 메모리를 요청하도록 구현되어 있다.
  2. 러스트에서는 이 문제를 변수가 자신이 소속된 스코프를 벗어나는 순간 자동으로 메모리를 해제하는 방식으로 (allocate - free) 해결했다.
    2-1. 러스트는 변수가 스코프 밖으로 벗어나면 drop이라는 함수를 호출한다. 이 함수는 해당 타입을 개발한 개발자가 직접 메모리 해제 코드를 작성해 넣을 수 있게 되어 있으며, 이 경우 String을 개발한 사람이 작성한 메모리 해제 코드가 실행될 것. drop은 닫힌 중괄호(})가 나타나는 지점에서 자동으로 호출된다.

==> 러스트에서는 얕은 복사 대신 이동이다!!



에러 발생하는 코드)

let s1 = String::from("hello");
let s2 = s1;

println!("{}, world!", s1);

s1, s2 두 포인터는 같은 메모리를 가리키기 때문에 s1, s2 가 스코프 밖으로 벗어날 때 각각 메모리를 해제하게 되면 중복 해제 에러가 발생한다.
메모리 안정성을 보장하기 위해 러스트는 let s2 = s1; 라인 뒤로는 s1이 더 이상 유효하지 않다고 판단하며 이로써 러스트는 s1이 스코프를 벗어나더라도 아무것도 해제할 필요가 없다.
이렇게 러스트에서는 얕은 복사 시 기존의 변수를 무효화하기 때문에 이를 얕은 복사가 아닌 이동이라 하고 앞선 코드는 s1이 s2로 이동되었다라고 표현한다.


String의 힙 데이터까지, 깊은 복사를 하고 싶을 땐 clone 메서드 사용.

let s1 = String::from("hello");
let s2 = s1.clone();

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



String과 다르게 정수형 등 컴파일 타임에 크기가 고정되는 타입은 모두 스택에 저장되기 때문에 이럴 경우 깊은 복사와 얕은 복사 간에 차이가 없다. (clone을 호출한거나 안한거나 차이가 없다)

  • (정수형처럼) 스택에 저장되는 타입 목록 (= Copy trait 사용 가능)
    • 모든 정수형 타입 (ex. u32)
    • true, false 값을 갖는 논리 자료형 bool
    • 모든 부동 소수점 타입 (ex. f64)
    • 문자 타입 char
    • Copy 가능한 타입만으로 구성된 튜플 (ex. (i32, i32) Copy는 가능. (i32, String) 은 불가능.

: 일반적으로 단순한 스칼라 값의 묶음은 Copy 가능하고, 할당이 필요하거나 리소스의 일종인 경우엔 불가능하다.

예시)

fn main() {
    let s1 = gives_ownership();         // gives_ownership이 자신의 반환 값을 s1로
                                        // 이동시킵니다

    let s2 = String::from("hello");     // s2가 스코프 안으로 들어옵니다

    let s3 = takes_and_gives_back(s2);  // s2는 takes_and_gives_back로 이동되는데,
                                        // 이 함수 또한 자신의 반환 값을 s3로
                                        // 이동시킵니다
} // 여기서 s3가 스코프 밖으로 벗어나면서 버려집니다. s2는 이동되어서 아무 일도
  // 일어나지 않습니다. s1은 스코프 밖으로 벗어나고 버려집니다.

fn gives_ownership() -> String {             // gives_ownership은 자신의 반환 값을
                                             // 자신의 호출자 함수로 이동시킬
                                             // 것입니다

    let some_string = String::from("yours"); // some_string이 스코프 안으로 들어옵니다

    some_string                              // some_string이 반환되고
                                             // 호출자 함수 쪽으로
                                             // 이동합니다
}

// 이 함수는 String을 취하고 같은 것을 반환합니다
fn takes_and_gives_back(a_string: String) -> String { // a_string이 스코프 안으로
                                                      // 들어옵니다

    a_string  // a_string이 반환되고 호출자 함수 쪽으로 이동합니다
}



!! 상황은 다양할지라도, 변수의 소유권 규칙은 언제나 동일하다. 어떤 값을 다른 변수에 대입하면 값이 이동하고, 힙에 데이터를 갖는 변수가 스코프를 벗어나면, 사전에 해당 데이터가 이동하여 소유권이 다른 변수에 이동되지 않는 이상 drop에 의해 데이터가 제거된다.

  • 함수가 값을 사용할 수 있도록 하되 소유권은 가져가지 않도록 하고 싶은 경우 (함수에 넘겨줄 값을 함수 호출 이후에도 쓰고 싶은 경우)
    => 튜플을 사용하여 여러 값을 반환하는 게 가능하다
fn main(){
    let s1 = String::from("hello");

    let (s2, len) = calculate_length(s1);

    println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len();  // len은 String의 길이를 반환

    (s, length)
}

하지만 러스트에는 참조자 (reference) 기능이 있어서 소유권 이동 없이 값을 사용할 수 있다!




https://www.inflearn.com/course/%EC%B0%A8%EC%84%B8%EB%8C%80-%EA%B3%A0%EC%84%B1%EB%8A%A5-%EC%96%B8%EC%96%B4-rust-%EC%8B%A4%EC%9A%A9-%EC%9E%85%EB%AC%B8



s1과 s2가 힙메모리(헬로의) 주소를 동시에 가리키게 되고 (s1, s2는 포인터 변수임.)
소유권 규칙으로 인해 s1은 해제되고 s2만 헬로를 가리키게 된다.

fn main() {
    {
        let s1 = String::from("헬로");  // Heap
        let s2 = s1;   // 소유권 이전. "헬로" 문자열을 s1이 아닌 s2가 갖게 됨.
        println!("s2 = {}", s2);
        println!("s1 = {}", s1);   // 에러. s1은 사용할 수 없음.
    }
    // 블록을 벗어나면 s2도 사용 불가.
}




소유권 이전 없이 s1, s2 둘 다 사용하고 싶다면 clone을 사용하면 된다.

fn main() {
    let s1 = String::from("헬로");
    println!("s1 = {}", s1);
    let s2 = s1.clone();  // 데이터를 복사해서 s1,s2 둘 다 사용하고 싶다면 clone 사용. 힙 메모리 자체도 복사됨.
    println("s1 = {}", s1);
}




기본 데이터 타입의 경우 (Stack으로만 저장되는 값) Copy가 된다. (별도로 clone 없이도 복사가 된다)
소유권 개념이 없다고 봐도 무방하다.

fn main() {
    let x = 3;
    let y = x;
}




기본 데이터 타입

튜플 같은 경우 튜플 안에 있는 값이 모두 기본형이면 스택에 잡히고 튜플 안에 String과 같이 힙에 저장되는 타입이 껴 있다면 스택에 잡히지 않는다. (힙으로 관리된다)




함수 호출과 동시에 (힙에 있는)매개변수 전달 시 소유권도 넘어간다.
(기본 데이터의 경우는 넘어가지 않는다. 양쪽에서 모두 사용이 가능하다. (소유권 관리 없이 복사가 일어나기 때문))

fn main () {
    let s = String::from("헬로");
    string_length(s);  // 함수 호출과 동시에 s의 소유권이 메인 함수에서 string_length 함수로 넘어간다.
    println!("s = {}", s); // 에러.
}

fn string_length(s: String){
    println!("문자열 s의 길이는: {}", s.len());
}




함수 반환값의 경우 소유권도 반환되어서 호출한 함수에게 소유권도 다시 넘어간다.

fn main() {
    let s1 = String::from("헬로");
    let s2 = string_length(s1);  // s1의 소유권이 넘어갔다가 다시 돌아온다. 
    println!("s2 = {}", s2);
}

fn string_length(s: String) -> String {
    println!("s의 길이는 = {}", s.len());
    s  // s의 소유권도 같이 반환됨.
}

0개의 댓글