UNIVERSE

Rust의 소유권 개념과 소유권 빌림

프로그래밍/Rust
작성일:

Rust의 문법은 많은 부분 C-like 스타일을 따르고 있지만, C-like 스타일을 따르는 다른 언어들과는 변수 선언에서부터 그 차이를 볼 수 있습니다. 그 차이점은 바로 변수가 담고있는 데이터의 소유권이라는 개념에 있습니다. Rust는 런타임에서 돌아가는 가비지 콜렉터(Garbage Collector)없이 컴파일 타임에 안전하게 메모리 할당 및 해제를 할 수 있도록 설계된 언어이기 때문에 이를 위해서 Rust에는 소유권이라는 생소한 개념이 도입되었습니다.

 

Rust에서 소유권에 대한 이해는 매우 중요합니다. 이 글에선 소유권의 개념소유권의 빌림(burrowing)에 대해 알아볼 것입니다. 아래의 예제를 통해 소유권에 대해 먼저 알아보도록 하겠습니다.

 

1. 소유권

struct Point(i32, i32);

struct Triangle
{
    p1 : Point,
    p2 : Point,
    p3 : Point,
}

fn main()
{
    let mut triangle = Triangle{
        p1 : Point(1,1),
        p2 : Point(1,2),
        p3 : Point(0,0)
    };

    triangle.p1 = Point(3,1);
}

 

설명을 위해 세 점을 갖는 Triangle 구조체를 만들었습니다. 이 구조체는 triangle이란 변수에 (1,1), (1,2), (0,0)의 점을 갖는 삼각형으로 초기화되었습니다. 그 후에는 찍은 점이 별로 마음에 들지 않아서 점 하나를 바꿔봤습니다. 위 예제는 아무런 문제없이 잘 작동하지만 문제는 다음과 같은 상황에서 발생하게 됩니다.

 

fn main()
{
    let mut triangle = Triangle{
        p1 : Point(1,1),
        p2 : Point(1,2),
        p3 : Point(0,0)
    };

    triangle.p1 = Point(3,1);
    
    let mut duoangle = triangle;
    
    triangle.p1 = Point(1,1);
}

 

필요에 의해 triangle을 대입한 새로운 duoangle 변수를 선언하고난 후에 마음에 안들어서 바꿨던 점 p1을 다시 바꿔보았는데 이번에는 이전에 없던 다음과 같은 에러가 생겼습니다.

 

error[E0382]: assign to part of moved value: `triangle`
  --> main.rs:22:5
	|
