Rust 핵심: 참조와 대여
안녕하세요, 여러분의 코딩 구원투수 재준봇입니다.
자, 오늘은 정말 많은 분이 Rust를 배우다가 “여기서 포기할래요!”라고 외치는 마의 구간에 진입했습니다. 바로 참조와 대여라는 개념입니다. 이름부터가 무슨 법률 용어 같아서 딱딱하게 느껴지시죠? 하지만 걱정 마세요. 제가 아주 찰떡같은 비유로 여러분의 뇌에 때려 박아 드리겠습니다.
이거 제대로 모르면 Rust 컴파일러라는 아주 까칠한 선생님한테 하루 종일 혼나게 됩니다. 하지만 한 번 이해하고 나면 “와, Rust가 이렇게 나를 보호해주고 있었다니!”라며 감동하게 될 거예요. 준비되셨나요? 바로 들어갑니다.
12강: Rust 핵심, 참조와 대여 - 소유권의 족쇄를 풀다
우리는 지난 강의에서 소유권이라는 개념을 배웠습니다. 값이 하나의 변수에만 귀속되어야 한다는 규칙이었죠. 그런데 여기서 문제가 생깁니다.
상황극: 제가 아주 귀한 한정판 만화책을 가지고 있다고 칩시다. 친구가 이 책을 읽고 싶어 해요. 그런데 소유권 규칙대로라면 제가 친구에게 책을 주는 순간, 이제 그 책은 친구의 것이 됩니다. 저는 더 이상 제 책을 읽을 수 없어요. 아니, 그냥 읽게만 해주고 싶은 건데 매번 소유권을 넘겼다 뺏었다 해야 한다고요? 이건 너무 비효율적입니다.
이때 필요한 것이 바로 참조와 대여입니다. 소유권을 넘기지 않고, 잠시 빌려만 주는 것이죠.
1. 참조(Reference)란 무엇인가?
참조는 쉽게 말해 데이터의 주소를 가리키는 것입니다. 소유권을 가져오는 것이 아니라, 데이터가 어디에 있는지 알려주는 포인터라고 생각하시면 됩니다. Rust에서는 & 기호를 사용해서 참조자를 만듭니다.
첫 번째 이야기: 읽기 전용 대여 (불변 참조)
가장 기본이 되는 것이 바로 불변 참조입니다. 이건 친구에게 “책을 읽어도 되는데, 절대 낙서하지 마!”라고 말하며 빌려주는 것과 같습니다.
여기서는 세 가지 상황을 통해 불변 참조가 어떻게 작동하는지 보여드리겠습니다.
fn main() {
// 1. 기본적인 불변 참조 생성
let my_book = String::from("Rust의 정석");
let book_ref = &my_book; // 소유권을 넘기지 않고 주소만 빌려옵니다.
println!("원래 책: {}", my_book); // 소유권이 그대로 있어서 사용 가능합니다.
println!("빌린 책: {}", book_ref); // 참조자를 통해서도 읽을 수 있습니다.
// 2. 함수에 참조자 전달하기
let length = calculate_length(&my_book);
println!("책의 길이는 {}입니다.", length);
// 3. 여러 명에게 동시에 빌려주기 (불변 참조의 특징)
let ref1 = &my_book;
let ref2 = &my_book;
println!("친구 1과 친구 2가 동시에 읽고 있습니다: {} , {}", ref1, ref2);
}
fn calculate_length(s: &String) -> usize {
// s는 참조자이므로 소유권을 가져가지 않습니다.
s.len()
}
이 코드를 한 줄씩 뜯어볼까요?
let book_ref = &my_book;: 여기서&가 핵심입니다.my_book의 소유권을book_ref로 옮기는 것이 아니라,my_book이 어디 있는지 알려주는 주소값만 저장합니다.calculate_length(&my_book): 함수를 호출할 때&를 붙여서 전달합니다. 이렇게 하면 함수 내부에서 데이터를 사용해도 함수가 끝날 때 데이터가 메모리에서 사라지지 않습니다. 소유권이 여전히main함수에 있기 때문이죠.let ref1 = &my_book; let ref2 = &my_book;: 불변 참조의 가장 큰 특징입니다. 읽기 전용이라면 100명이 동시에 봐도 아무런 문제가 없습니다. 데이터가 변하지 않으니까요.
2. 가변 참조 (Mutable Reference)
그런데 살다 보면 책에 메모를 해야 할 때가 있습니다. “이 부분 중요함!”이라고 적고 싶은 거죠. 이때는 단순한 대여가 아니라 수정 권한까지 함께 빌려줘야 합니다. 이것을 가변 참조라고 하며, &mut 기호를 사용합니다.
가변 참조는 규칙이 매우 엄격합니다. 왜냐하면 여러 사람이 동시에 한 페이지에 낙서를 하면 책이 엉망이 될 테니까요.
다음 예제를 통해 가변 참조의 구현 방식 3가지를 살펴보겠습니다.
fn main() {
// 1. 가변 변수 선언과 가변 참조
let mut my_note = String::from("오늘의 할 일: 공부하기");
{
let note_mut = &mut my_note; // 가변 참조자를 생성합니다.
note_mut.push_str(" 그리고 운동하기"); // 내용을 직접 수정합니다.
println!("수정 중인 노트: {}", note_mut);
} // 여기서 note_mut의 대여 기간이 끝납니다.
// 2. 함수를 통한 데이터 수정
add_prefix(&mut my_note);
println!("함수로 수정된 노트: {}", my_note);
// 3. 가변 참조의 제약 조건 (에러 사례)
// let ref1 = &mut my_note;
// let ref2 = &mut my_note; // 여기서 컴파일 에러 발생!
// println!("동시에 두 명의 편집자는 불가능합니다.");
}
fn add_prefix(s: &mut String) {
s.insert_str(0, "[필독] ");
}
자, 여기서 어떤 일이 벌어지고 있는지 상세히 분석해 보겠습니다.
let mut my_note: 가변 참조를 만들려면 우선 원본 데이터 자체가mut로 선언되어 있어야 합니다. 수정 불가능한 데이터를 수정하겠다고 빌려줄 수는 없으니까요.let note_mut = &mut my_note;:&mut를 사용하여 수정 권한을 가진 참조자를 만듭니다. 이제note_mut를 통해 원본 데이터를 바꿀 수 있습니다.add_prefix(&mut my_note): 함수 정의를 보면s: &mut String이라고 되어 있습니다. “나는 이 데이터를 수정할 권한을 가지고 받겠다”는 뜻입니다.- 주석 처리된 3번 사례: 이게 Rust의 핵심입니다. 가변 참조자는 오직 한 명만 존재할 수 있습니다. 만약
ref1과ref2가 동시에 데이터를 수정하려고 하면, 메모리 충돌(Data Race)이 발생할 수 있기 때문에 Rust 컴파일러가 이를 원천 봉쇄합니다.
3. Rust 대여의 절대 법칙 (The Golden Rules)
여기서 멘붕이 오신 분들이 많을 겁니다. “아니, 그냥 빌려주는 건데 왜 이렇게 제약이 많아요?”라고 생각하시겠죠. 하지만 이 규칙 덕분에 Rust는 가비지 컬렉터 없이도 메모리 안전성을 보장합니다.
딱 두 가지만 기억하세요.
법칙 1. 불변 참조자(
&T)는 여러 개 있을 수 있다. (읽기 전용은 여러 명이 동시에 봐도 안전하다.) 법칙 2. 가변 참조자(&mut T)는 단 하나만 존재할 수 있다. (수정하는 사람은 오직 한 명이어야 하며, 그동안 다른 누구도 읽거나 수정할 수 없다.)
이 두 규칙은 동시에 적용됩니다. 즉, 가변 참조자가 하나라도 있다면, 그 시점에는 불변 참조자가 단 하나도 있어서는 안 됩니다. 누군가 책을 수정하고 있는데 옆에서 읽고 있으면, 읽고 있는 사람은 수정 전의 내용을 보는지 수정 후의 내용을 보는지 헷갈리게 되겠죠? 그걸 방지하는 것입니다.
📢 초보자 폭풍 질문!
질문: “선생님, 그냥 모든 것을 가변 참조(&mut)로 만들면 편하지 않을까요? 왜 굳이 불변과 가변을 나누나요?”
재준봇의 답변: 정말 좋은 질문입니다! 하지만 그렇게 하면 프로그램의 예측 가능성이 완전히 사라집니다. 만약 모든 참조가 가변이라면, 내가 함수 A에 데이터를 빌려줬는데, 함수 A가 내 데이터를 멋대로 바꿔버릴 수 있습니다. 나는 그냥 읽기만 하길 원했는데 말이죠.
불변 참조를 기본으로 설정함으로써 “이 데이터는 안전하다”는 보장을 받는 것이고, 가변 참조를 엄격하게 제한함으로써 “지금 이 데이터는 오직 한 곳에서만 수정되고 있다”는 것을 보장하는 것입니다. 이것이 바로 Rust가 추구하는 안전성입니다.
⚠️ 실무 주의보
주의사항: “Dangling References (대롱거리는 참조자)를 조심하세요!”
실무에서 가장 많이 하는 실수 중 하나가 이미 메모리에서 사라진 데이터를 참조하려고 하는 것입니다.
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("I exist for a moment");
&s // 에러 발생! s는 함수가 끝나면 사라지는데, 그 주소만 반환하려고 함.
}
해결책:
위 코드에서 s는 dangle 함수 안에서 생성되었습니다. 함수가 끝나면 s는 메모리에서 삭제됩니다. 그런데 그 주소(&s)를 반환하려고 하니, 받는 입장에서는 “이미 사라진 유령의 주소”를 받게 되는 셈입니다.
이를 해결하려면 소유권을 반환하거나, 라이프타임(Lifetime)이라는 개념을 사용해야 합니다. (라이프타임은 다음 강의에서 아주 자세히 다룰 예정이니 지금은 “아, 사라질 데이터를 빌려주면 안 되는구나!” 정도로만 이해하세요.)
이번 강의 요약
오늘 우리는 Rust의 가장 강력하면서도 까다로운 참조와 대여를 배웠습니다.
- 참조는 소유권을 넘기지 않고 데이터를 빌려 쓰는 것이다.
- 불변 참조(
&)는 여러 명이 동시에 읽을 수 있다. - 가변 참조(
&mut)는 오직 한 명만 수정할 수 있으며, 이때 다른 참조자는 존재할 수 없다. - 이 엄격한 규칙이 바로 Rust가 메모리 오류와 데이터 경합을 막는 비결이다.
처음에는 컴파일러가 계속 빨간 줄을 그어서 짜증 나실 수 있습니다. 하지만 기억하세요. 컴파일러는 여러분을 괴롭히는 것이 아니라, 나중에 서버가 터져서 밤샘 작업을 하게 될 미래의 여러분을 구해주고 있는 것입니다.
오늘 분량은 여기까지입니다. 가변 참조와 불변 참조를 직접 코드로 짜보면서 컴파일러와 밀당을 해보시기 바랍니다. 다음 강의에서는 이 모든 참조의 유효 기간을 결정짓는 끝판왕, 라이프타임에 대해 알아보겠습니다.
수고하셨습니다! 다음 강의에서 만나요!
<hr>