동작하는 코드
빌림(borrowing)은 값을 복사 없이 사용하는 좋은 방법이에요. 그런데 빌린 값을 너무 오래 들고 있으면 문제가 생겨요.
fn main() {
let result;
{
let text = String::from("안녕");
result = &text;
} // text가 여기서 사라져요!
// println!("{result}"); // 사라진 값을 가리키고 있어요!
}
주석을 풀면 에러가 나요. text는 중괄호 안에서 사라졌는데, result는 여전히 그 값을 가리키고 있거든요. 이게 바로 댕글링 참조(dangling reference) 예요 — 모듈 08에서 배운 그 위험한 상황이에요!
Rust는 이걸 컴파일할 때 잡아줘요. 어떻게? 수명(lifetime) 을 추적해서예요.
"왜?" — 대출 기한
수명을 대출 기한으로 생각하면 쉬워요.
- 도서관에서 책을 빌리면 대출 기한이 있어요
- 기한 안에는 마음껏 읽을 수 있지만, 기한이 지나면 반납해야 해요
- 빌린 값(참조)도 마찬가지예요 — 원본이 살아있는 동안만 유효해요
fn main() {
let text = String::from("안녕하세요"); // text의 수명 시작
let r = &text; // r은 text를 빌림
println!("{r}"); // OK! text가 살아있으니까
} // text와 r 모두 끝
이 코드는 문제없어요. r이 text를 빌렸고, text가 살아있는 동안에만 r을 사용했으니까요.
함수에서 참조를 반환할 때
문제는 함수가 참조를 반환할 때 생겨요.
fn longer(s1: &str, s2: &str) -> &str {
if s1.len() >= s2.len() {
s1
} else {
s2
}
}
error[E0106]: missing lifetime specifier
--> src/main.rs:1:35
|
1 | fn longer(s1: &str, s2: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value,
but the signature does not say whether it is borrowed
from `s1` or `s2`
해석: "반환값이 빌린 값인데, s1에서 빌린 건지 s2에서 빌린 건지 모르겠어요."
해결: 수명 표기로 "반환값의 수명은 입력과 같다"고 알려주세요.
컴파일러 입장에서 생각해보세요. s1을 반환할 수도 있고 s2를 반환할 수도 있어요. 반환된 참조가 언제까지 유효한지 알 수 없으니까 에러를 내는 거예요.
수명 표기 'a
수명 표기로 "이 참조들은 같은 기간 동안 유효하다"고 알려줄 수 있어요.
fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() >= s2.len() {
s1
} else {
s2
}
}
fn main() {
let a = String::from("hello");
let b = String::from("hi");
let result = longer(&a, &b);
println!("더 긴 쪽: {result}");
}
'a는 수명 매개변수예요. "반환값의 수명은 s1과 s2 중 짧은 쪽과 같다"라는 뜻이에요.
수명 표기가 복잡해 보이지만, 걱정하지 마세요. 핵심만 기억하면 돼요.
수명 표기는 참조가 얼마나 오래 유효한지 컴파일러에게 알려주는 힌트예요.
수명 생략 규칙 — 대부분은 안 써도 돼요
사실 수명을 직접 써야 하는 경우는 많지 않아요. Rust 컴파일러가 수명 생략 규칙(lifetime elision rules) 에 따라 알아서 추론해줘요.
// 이 함수는 수명 표기가 필요 없어요
fn first_word(s: &str) -> &str {
match s.find(' ') {
Some(i) => &s[..i],
None => s,
}
}
입력 참조가 하나뿐이면 "반환값의 수명 = 입력의 수명"이라고 컴파일러가 추론해요. 입력이 여러 개일 때만 직접 써야 해요.
안심하세요 — 대부분의 코드에서 수명을 직접 쓸 일은 거의 없어요. 컴파일러가 알아서 해줘요!
구조체에 참조를 넣으려면
모듈 12에서 구조체에 &str을 넣으려다 에러가 난 적 있죠? 이제 그 이유를 알 수 있어요.
struct Quote<'a> {
text: &'a str,
author: &'a str,
}
fn main() {
let text = String::from("배움에는 끝이 없다");
let author = String::from("공자");
let quote = Quote {
text: &text,
author: &author,
};
println!("\"{}\" - {}", quote.text, quote.author);
}
구조체가 참조를 가지면, 그 참조의 수명을 명시해야 해요. "Quote는 'a 동안 살아있는 참조를 가지고 있다"라는 뜻이에요.
하지만 대부분의 경우 구조체에는 String을 쓰는 게 더 간단하고 안전해요. &str은 정말 필요할 때만 쓰세요.
더 알아보기: 'static 수명
'static은 프로그램 전체 동안 유효한 수명이에요. 문자열 리터럴이 대표적이에요.
fn main() {
let s: &'static str = "이 문자열은 프로그램 끝까지 살아요";
println!("{s}");
}
"문자열 리터럴"은 프로그램 바이너리에 포함되어 있어서 프로그램이 실행되는 내내 유효해요. 그래서 'static 수명을 가져요.
연습 1. 아래 코드가 왜 컴파일되지 않는지 설명하고, 수정해보세요.
fn main() {
let r;
{
let x = 5;
r = &x;
}
println!("{r}");
}
힌트: x의 수명과 r이 사용되는 시점을 비교해보세요.
연습 2. 아래 함수에 수명 표기를 추가해서 컴파일되도록 만들어보세요.
fn first_or_default(s: &str, default: &str) -> &str {
if s.is_empty() {
default
} else {
s
}
}
fn main() {
let result = first_or_default("hello", "기본값");
println!("{result}");
}
힌트: 반환값이 s일 수도 default일 수도 있으니, 두 입력의 수명을 연결해야 해요.
Q1. 수명(lifetime)이란?
- A) 변수가 메모리에서 차지하는 크기
- B) 참조가 유효한 범위
- C) 프로그램의 실행 시간
- D) 함수가 호출되는 횟수
Q2. 아래 코드에서 'a의 역할은?
fn pick<'a>(s1: &'a str, s2: &'a str) -> &'a str {
s1
}
- A) 새로운 타입을 정의한다
- B) 반환값의 수명이 입력 참조들과 연결됨을 나타낸다
- C) 메모리를 직접 관리한다
- D) 함수를 제네릭으로 만든다
Q3. 수명을 직접 써야 하는 가장 흔한 경우는?
- A) 변수를 선언할 때
- B)
println!을 사용할 때 - C) 함수가 여러 참조 중 하나를 반환할 때
- D) 정수를 사용할 때