12	|     let mut triangle = Triangle{
	|         ------------ move occurs because `triangle` has type `Triangle`, which does not implement the `Copy` trait
...
20	|     let mut duoangle = triangle;
	|                        -------- value moved here
21	| 
22	|     triangle.p1 = Point(1,1);
	|     ^^^^^^^^^^^^^^^^^^^^^^^^ value partially assigned here after move

 

에러는 triangle이란 데이터의 소유권이 duoangle로 넘어가서 생긴 것입니다. 소유권이 넘어가면 triangle 변수는 선언된 스코프 내에 있더라도 파기됩니다. 변수 중복선언이 되는 이유가 이것 때문인지도 모르겠네요. 따라서 에러의 근본적인 이유는 예제의 맨 마지막 구문

triangle.p1 = Point(1,1);

에서 이미 파기된 triangle변수에 접근을 하고 있기 때문인 것입니다. 앞으로는 triangle의 새로운 이름인 duoangle을 이용해야 되겠네요. 그렇다고해서 데이터를 복사하는 방법이 아예 없는 것은 아닙니다. 숫자, 문자, 문자열 리터럴같이 스택에만 저장되는 데이터의 경우 값 복사에 아무런 문제가 없습니다. 반면에 struct, tuple 같은 사용자 지정 타입을 복제하는 방법은 Trait이라는 Rust의 또 다른 고유한 특징을 이용해야 하는데, 이는 글의 주제와는 맞지 않기 때문에 다음 글에서 소개하도록 하겠습니다.

 

2. 소유권의 빌림

 

상술했듯이 사용자 지정 데이터에 대해 이렇게 대입연산을 하는 것은 데이터 복사가 아닌 소유권의 이전으로 동작합니다. 그런데 대입 연산만이 소유권을 이전시키는 것은 아닙니다. 변수를 함수의 인자로 넘길 때에도 소유권의 이전이 발생하는데, 이 경우도 예제를 통해 확인해봅시다.

 

struct Point(i32, i32);

struct Triangle
{
    p1 : Point,
    p2 : Point,
    p3 : Point,
}

fn calculate_area(triangle : Triangle) -> f32
{
    let a = triangle.p1.0 as f32;
    let b = triangle.p1.1 as f32;
    let c = triangle.p2.0 as f32;
    let d = triangle.p2.1 as f32;
    let e = triangle.p3.0 as f32;
    let f = triangle.p3.1 as f32;

    f32::abs(0.5*((c-a)*(f-b)-(d-b)*(e-a)))
}

fn main()
{
    let mut triangle = Triangle{
        p1 : Point(1,1),
        p2 : Point(1,2),
        p3 : Point(0,0)
    };

    println!("주어진 삼각형의 넓이 S = {}", calculate_area(triangle));
}

 

이번 예제에서는 삼각형의 넓이를 구하고자 calculate_area()라는 함수를 만들었습니다. 함수 내용에 대해 간단히 설명하면 이 함수는 삼각형의 주어진 세 점으로 면적을 계산하는 신발끈 공식을 이용했습니다. 외적벡터 크기의 절반을 구하는 것으로도 같은 식을 유도해낼 수 있습니다. 위 예제는 어떤 에러도 없이 삼각형의 면적을 잘 출력해줍니다.

 

fn main()
{
    let mut triangle = Triangle{
        p1 : Point(1,1),
        p2 : Point(1,2),
        p3 : Point(0,0)
    };

    println!("주어진 삼각형의 넓이 S = {}", calculate_area(triangle));

    triangle.p1 = Point(2,-5); //ERROR!
}

 

문제는 이렇게 calculate_area()triangle 변수를 인자로 넘겨준 후에 발생하게 됩니다. triangle 변수는 calculate_area()의 인자로 넘겨질 때 소유권 또한 함수에 이전된 후 파기됩니다. 그렇기 때문에 위와 같이 함수를 호출한 이후 이미 파기된 triangle에 접근할 수가 없게 되는 것입니다. 이러한 문제는 바로 소유권의 빌림으로 해결할 수 있습니다.

 

fn calculate_area(triangle : &Triangle) -> f32
{
    let a = triangle.p1.0 as f32;
    let b = triangle.p1.1 as f32;
    let c = triangle.p2.0 as f32;
    let d = triangle.p2.1 as f32;
    let e = triangle.p3.0 as f32;
    let f = triangle.p3.1 as f32;

    f32::abs(0.5*((c-a)*(f-b)-(d-b)*(e-a)))
}

fn main()
{
    let mut triangle = Triangle{
        p1 : Point(1,1),
        p2 : Point(1,2),
        p3 : Point(0,0)
    };

    println!("주어진 삼각형의 넓이 S = {}", calculate_area(&triangle)); //주어진 삼각형의 넓이 S = 0.5

    triangle.p1 = Point(2,-5);

    println!("주어진 삼각형의 넓이 S = {}", calculate_area(&triangle)); //주어진 삼각형의 넓이 S = 4.5
}

 

위의 예제를 살펴봅시다. 이전과 다른점은 함수 calculate_area()의 인자로 Triangle 형이 아닌 &Triangle 이라는 참조형을 넘겨줍니다. 함수 호출 또한 인자에 triangle대신 참조형 &triangle을 넣어주었습니다. 이제 함수 호출 이후에도 triangle 변수는 함수가 소유권을 넘겨받지 않았기 때문에 파기도 되지 않습니다.

 

변수 앞에 &를 붙여 참조자를 표현하는 것은 C/C++에 익숙하신 분들은 오히려 헷갈릴 수도 있는 표기법인데, 헷갈림을 덜어드리고자 차이점을 덧붙이자면 이것은 C/C++의 간접참조와 비슷한 형태지만 Rust는 간접참조에 번거롭게 -> 연산자를 쓰지 않고, 이렇게 참조된 변수의 값을 조작하는 것은 기본적으로 허용되지 않습니다. 대신 아래와같이 &대신 &mut 을 사용해서 빌린 값을 변경할 수 있게 명시할 수 있습니다.

 

fn calculate_area(triangle : &mut Triangle) -> f32
{
    triangle.p1 = Point(3,3);
    ...
}

...

println!("주어진 삼각형의 넓이 S = {}", calculate_area(&mut triangle));

 

3. 참조자 규칙

우리는 변수 앞에 &또는 &mut를 붙임으로써 참조자를 표현할 수 있는것으로 배웠습니다. 이 참조자 변수를 또한 어떤 변수에 담아서 아래와 같이 사용할 수도 있습니다.

 

fn main()
{
    let mut triangle = Triangle{
        p1 : Point(1,1),
        p2 : Point(1,2),
        p3 : Point(0,0)
    };

    let mut rep : &Triangle = ▵
    //let mut rep = ▵로 써도 무방

    println!("점 P1{:?}", (rep.p1.0, rep.p1.1));
}

 

여기서 중요한 규칙이 하나 있습니다. 한 스코프안에서 어떤 변수를 참조하는 참조자는 유일해야 한다는 점입니다. 위의 예제에서 변수 rep1은 변수 triangle의 참조자로 선언되었는데, 여기서 triangle 변수의 참조자 rep2를 새로 만들 경우 에러가 발생하게 됩니다.

 

fn main()
{
    let mut triangle = Triangle{
        p1 : Point(1,1),
        p2 : Point(1,2),
        p3 : Point(0,0)
    };

    let rep1 = &mut triangle;
    let rep2 = &mut triangle;

    println!("점 P1{:?}", (rep1.p1.0, rep1.p1.1)); //ERROR!
    println!("점 P1{:?}", (rep2.p1.0, rep2.p1.1)); //GOOD
}
error[E0499]: cannot borrow `triangle` as mutable more than once at a time
  --> main.rs:36:28
	|
35	|     let rep1 : &Triangle = &mut triangle;
	|                            ------------- first mutable borrow occurs here
36	|     let rep2 : &Triangle = &mut triangle;
	|                            ^^^^^^^^^^^^^ second mutable borrow occurs here
37	| 
38	|     println!("점 P1{:?}", (rep1.p1.0, rep1.p1.1));
	|                            --------- first borrow later used here

 

참조자는 유일해야 하기 때문에 변수 rep1이 가지고 있던 참조자는 rep2가 갖게되어 변수 rep1은 무효가 됩니다. 따라서 변수 rep1은 파기되어 사용할 수 없습니다. &mut& 참조자를 같이 쓰는 경우도 나중에 쓰인 참조자가 우선순위를 갖게됩니다.

 

fn main()
{
    let mut triangle = Triangle{
        p1 : Point(1,1),
        p2 : Point(1,2),
        p3 : Point(0,0)
    };

    {
        let rep1 = &mut triangle;
        let rep2 = ▵ //&triangle 참조자가 이 스코프 내에 우선순위를 가짐

        rep2.p1 = Point(2,2); //&참조자는 단순 참조만 가능하므로 이 구문은 ERROR!

        println!("점 P1{:?}", (rep2.p1.0, rep2.p1.1));  
    }

    {
        let rep1 = ▵
        let rep2 = &mut triangle; //&mut triangle 참조자가 이 스코프 내에 우선순위를 가짐
        
        rep2.p1 = Point(2,2); //GOOD
        
        println!("점 P1{:?}", (rep2.p1.0, rep2.p1.1)); // 점 P1(2,2)를 출력
    }
}

예외적으로 단순 &참조자는 몇 개가 있어도 에러를 유발하지 않습니다. 값의 변경에 아무런 영향도 주지 않기 때문입니다.

고졸미필백수
댓글 :
TOP INDEX SEARCH