07. 참조와 빌림 (&T, &mut T, 빌림 규칙)
빌림(borrowing) 은 소유권을 옮기지 않고 값을 잠깐 빌려 쓰는 방법입니다. & 로 불변 참조, &mut 로 가변 참조. 핵심 규칙은 한 시점에 (불변 참조 N개) OR (가변 참조 1개), 둘이 섞일 수 없습니다. 이 규칙이 어떻게 데이터 경합을 컴파일 타임에 막아 주는지 살펴봅니다.
이 강의에서 배우는 것
- 1& 와 &mut 로 참조를 만들고 dereference 한다
- 2빌림 규칙(공유 N or 독점 1) 을 설명한다
- 3함수 인자로 참조를 받아 소유권 이동을 피한다
- 4borrow checker 의 에러 메시지를 읽고 해결한다
- 5참조의 스코프(NLL) 가 어떻게 동작하는지 안다
소개
이전 강의의 move 만으로는 함수에 값을 넘기고 나면 호출자에서 못 쓰게 됩니다. 매번 반환으로 돌려받으면 코드가 지저분해지죠. 빌림은 이 문제를 해결하면서도 **데이터 경합을 컴파일 타임에 막는** 핵심 메커니즘입니다.
핵심 개념
1) 두 종류의 참조
- **&T** — 불변 참조. 읽기만 가능. 한 시점에 여러 개 OK
- **&mut T** — 가변 참조. 쓰기 가능. 한 시점에 단 하나만
2) 빌림 규칙 (한 줄 요약)
**같은 데이터에 대해 한 시점에는 (불변 참조 N개) 또는 (가변 참조 1개) — 둘 중 하나만 존재 가능**.
3) 왜? — 데이터 경합 방지
한 스레드가 읽고 있는데 다른 스레드가 쓰면 데이터 경합입니다. Rust 는 이걸 **단일 스레드 코드에서조차** 막아 동시성 안전을 단순화합니다. 가변 참조 하나만 허용한다는 규칙이 멀티스레드까지 그대로 확장됩니다.
4) NLL — Non-Lexical Lifetimes
참조의 유효 범위는 **마지막 사용 지점까지** 입니다. 스코프 끝까지가 아닙니다. 이 덕에 같은 변수에 대해 불변 → 가변 → 불변 식의 sequential 사용이 자연스럽게 됩니다.
| 조합 | OK? |
|---|---|
| &x 만 여러 개 | ✓ |
| &mut x 한 개 | ✓ |
| &x + &x | ✓ |
| &x + &mut x | ✗ |
| &mut x + &mut x | ✗ |
핵심 예제
함수에 참조로 넘기기 — 소유권 이동 없음:
fn print_len(s: &String) {
println!("len={}", s.len());
}
fn main() {
let s = String::from("hello");
print_len(&s); // 빌려줌
println!("{}", s); // OK — s 는 여전히 살아있음
}가변 참조로 안쪽 값 변경:
fn add_world(s: &mut String) {
s.push_str(" world");
}
fn main() {
let mut s = String::from("hello");
add_world(&mut s);
println!("{}", s); // hello world
}동시 가변 참조는 컴파일 에러:
let mut x = 5;
let r1 = &mut x;
let r2 = &mut x; // error[E0499]: cannot borrow `x` as mutable more than once at a time
println!("{} {}", r1, r2);NLL — 사용 지점 끝나면 새 가변 빌림 가능:
let mut x = 5;
let r1 = &x;
let r2 = &x;
println!("{} {}", r1, r2); // r1, r2 의 마지막 사용
let r3 = &mut x; // OK — 위 둘은 더 안 쓰이므로
*r3 += 1;
println!("{}", r3);자주 하는 실수
Q. 함수 인자에 &mut 를 받는데 호출 시 또 &mut 를 써야 하나요?
A. 네, 호출자도 `add_world(&mut s)` 처럼 명시적으로 가변 참조를 만들어 넘겨야 합니다. 코드만 읽어도 어디서 변경이 일어나는지 보입니다.
Q. 참조에 `.` 를 쓰면 자동으로 dereference 되나요?
A. 네, `s.len()` 처럼 메서드 호출 시 컴파일러가 알아서 deref 합니다. 원시 dereference 는 `*r` 으로 명시.
Q. 빌림 검사기가 너무 빡빡해요. 어떻게 해야 하나요?
A. 에러 메시지를 천천히 읽으세요. Rust 컴파일러는 거의 항상 **수정 제안** 까지 같이 알려줍니다. 그리고 보통 코드 구조를 약간 바꿔(스코프 좁히기, 같은 함수에서 가변·불변 동시 안 쓰기) 풀립니다.
정리
- 빌림은 소유권 이동 없이 잠깐 빌리는 것 — & 와 &mut
- 한 시점에 (불변 N) 또는 (가변 1) — 동시에 섞일 수 없음
- 데이터 경합 방지가 단일 스레드 코드에서부터 강제됨
- NLL 덕에 마지막 사용 후 새 빌림 가능
과제
- 문자열을 받아 길이를 출력하는 함수를 참조 버전과 소유권 버전으로 둘 다 작성 후 호출자에서 차이 확인
- Vec<i32> 의 모든 원소에 1 을 더하는 함수를 &mut Vec<i32> 로 작성
- 동일 변수에 &mut 를 두 번 만드는 코드를 작성하고 컴파일 에러 메시지를 그대로 옮겨 적기