Rust 대여
소유권 페이지에서 한 가지 문제를 봤어요. String을 함수에 넘기면 값이 이동해서, 그 뒤로는 쓸 수 없었죠. 값을 매번 이리저리 옮기거나 복제하는 건 정말 번거로울 거예요. 그래서 Rust가 내놓은 해결책이 대여(borrowing)예요.
대여는 소유권을 넘기지 않고 다른 코드가 값을 사용하게 해 주는 거예요. 친구에게 책을 빌려주는 것과 비슷해요. 친구는 책을 읽고 돌려주고, 책의 주인은 여전히 여러분이에요.
참조자: &로 대여하기
섹션 제목: “참조자: &로 대여하기”값 앞에 &를 붙이면 참조자(reference)가 만들어져요. 참조자를 쓰면 함수가 데이터를 소유하지 않고도 들여다볼 수 있어요.
fn print_length(s: &String) { // s를 대여할 뿐, 소유하지 않아요 println!("길이: {}", s.len());}
fn main() { let text = String::from("hello"); print_length(&text); // text를 함수에 빌려줘요 println!("아직 내 거예요: {text}"); // ✅ 여기서도 text는 여전히 유효해요}실행 결과:
길이: 5아직 내 거예요: hello&text는 “이 함수가 text를 대여하게 해 줘”라는 뜻이에요. 함수는 빌리기만 했으니 소유권은 main에 남아 있고, 그 뒤에도 text를 계속 쓸 수 있어요. & 없이 같은 코드를 썼다가 에러가 났던 소유권 페이지와 비교해 보세요.
여기서 "hello"를 영어로 둔 이유가 있어요. .len()은 글자 수가 아니라 바이트 수를 세는데, 한글 한 글자는 UTF-8에서 3바이트라서 결과가 달라지기 때문이에요.
가변 참조자: 값을 바꾸려고 빌리기
섹션 제목: “가변 참조자: 값을 바꾸려고 빌리기”그냥 &로 만든 참조자는 읽기 전용이에요. 볼 수는 있지만 건드릴 수는 없어요. 함수가 빌린 값을 바꿔야 한다면 &mut로 가변 참조자(mutable reference)를 쓰세요.
fn add_excitement(s: &mut String) { s.push_str("!!!");}
fn main() { let mut text = String::from("안녕"); add_excitement(&mut text); // 가변으로 빌려줘요 println!("{text}"); // 안녕!!!}실행 결과:
안녕!!!세 가지가 맞아떨어져야 했다는 점을 눈여겨보세요. text를 mut로 선언했고, 함수는 &mut String을 받고, 호출할 때 &mut text를 넘겼어요. 빌린 값을 바꾸려면 이 세 가지가 모두 필요해요.
대여 규칙
섹션 제목: “대여 규칙”Rust는 참조자에 대해 두 가지 규칙을 강제해요. 이 규칙은 데이터 경쟁(data race)을 막기 위해 있어요. 데이터 경쟁은 두 코드가 같은 데이터를 동시에 건드리다가 데이터를 망가뜨리는 버그들을 말해요.
어느 순간이든 다음 둘 중 하나만 가질 수 있어요.
- 읽기 전용 참조자(
&) 여러 개, 또는 - 가변 참조자(
&mut) 딱 하나.
둘을 동시에 가질 수는 없어요.
쉽게 말하면, 읽는 사람은 여럿이어도 되고 쓰는 사람은 한 명만 있어야 하며, 둘이 함께 있으면 안 돼요. 이 코드는 규칙을 어겨요.
fn main() { let mut text = String::from("안녕");
let r1 = &text; // 읽기 전용 대여 let r2 = &mut text; // ❌ r1이 있는 동안에는 가변으로 빌릴 수 없어요
println!("{r1} {r2}");}컴파일러가 이렇게 막아 줘요.
error[E0502]: cannot borrow `text` as mutable because it is also borrowed as immutable“text가 이미 불변으로 대여되어 있어서 가변으로 대여할 수 없다”는 뜻이에요.
대여는 언제 끝날까요?
섹션 제목: “대여는 언제 끝날까요?”대여가 스코프의 닫는 }까지 계속될 거라고 생각할 수 있어요. 그렇지 않아요. 대여는 참조자가 마지막으로 쓰인 곳까지만 이어져요. 그 뒤로는 값을 다시 대여할 수 있어요. 가변으로도요.
그래서 위의 잘못된 예제는 r2를 만들기 전에 r1을 다 쓰기만 하면 고칠 수 있어요.
fn main() { let mut text = String::from("안녕");
let r1 = &text; println!("읽는 중: {r1}"); // r1을 마지막으로 쓰는 곳이라 여기서 대여가 끝나요
let r2 = &mut text; // ✅ 괜찮아요: 이제 읽는 사람이 없어요 r2.push_str(", 세상");
println!("{text}");}실행 결과:
읽는 중: 안녕안녕, 세상규칙은 여전히 “읽는 사람 여럿 또는 쓰는 사람 하나”예요. 다만 각 참조자가 실제로 쓰이는 구간을 기준으로 검사할 뿐이에요. 쓰는 쪽 r2가 나타날 때쯤에는 읽는 쪽 r1이 이미 끝났고, 마지막 println!이 text를 다시 읽기 전에 r2도 끝나요. 겹치는 곳이 없으니 Rust도 만족해요.
대여 에러를 고치는 가장 흔한 방법이 바로 이거예요. 순서를 바꾸세요. 읽기와 쓰기를 섞지 말고, 필요한 읽기를 모두 끝낸 다음 쓰기를 하세요(반대 순서여도 괜찮아요).
이게 왜 중요할까요?
섹션 제목: “이게 왜 중요할까요?”이 규칙들은 모두 컴파일할 때 검사되기 때문에, 프로그램이 실행될 때는 비용이 전혀 들지 않아요. Rust가 “데이터 경쟁 없음”을 약속하면서도 C만큼 빠를 수 있는 이유예요. 안전성이 프로그램이 시작되기도 전에 증명되거든요.
연습 문제
섹션 제목: “연습 문제”먼저 Rust Playground나 내 컴퓨터의 프로젝트에서 직접 풀어 보세요. 컴파일러가 불평하면 에러 메시지를 읽어 보세요. 대부분 대여가 정확히 어디서 겹치는지 보여 줘요.
1. 가져가지 않고 세기
섹션 제목: “1. 가져가지 않고 세기”Vec<String>을 대여해서 모든 단어의 글자 수를 합한 값을 반환하는 함수 total_letters를 작성해 보세요. 함수를 호출한 뒤에도 main에서 벡터를 출력할 수 있어야 해요.
힌트: 매개변수 타입은 &Vec<String>이고, 호출할 때는 &words를 넘겨요. 빌린 벡터를 for word in list로 돌면서 word.len()을 더하세요. .len()은 바이트 수를 세기 때문에, 단어는 영어로 두었어요.
정답 보기
fn total_letters(list: &Vec<String>) -> usize { let mut total = 0; for word in list { total += word.len(); } total}
fn main() { let words = vec![String::from("borrow"), String::from("check")]; let count = total_letters(&words); // 벡터를 빌려줘요 println!("글자 수: {count}"); println!("{:?} 벡터는 아직 main이 갖고 있어요", words);}실행 결과:
글자 수: 11["borrow", "check"] 벡터는 아직 main이 갖고 있어요함수는 단어를 보기만 하면 되니까, 읽기 전용 & 대여로 충분해요. 만약 list: Vec<String>이라고 썼다면 벡터가 함수 안으로 이동해서, 마지막 println!이 컴파일되지 않았을 거예요.
2. 장보기 목록
섹션 제목: “2. 장보기 목록”함수 두 개를 작성해 보세요. 장보기 목록에 물건을 추가하는 add_item과, 모든 물건을 출력하는 print_list예요. 물건 세 개를 추가한 다음 목록을 출력하세요.
힌트: 한 함수는 목록을 바꾸고, 다른 하나는 읽기만 해요. 어느 쪽에 &mut Vec<String>이 필요하고, 어느 쪽은 &Vec<String>이면 될까요? &str을 String으로 바꾸려면 String::from(item)을 쓰세요.
정답 보기
fn add_item(list: &mut Vec<String>, item: &str) { list.push(String::from(item));}
fn print_list(list: &Vec<String>) { for item in list { println!("- {item}"); }}
fn main() { let mut list = Vec::new(); add_item(&mut list, "우유"); add_item(&mut list, "빵"); add_item(&mut list, "사과"); print_list(&list);}실행 결과:
- 우유- 빵- 사과add_item은 목록을 바꾸니까 &mut로 대여받고, main의 list도 mut여야 해요. print_list는 읽기만 하니까 그냥 &면 충분해요. 각 &mut list 대여는 함수 호출이 끝날 때 끝나요. 그래서 다음 호출에서 다시 빌릴 수 있는 거예요.
3. 쓰는 사람이 동시에 둘
섹션 제목: “3. 쓰는 사람이 동시에 둘”이 프로그램은 컴파일되지 않아요. first와 second를 둘 다 남겨 둔 채로, 점수: 10점이 출력되도록 고쳐 보세요.
fn main() { let mut message = String::from("점수:"); let first = &mut message; let second = &mut message; first.push_str(" 10"); second.push_str("점"); println!("{message}");}error[E0499]: cannot borrow `message` as mutable more than once at a time --> src/main.rs:4:18 |3 | let first = &mut message; | ------------ first mutable borrow occurs here4 | let second = &mut message; | ^^^^^^^^^^^^ second mutable borrow occurs here5 | first.push_str(" 10"); | ----- first borrow later used here“message를 한 번에 두 번 이상 가변으로 대여할 수 없다”는 뜻이에요.
힌트: 쓰는 사람은 한 번에 한 명이에요. 위의 대여는 언제 끝날까요?를 다시 보세요. second를 만들기 전에 first를 다 쓸 수 있을까요?
정답 보기
fn main() { let mut message = String::from("점수:");
let first = &mut message; first.push_str(" 10"); // first를 마지막으로 쓰는 곳: 여기서 대여가 끝나요
let second = &mut message; second.push_str("점");
println!("{message}");}실행 결과:
점수: 10점원래 코드에서는 4번째 줄에서 second를 만들 때, first가 아직 5번째 줄에서 쓰일 예정이었어요. 쓰는 사람이 동시에 둘이었던 거죠. first.push_str을 위로 옮기면 second가 생기기 전에 first가 끝나니까, &mut는 언제나 한 번에 하나만 있게 돼요.
- 대여(
&)를 쓰면 소유권을 넘기지 않고 값을 사용할 수 있어요. 값은 다시 돌아와요. - 그냥
&참조자는 읽기 전용이에요. 빌린 값을 바꾸려면&mut를 쓰세요. - 참조자를 통해 값을 바꾸려면 변수, 매개변수, 호출하는 곳 모두에
mut/&mut가 필요해요. - 규칙: 읽는 사람 여럿 또는 쓰는 사람 하나. 둘이 동시에 있을 수는 없어요.
- 대여는 참조자가 마지막으로 쓰인 곳에서 끝나요. 그래서 코드 순서만 바꿔도 대여 에러가 풀리는 경우가 많아요.
- 이 모든 건 컴파일할 때 검사되니까, 실행할 때는 비용이 들지 않아요.
이해도 점검
섹션 제목: “이해도 점검”이해도 점검
1. 함수에 값을 넘길 때 값 앞에 &를 붙이면 어떻게 되나요?
2. 대여 규칙에 따르면, 동시에 가질 수 있는 조합은 무엇인가요?
3. 참조자를 통해 값을 바꾸려면 무엇이 필요한가요?
4. Rust가 실행 성능을 전혀 잃지 않고 대여 규칙을 강제할 수 있는 이유는 무엇인가요?
점수: 0 / 4
더 알아보기
섹션 제목: “더 알아보기”- 참조와 대여 — The Rust Programming Language 한국어판: 공식 책의 대여 장이에요. 댕글링 참조와, Rust가 그런 참조를 왜 만들지 못하게 하는지도 다뤄요.
- Borrowing — Rust by Example (영어):
&대여를 보여 주는 실행 예제예요. 이어지는 페이지에서 가변 대여와 에일리어싱도 다뤄요. - Non-lexical lifetimes — The Edition Guide (영어): 대여가 닫는 중괄호가 아니라 마지막으로 쓰인 곳에서 끝나도록 바뀐 변경 사항을 설명해요.
- 슬라이스 — The Rust Programming Language 한국어판: 문자열이나 배열의 일부분만 대여하는 방법이에요. 참조자 다음으로 배우기 딱 좋은 주제예요.
reference— 표준 라이브러리 문서 (영어):&T와&mut T가 무엇인지, 어떤 트레이트를 구현하는지 알려 줘요.