빌림과 참조

러스트에서 소유권이란 어떤 값에 대해 그 값을 완전히 책임질 수 있는 권리를 말한다. 그러나 모든 상황에서 소유권을 직접 가지는 것이 바람직하지 않을 때도 있다. 예컨대 이미 소유권을 가진 변수를 다른 함수로 넘겨서 사용만 하고 싶거나, 소유권을 여러 곳에서 공유해 값에 대한 읽기 작업만 하려 할 때 빌림을 활용할 수 있다. 빌림은 말 그대로 값을 잠시 빌려 사용하는 개념이다. 이때 빌림은 참조(Reference)를 통해서 이루어진다.

참조가 발생하면 원래 소유자가 여전히 값을 소유하고, 빌리는 쪽은 그 값에 대한 접근 권한만 얻는다. 참조는 소유권을 이전하지 않고도 값에 접근할 수 있게 해주므로, 메모리 사용 효율이나 코드 안전성 측면에서 중요한 개념이다. 러스트 컴파일러는 이 참조가 유효한지, 즉 참조된 값이 메모리에 안전하게 유지되는지를 검사하여 런타임 오류를 방지한다.

이렇게 참조로 값을 빌릴 때는 크게 두 가지 형태가 있다. 공유 참조(&T)와 가변 참조(&mut T)이다. 공유 참조는 여러 개가 동시에 존재해도 문제가 없지만, 가변 참조는 같은 스코프 안에서 단 하나만 존재할 수 있다. 왜냐하면 가변으로 빌린 값을 다른 참조와 동시에 읽거나 수정하는 것은 데이터 경쟁(data race)을 초래할 수 있기 때문이다. 러스트에서는 이런 위험한 상황을 원천적으로 봉쇄하기 위해 다음과 같은 핵심 규칙을 지킨다. 하나의 가변 참조가 존재하는 동안 같은 범위 안에서는 그 값에 대한 다른 참조(공유든 가변이든)를 생성할 수 없다. 반면 공유 참조는 여러 개가 동시에 만들어져도 상관없다.

아래 예시는 참조를 단순히 함수에 넘겨주는 상황을 살펴본 것이다.

fn main() {
    let s = String::from("Hello, Rust!");
    
    // 공유 참조를 함수에 넘기는 예시
    print_message(&s);

    // 여기서 s는 여전히 유효하고, 소유권도 그대로 유지된다.
    println!("원본 s는 여전히 사용 가능: {}", s);
}

fn print_message(message: &String) {
    println!("메시지: {}", message);
}

이 코드에서 print_message 함수는 message: &String 형태의 매개변수를 받는다. 이는 print_message가 원본 문자열 s를 빌려만 쓰고, 소유권을 넘겨받지는 않는다는 의미다. 함수가 끝난 뒤에도 s의 소유권은 main 함수가 계속 유지하므로, main에서는 여전히 s를 사용할 수 있다.

가변 참조를 다루는 예시를 보면 더욱 분명해진다.

change_string 함수는 &mut String 매개변수를 통해 가변 참조를 전달받는다. 이 함수는 문자열에 push_str 메서드를 사용하여 새로운 부분을 추가할 수 있다. 이렇게 함수를 호출하는 시점에는 s에 대한 가변 참조가 존재하므로, 같은 범위 내에서 s를 가리키는 다른 참조(공유든 가변이든)를 만들면 컴파일 에러가 발생한다.

가변 참조는 데이터를 변경할 수 있으므로, 여러 가변 참조가 동시에 존재한다면 누가 언제 어떻게 데이터를 수정하는지 알 수 없게 된다. 이는 멀티스레드 환경에서 동시 접근 문제(데이터 경쟁)로 이어질 수 있으며, 전통적인 언어에서는 런타임 예외나 예측 불가능한 버그를 유발한다. 러스트는 컴파일 타임에 이를 철저히 검사하여, 동시에 유효한 여러 가변 참조가 생기지 않도록 막는다.

공유 참조는 여러 곳에서 동시에 읽기만 하면 되므로 안전성 문제를 일으키지 않는다. 그래서 &T 형태의 참조는 같은 스코프 안에서 여러 개가 만들어져도 괜찮다. 그러나 공유 참조가 존재하는 동안에는 가변 참조를 생성할 수 없다. 공유 참조는 읽기 전용 접근을 보장하고, 가변 참조는 쓰기 가능 접근을 보장한다. 둘을 섞으면 서로 다른 관점에서 값이 동시에 변경 또는 관찰될 수 있어 안전성을 잃게 되므로, 러스트에서는 이를 허용하지 않는다.

참조를 사용할 때는 주의해야 할 또 다른 측면이 있다. 참조가 유효해야 하는 범위(수명, Lifetime)이다. 러스트는 스코프가 끝나면 변수가 해제되는데, 만약 그 변수에 대한 참조가 여전히 존재한다면 메모리 안전성 문제가 발생한다. 다행히 러스트 컴파일러가 이 과정을 자동으로 추적해 참조의 유효 범위를 초과하지 않도록 검사한다. 간단한 상황에서는 사용자가 수명 표기를 명시할 필요 없이 러스트가 알아서 추론해 주지만, 때때로 함수 시그니처에 수명 매개변수를 지정해야 할 경우도 있다. 예를 들어 여러 참조를 동시에 반환하거나, 복잡하게 참조가 뒤엉킨 상황에서 컴파일러에게 구체적 수명을 알려주어야 할 때가 있다.

결과적으로 참조는 “어떤 값을 빌려서 사용한다”는 직관적인 개념이지만, 내부적으로는 러스트 컴파일러가 수명과 소유권 체계를 기반으로 정교하게 메모리 안전을 보장하도록 구성된 핵심 요소다. 간단히 정리하면 공유 참조는 데이터 변경 없이 여러 개가 가능하고, 가변 참조는 단 하나만이 존재하며 데이터를 변경할 수 있다. 이 원칙으로 인해 대규모 시스템 개발에서도 메모리 안전성과 동시성 안전을 훨씬 쉽게 확보할 수 있다.

러스트의 빌림과 참조는 다른 언어에서 흔히 발생하는 널 포인터 참조나 데이터 경쟁 문제를 예방한다. 이는 컴파일러가 철저한 검사 과정을 통해 무효한 참조나 중복 가변 참조를 허용하지 않도록 해주기 때문이다. 따라서 빌림과 참조 개념을 정확히 이해하고 코드에 활용하면, 런타임에 발생할 수 있는 메모리 관련 오류들을 사전에 제거할 수 있고, 최적의 메모리 접근 방식을 통해 성능상 이점도 얻게 된다.

Last updated