복합 타입에서의 소유권

Rust에서 소유권은 단순 스칼라 타입뿐 아니라 다양한 복합 타입에도 동일하게 적용된다. 복합 타입이 스택에 저장되는지, 힙에 저장되는지, 혹은 내부에서 다른 메모리를 참조하는지에 따라 소유권과 빌림의 규칙이 미묘하게 달라질 수 있다. 복합 타입이 어떤 자원을 소유한다면, Rust는 그 소유권을 안전하게 관리하기 위해 컴파일 타임에 엄격한 검사 과정을 거친다. 소유권이 이동( move )될 때 실제로 복합 타입이 갖고 있던 힙 메모리가 이동하거나 무효화되며, 빌림( borrow )이 이루어질 때는 그 복합 타입의 데이터를 읽거나 수정하기 위해 안전한 참조자를 생성한다.

복합 타입에는 대표적으로 String, Vec, 구조체(struct), 열거형(enum), 튜플(tuple) 등이 있다. 이들은 스칼라 타입과 달리 힙에 데이터를 저장하거나 내부적으로 동적 할당을 할 수 있으므로, Rust는 다른 언어에서 발생하기 쉬운 메모리 안전성 문제를 사전에 예방하기 위해 소유권 시스템을 엄격하게 적용한다. 복합 타입에서 어떤 상황에서 소유권이 이동하고, 빌림이 이루어지며, 그로 인한 효과가 어떻게 발생하는지 구체적으로 알아보자.

먼저 String은 힙에 데이터가 저장되는 가변 텍스트 타입이다. String 타입 변수를 새로 할당하고, 다른 변수에 대입하면 원본 변수가 갖고 있던 소유권이 대상 변수로 이동한다. 원본 변수를 다시 사용하려 하면 컴파일 오류가 발생한다. 다음 코드를 보면 String 소유권이 어떻게 이동하는지 확인할 수 있다.

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;  // s1의 소유권이 s2로 이동 (move)
    // println!("{}", s1);  // 이 시점에서 s1은 더 이상 유효하지 않음

    println!("{}", s2);  // 출력: hello
}

s1이 String::from으로 생성한 "hello"라는 힙 데이터의 소유권을 갖고 있다가, s2에 대입되는 순간 그 소유권이 s2로 이동한다. 따라서 이후에 s1을 다시 사용하면 컴파일 오류가 발생한다. String은 단순 복사가 아닌 move语语语 (얕은 복사 후 원본 무효화)가 일어나기 때문이다.

만약 소유권을 이동하지 않고 같은 데이터를 여러 변수가 공유해야 한다면 참조자를 빌려야 한다. 소유권이 있는 값에 대해 &를 통해 불변 참조를 빌리거나 &mut 를 통해 가변 참조를 빌릴 수 있다. 불변 참조는 데이터를 읽기만 가능하고, 가변 참조는 데이터를 수정할 수 있다. 코드로 보면 아래와 같이 표현된다.

fn main() {
    let s1 = String::from("hello");
    let s2 = &s1;         // s1을 불변 참조
    let s3 = &mut s1;     // 불가능: 이미 불변 참조가 존재함

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

위 예시에서는 s1의 불변 참조 s2가 생성된 상태이므로, 동시에 가변 참조 &mut s1을 만드는 것은 허용되지 않는다. 이처럼 Rust는 동일 스코프 내에서 “여러 개의 불변 참조” 또는 “단 하나의 가변 참조”만 허용함으로써 데이터 경쟁(race condition)을 원천 차단한다.

복합 타입 중 Vector(Vec)도 String과 마찬가지로 힙에 데이터를 저장한다. 따라서 Vec 역시 다른 변수에 대입하면 move가 일어나고, 원본 변수의 소유권은 무효가 된다. 데이터가 필요하지만 굳이 소유권이 이동할 필요가 없는 경우, &Vec 형태로 참조를 빌려 쓸 수 있다. 예시는 다음과 같다.

구조체(struct) 역시 내부 필드에 힙 데이터를 담고 있으면 그것을 소유하게 된다. 구조체 인스턴스를 대입하면 구조체 전체가 move된다. 구조체 안의 어떤 필드만 참조하거나, 부분적으로만 빌림을 할 수 있는 상황이 발생할 수도 있다. Rust는 내부에 참조자가 있는 구조체를 정의할 때마다 수명(lifetime)을 명시적으로 표기하도록 하여, 구조체가 유효한 동안 내부 참조자가 항상 유효함을 보장하게 해 준다. 예시는 아래와 같다.

p1의 구조체 Person이 갖고 있던 힙 데이터(name 필드)가 p2로 이동하면서 p1은 무효화된다. 만약 이름만 잠시 조회하고자 한다면 &p1.name 형태로 불변 참조를 빌려 접근해야 한다. 가변 참조를 얻어 수정을 하고 싶다면 &mut p1.name 같은 방식으로 빌릴 수 있는데, 이는 동시에 불변 참조가 존재하지 않을 때만 가능하다.

열거형(enum)도 내부에 힙 자원을 담을 수 있다. 실제로 Option과 같은 열거형 변형(variant)을 다룰 때는, Some(String::from("데이터")) 형태로 힙 데이터를 저장하게 되는데, 이 열거형 값이 다른 변수로 이동하면 String의 소유권도 함께 이동한다. 다만 불변 참조나 가변 참조를 사용해 빌려 쓰는 것은 구조체와 동일한 방식으로 동작한다.

튜플(tuple)은 여러 타입을 묶는 또 다른 복합 타입으로, 예를 들어 (String, i32)와 같이 힙 데이터를 포함할 수 있다. 마찬가지로 튜플 안에 들어 있는 String의 소유권은 튜플 자체의 소유권에 종속되어 있다. 튜플이 다른 변수로 이동하면 내부 String도 함께 move된다. 부분 빌림을 통해 튜플 내 특정 필드만 참조하는 것도 가능하다.

상기의 예시에서 t1이 갖고 있던 String("tuple")은 t2로 이동했고, t1을 다시 사용하면 컴파일 오류가 난다. 만약 문자열에만 접근하고 싶다면 &t1.0의 형태로 참조자를 빌리는 방식이 필요하다.

정리하면, 복합 타입은 내부적으로 힙 메모리를 사용하거나 다른 자원에 대한 핸들을 포함할 수 있다. Rust는 소유권 시스템을 통해 어떤 복합 타입이 메모리 리소스를 안전하게 관리하도록 엄격한 규칙을 적용한다. 복합 타입 인스턴스 자체를 함수에 넘기거나 다른 변수에 대입하면 move가 일어나고, 참조(&, &mut)를 사용하는 빌림 방식으로는 소유권을 이동시키지 않고 같은 데이터를 공유할 수 있다. 이 과정에서 참조가 유효한 동안 원본 값을 해제하거나 변경할 수 없도록 제어함으로써, 시스템 전반에 메모리 안전성을 강제한다.

이러한 소유권과 빌림 규칙은 처음 접하면 번거롭게 느껴질 수 있으나, 실제로는 런타임에 추가 오버헤드 없이 프로그램 전체의 안정성과 신뢰성을 높여 준다. Rust의 소유권 시스템을 이해하고 복합 타입에도 일관되게 적용하는 것은 필수적이며, 이 원칙을 잘 활용하면 러스트다운 안전하고 효율적인 코드를 작성할 수 있다.

Last updated