5. 빌림, 참조, 슬라이스, 라이프타임 입문
지난 장에서 우리는 Rust의 소유권을 배웠습니다. 값은 주인이 하나이고, 대입하거나 함수에 넘기면 소유권이 이동할 수 있으며, 주인이 스코프를 벗어나면 값이 정리된다고 했습니다. 여기까지 이해하면 Rust의 절반쯤은 들어온 셈입니다.
하지만 곧바로 이런 생각이 듭니다. "매번 소유권이 이동하면 너무 불편한데?", "함수에 문자열 길이만 재보라고 넘겼는데 왜 원래 값을 못 쓰지?" 바로 이 지점에서 등장하는 것이 빌림과 참조입니다. 이것은 소유권을 없애는 기능이 아니라, 소유권 규칙 위에서 더 유연하게 코드를 쓰게 해 주는 장치입니다.1
이번 장은 Rust 초보자가 가장 많이 멈추는 구간입니다. 대신 여기만 제대로 넘기면 이후의 문자열, 컬렉션, 구조체, 에러 처리까지 훨씬 편해집니다.

왜 참조가 필요한가
먼저 아주 흔한 상황부터 봅시다.
fn main() {
let s = String::from("hello");
let len = calculate_length(s);
println!("길이: {}", len);
// println!("문자열: {}", s); // 오류
}
fn calculate_length(text: String) -> usize {
text.len()
}이 코드는 calculate_length에 s를 넘기는 순간 소유권이 함수로 이동합니다. 그래서 함수 호출 뒤에는 s를 다시 쓸 수 없습니다. 길이만 재고 싶었는데 문자열 자체를 빼앗겨 버린 셈입니다.
이럴 때 참조를 씁니다.
fn main() {
let s = String::from("hello");
let len = calculate_length(&s);
println!("길이: {}", len);
println!("문자열: {}", s);
}
fn calculate_length(text: &String) -> usize {
text.len()
}&s는 s를 빌려서 보여 주는 참조입니다. 함수는 값을 소유하지 않고 잠깐 읽기만 합니다. 그래서 함수가 끝나도 원래 주인인 s는 그대로 남아 있습니다.1
쉽게 말하면 이렇습니다. 소유권은 집문서이고, 참조는 방문증입니다. 방문증이 있다고 집 주인이 바뀌지는 않습니다.
참조와 빌림은 무엇인가
참조는 어떤 값을 가리키는 안전한 접근 수단입니다. 포인터처럼 어떤 데이터를 따라가서 사용할 수 있지만, Rust에서는 이 참조가 유효한 값만 가리키도록 컴파일러가 검사합니다.1
참조를 만드는 행위를 빌림(borrowing)이라고 부릅니다.
fn main() {
let name = String::from("Rust");
let r = &name;
println!("name = {}", name);
println!("r = {}", r);
}여기서 name이 진짜 주인이고, r은 name을 빌려 본 참조입니다. 중요한 점은 r이 끝나도 name은 사라지지 않는다는 것입니다. 참조는 소유자가 아니기 때문입니다.1
불변 참조: 읽기 전용으로 빌리기
기본 참조는 불변 참조입니다. 즉, 읽을 수는 있지만 바꿀 수는 없습니다.
fn main() {
let s = String::from("hello");
print_text(&s);
print_text(&s);
}
fn print_text(text: &String) {
println!("{}", text);
}불변 참조는 동시에 여러 개 만들어도 괜찮습니다.
fn main() {
let s = String::from("rust");
let r1 = &s;
let r2 = &s;
let r3 = &s;
println!("{}, {}, {}", r1, r2, r3);
}왜 괜찮을까요? 모두 읽기만 하기 때문입니다. 책을 여러 사람이 동시에 읽는다고 책 내용이 바뀌지는 않습니다.
가변 참조: 수정 권한까지 빌리기
값을 바꾸고 싶다면 가변 참조가 필요합니다. 이때는 두 가지가 모두 필요합니다.
원래 값이
mut이어야 한다.참조도
&mut이어야 한다.
fn main() {
let mut s = String::from("hello");
change(&mut s);
println!("{}", s);
}
fn change(text: &mut String) {
text.push_str(", world");
}이제 함수는 문자열을 빌려서 수정할 수 있습니다. 하지만 아무 때나 마구 수정할 수 있게 두지는 않습니다. Rust는 여기서 아주 엄격한 규칙을 적용합니다.1
Rust의 빌림 규칙: 읽는 사람은 여럿, 쓰는 사람은 하나
초보자에게 가장 중요한 규칙은 딱 두 줄입니다.
어떤 시점에는 여러 개의 불변 참조를 가질 수 있다.
또는 하나의 가변 참조만 가질 수 있다.
둘을 동시에 마음대로 섞을 수는 없습니다.1
예를 들어 아래 코드는 오류입니다.
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &mut s; // 오류
println!("{}, {}, {}", r1, r2, r3);
}왜 막을까요? 누군가는 읽고 있는데 다른 누군가가 동시에 값을 바꿔 버리면, 읽는 쪽이 예상하던 상태가 깨질 수 있기 때문입니다. Rust는 이런 충돌을 실행 중에 운 좋게 피하는 방식이 아니라, 애초에 컴파일 자체를 막는 방식으로 처리합니다.1
가변 참조를 두 개 동시에 만드는 것도 안 됩니다.
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // 오류
println!("{}, {}", r1, r2);
}이 규칙은 단순히 까다로우라고 만든 것이 아닙니다. 같은 데이터를 동시에 여러 경로로 수정할 때 생길 수 있는 위험을 막기 위한 장치입니다.1
같은 스코프인데도 되는 경우가 있다
여기서 초보자가 한 번 더 놀랍니다. 아래 코드는 됩니다.
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} {}", r1, r2);
let r3 = &mut s;
r3.push_str("!");
println!("{}", r3);
}분명 같은 main 안인데 왜 될까요?
핵심은 참조의 수명은 단순히 중괄호 끝까지가 아니라, 실제 마지막으로 사용되는 지점까지라는 점입니다. 위 코드에서는 r1, r2가 println!에서 마지막으로 쓰인 뒤 더는 사용되지 않습니다. 그래서 그다음 줄에서 r3라는 가변 참조를 만드는 것이 허용됩니다.1
이 감각이 매우 중요합니다. Rust는 "선언된 위치"만 보는 것이 아니라 "언제까지 실제로 쓰이는가"도 봅니다.
빌림 오류를 읽는 눈 만들기
빌림 관련 에러를 보면 처음에는 숨이 턱 막힙니다. 하지만 대부분은 같은 질문으로 풀립니다.
"지금 이 값에 접근하는 사람이 몇 명이지?"
그리고 그 사람들은 무엇을 하고 있나요?
읽기만 하는가?
수정하려는가?
아직 사용이 끝나지 않았는가?
이 세 가지를 보면 대부분의 빌림 에러는 방향이 보입니다. Rust 컴파일러는 성격이 엄격할 뿐, 억울한 판결을 내리는 편은 아닙니다.
댕글링 참조를 Rust가 막는 방식
참조에는 또 하나의 규칙이 있습니다. 참조는 항상 유효해야 합니다.14
다음 코드를 봅시다.
fn main() {
let r;
{
let x = 10;
r = &x;
}
// println!("{}", r); // 오류
}안쪽 블록이 끝나면 x는 사라집니다. 그런데 r이 계속 x를 가리키게 두면, 이미 사라진 값을 가리키는 위험한 참조가 됩니다. 이런 것을 댕글링 참조라고 합니다. Rust는 이런 코드를 컴파일 단계에서 막습니다.4
이 점이 Rust 참조의 강력함입니다. 다른 언어에서는 런타임 버그가 될 수 있는 문제가, Rust에서는 아예 프로그램이 빌드되지 않습니다.
슬라이스: 전체가 아니라 일부만 빌리기
이제 참조의 특별한 형태인 슬라이스(slice)를 봅시다. 슬라이스는 "전체 데이터를 빌리는 것"이 아니라 "그중 일부 구간만 빌리는 것"입니다.2
문자열에서는 주로 &str 형태를 봅니다.
fn main() {
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
println!("{}", hello);
println!("{}", world);
}여기서 hello와 world는 새로운 문자열을 만든 것이 아닙니다. 원래 문자열 s의 일부분을 가리키는 슬라이스입니다.2
문법도 자주 줄여 씁니다.
let a = &s[..5]; // 0..5
let b = &s[6..]; // 6부터 끝까지
let c = &s[..]; // 전체즉, 슬라이스는 "복사 없이 일부만 빌려 보는 방법"입니다.
String과 &str는 어떻게 다를까
초보자가 정말 자주 헷갈리는 조합입니다.
String은 문자열 데이터를 소유하는 타입입니다. 길이가 늘어나거나 내용이 바뀔 수 있습니다. 반면 &str는 문자열의 일부 또는 전체를 빌려서 보는 문자열 슬라이스입니다. &str 자체는 소유자가 아닙니다.3
예를 들어 문자열 리터럴은 원래 &str입니다.
let a: &str = "hello";
let b: String = String::from("hello");실무에서는 함수 매개변수로 &String보다 &str를 더 자주 씁니다. 더 유연하기 때문입니다.
fn print_name(name: &str) {
println!("{}", name);
}
fn main() {
let s = String::from("Rust");
let literal = "Borrowing";
print_name(&s);
print_name(literal);
}이 함수는 String도 받을 수 있고 문자열 리터럴도 받을 수 있습니다. 초보자 기준으로는 이렇게 기억하면 좋습니다.
String은 내 문자열 상자, &str는 그 상자 안을 잠깐 들여다보는 창문입니다.
문자열 슬라이스에서 특히 조심할 점
문자열 슬라이스는 아주 편리하지만, 하나 꼭 기억해야 할 것이 있습니다. Rust 문자열은 UTF-8이고, 슬라이싱 인덱스는 글자 수가 아니라 바이트 위치 기준이라는 점입니다.3
영어만 있을 때는 별문제가 없어 보입니다.
let s = String::from("hello");
let h = &s[0..1];하지만 한글은 한 글자가 여러 바이트를 차지할 수 있습니다. 그래서 글자 경계를 잘못 자르면 런타임에 오류가 날 수 있습니다.3
예를 들어 "안녕" 같은 문자열을 무심코 바이트 단위로 잘라 버리면, 글자 중간을 잘라 버리는 일이 생길 수 있습니다. 그래서 문자열 슬라이스를 쓸 때는 "문자 개수"와 "바이트 위치"가 다를 수 있다는 점을 꼭 기억해야 합니다.
처음에는 이렇게 안전하게 접근하면 됩니다.
영어 ASCII 예제로 슬라이스 감을 먼저 익힌다.
한글을 다룰 때는 바이트 인덱싱을 섣불리 하지 않는다.
복잡한 문자열 처리는 나중에
chars()나 관련 메서드를 함께 배운다.
슬라이스가 useful한 이유: 소유권 없이 필요한 조각만 전달하기
슬라이스의 진짜 장점은 함수 설계에서 드러납니다.
fn first_five(text: &str) -> &str {
&text[..5]
}이 함수는 문자열을 소유하지 않습니다. 그저 입력으로 받은 문자열의 앞부분을 다시 빌려서 돌려줍니다. 복사도 거의 없고, 소유권 이동도 없습니다. Rust가 좋아하는 방식입니다.
즉, 슬라이스는 "필요한 만큼만 빌려서 쓰기"의 대표 예시입니다.
라이프타임은 왜 등장할까
이제 많은 초보자가 긴장하는 단어가 나옵니다. 라이프타임(lifetime).
이름부터 무겁지만, 입문 단계의 핵심은 간단합니다. 라이프타임은 참조가 얼마나 오래 유효한지에 대한 정보입니다.4
사실 모든 참조에는 라이프타임이 있습니다. 다만 대부분은 컴파일러가 알아서 추론해 줍니다.4 그래서 평소에는 못 느낍니다. 문제가 되는 것은 여러 참조의 관계를 컴파일러가 혼자 판단하기 어려울 때입니다.
라이프타임의 목적은 수명을 늘리는 것이 아닙니다. 이미 있는 참조들이 서로 어떤 관계인지 설명해서, 잘못된 참조를 막는 것입니다.4
이 문장은 꼭 기억해 두세요.
라이프타임은 수명 연장 마법이 아니라, 참조 관계를 적는 계약서입니다.
반환값이 참조일 때 왜 어려워질까
예를 들어 이런 함수는 안 됩니다.
fn dangle() -> &String {
let s = String::from("hello");
&s
}왜냐하면 s는 함수가 끝나는 순간 사라지기 때문입니다. 그런데 그 참조를 바깥으로 돌려주면, 밖에서는 이미 없어진 값을 가리키게 됩니다. Rust는 이것을 허용하지 않습니다.14
이럴 때는 보통 두 가지 방향이 있습니다.
하나는 소유권이 있는 값 자체를 반환하는 것입니다.
fn make_text() -> String {
let s = String::from("hello");
s
}다른 하나는 입력으로 받은 참조 중 하나를 다시 반환하는 것입니다. 이때는 반환되는 참조가 입력 참조와 어떤 관계인지 설명해야 할 수 있습니다.4
가장 유명한 라이프타임 예제: 더 긴 문자열 고르기
아래 함수는 얼핏 보면 자연스럽습니다.
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}하지만 Rust는 이 함수에 불만을 가질 수 있습니다. 반환되는 &str가 x에서 온 것인지, y에서 온 것인지, 그리고 그 참조가 얼마나 오래 유효한지 함수 시그니처만 보고는 확신하기 어렵기 때문입니다.4
그래서 이렇게 씁니다.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}처음 보면 'a가 마치 주문처럼 보입니다. 하지만 뜻은 의외로 단순합니다.
"x와 y는 어떤 라이프타임 'a 동안 유효하고, 반환값도 그 'a 동안만 유효하다고 약속한다."4
더 쉽게 말하면, 반환값은 입력 둘 중 더 짧게 살아남는 쪽에 맞춰서만 안전하게 쓸 수 있다는 뜻입니다.4
즉, 'a는 시간을 늘려 주는 게 아니라 안전한 사용 범위를 표시해 줍니다.
라이프타임을 너무 무서워할 필요는 없다
초보자 기준에서 중요한 사실이 하나 있습니다. 실제로는 라이프타임을 매번 직접 적지 않습니다. 대부분의 함수에서는 컴파일러가 알아서 처리합니다.4
처음에는 다음 정도만 확실히 잡으면 충분합니다.
참조는 원본보다 오래 살 수 없다.
함수가 지역 변수의 참조를 반환하는 것은 보통 안 된다.
입력 참조를 다시 반환하는 함수에서는 라이프타임 표기가 필요할 수 있다.
이 정도만 이해해도 이미 아주 큰 진전입니다.
초보자를 위한 실전 체크리스트
빌림과 라이프타임에서 막혔을 때는 아래 순서로 점검해 보세요.
이 함수가 정말 소유권을 가져가야 하는가, 아니면 참조만 받아도 되는가?
지금 동시에 존재하는 참조가 여러 개인가, 그중 수정하는 참조가 있는가?
이 참조가 가리키는 원본 값이 더 먼저 사라지지는 않는가?
&String보다&str가 더 유연한 선택은 아닌가?
이 네 가지를 확인하면 초보자 에러의 상당수를 스스로 풀 수 있습니다.
이 장의 핵심 정리
이번 장은 Rust의 체감 난도가 확 올라가는 구간이지만, 핵심 논리는 일관됩니다. 소유권이 값의 책임자를 정한다면, 빌림은 그 값을 안전하게 함께 쓰는 방법입니다.
참조는 소유권을 옮기지 않고 값을 사용하게 해 줍니다. 불변 참조는 여러 개 가능하지만 읽기 전용입니다. 가변 참조는 수정할 수 있지만 한 시점에 하나만 허용됩니다. 슬라이스는 전체가 아니라 일부만 빌리는 방법이고, 문자열에서는 &str가 아주 자주 쓰입니다. 그리고 라이프타임은 참조가 유효한 관계를 컴파일러에게 알려 주는 장치입니다.14
처음에는 규칙이 많아 보여도, 실제로는 한 문장으로 요약할 수 있습니다.
Rust는 값을 여러 곳에서 보게 할 수는 있지만, 동시에 위험하게 바꾸게 하지는 않는다.
이 감각만 잡히면, 이제 Rust의 문자열과 컬렉션도 훨씬 덜 무섭습니다. 다음 장에서는 이 빌림 감각을 바탕으로, 구조체와 열거형으로 데이터를 더 사람답게 모델링해 보겠습니다.
참고
1References and Borrowing - The Rust Programming Language
2References & Borrowing | WebReference
3In-Depth Guide to Working with Strings in Rust
4Validating References with Lifetimes - The Rust Programming Language
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.