5편에서 ownership이 "값이 하나의 주인만 가진다"는 규칙이라면, 6편은 "그 값에 잠깐 접근하고 싶을 때 어떤 제약을 지켜야 하는가"에 초점을 맞춥니다. [reference|참조]는 ownership을 넘기지 않고 값을 읽거나 쓰는 통로이고, borrowing은 그 통로를 빌리는 행위입니다. 이번 글에서는 lifetime 심화 규칙을 꺼내지 않고, 불변 참조와 가변 참조가 언제 허용되는지만 몸에 익힙니다.
이 규칙이 존재하는 이유는, 같은 데이터를 누군가는 읽고 다른 누군가는 동시에 바꾸는 상황을 미리 차단하기 위해서입니다. Rust는 이런 충돌을 실행 중에 잡기보다 컴파일 단계에서 먼저 막으려 합니다.
이번 글에서 다룰 개념
- 불변 참조 (
&T): 값을 읽기만 할 수 있는 렌즈로, 동시에 여러 개를 만들 수 있습니다. - 가변 참조 (
&mut T): 값을 수정할 수 있지만, 같은 시점에 오직 하나만 존재해야 합니다. - 핵심 borrowing 규칙: "동시에 여러 불변 참조" 또는 "동시에 하나의 가변 참조"는 괜찮지만, 이 둘을 섞어 쓰는 순간 컴파일러가 막습니다.
예제로 보는 참조 규칙
Rust는 변수 자체가 기본적으로 불변이라는 점에서 시작합니다. 참조도 같은 철학을 따릅니다.
fn main() {
let note = String::from("Rust ownership");
let read_only = ¬e; // 불변 참조
let also_read = ¬e; // 또 다른 불변 참조
println!("{} / {}", read_only, also_read);
println!("원본도 그대로 쓸 수 있음: {}", note);
}
불변 참조는 값을 복사하지 않고도 여러 곳에서 읽게 해 줍니다. 여기서 중요한 지점은 "참조를 쓰는 동안 원본을 수정하려 하면" 오류가 난다는 점입니다. 반대로 가변 참조는 수정 권한을 빌려 오지만, 동시에 단 하나만 허용됩니다.
fn main() {
let mut score = 80;
{
let change = &mut score; // 가변 참조는 하나만
*change += 5;
} // scope이 끝나면 change가 drop되고, 다시 score를 쓸 수 있음
println!("최종 점수: {}", score);
}
*change는 참조를 역참조(dereference)해서 실제 값을 가리키도록 만드는 연산자입니다. 즉, "참조가 가리키는 원래 값으로 따라간다"는 뜻입니다. 가변 참조를 쓸 때는 mut와 &mut를 동시에 기억해야 합니다.
동시에 허용되지 않는 조합
Borrowing 규칙을 어기는 코드를 일부러 적어 보고, 컴파일러 메시지를 읽는 연습이 필요합니다.
fn main() {
let mut label = String::from("Rust");
let read = &label;
let write = &mut label; // error: 불변 참조가 살아 있는 동안 가변 참조 금지
println!("{} {}", read, write);
}
이 오류는 Rust가 데이터 경합을 컴파일 단계에서 차단하려고 하기 때문입니다. 여기서 중요한 표현은 "같은 scope"보다 "같은 시점에 참조가 살아 있는 동안"입니다. 실제 프로젝트에서는 scope을 좁히거나, 필요한 시점 이후로 참조를 옮겨 "동시 접근" 상황을 피합니다.
값으로 넘길 때와 참조로 넘길 때
fn read_title(title: &String) {
println!("읽기 전용: {}", title);
}
fn main() {
let title = String::from("Rust notes");
read_title(&title); // 참조를 넘김
println!("원본 계속 사용 가능: {}", title);
}
함수 인자에 String을 그대로 넘기면 ownership이 이동하지만, &String이나 &str를 넘기면 읽기 권한만 빌려주는 것입니다. 따라서 &x를 붙이는 순간 "가져가는 것이 아니라 빌려준다"는 의도가 코드에 드러납니다.
작게라도 실전 맥락에 연결하기
이 시리즈에서는 학습 진행 상황을 다루는 간단한 콘솔 프로그램을 계속 발전시킵니다. 우선 학습 진도를 문자열로 관리하고, 참조를 통해 요약을 만들어 봅니다.
fn describe(status: &String) {
println!("진행 상황: {}", status);
}
fn main() {
let mut status = String::from("ownership 복습 중");
describe(&status); // 불변 참조로 읽기만 허용
update(&mut status); // 가변 참조로 상태 변경
describe(&status);
}
fn update(status: &mut String) {
status.push_str(", borrowing 연습");
}
status의 ownership은 main이 유지합니다. describe는 읽을 권한만 빌리고, update는 수정 권한을 단독으로 빌립니다. 함수 시그니처에 & 또는 &mut를 명시하는 순간, "이 함수는 값을 가져가지 않고 빌린다"는 의도가 선명해집니다.
CodeSandbox로 이어서 실습하기
아래 샌드박스는 CodeSandbox의 Rust starter입니다. 이번 글의 핵심 코드를 src/main.rs에 옮기고, cargo check와 cargo run 결과를 나란히 보면서 컴파일 메시지와 실행 출력을 비교해 보세요.
💬 댓글
이 글에 대한 의견을 남겨주세요