Rust 라이프타임
라이프타임(lifetime)은 Rust를 처음 배우는 사람이 처음으로 진짜 벽을 만나는 지점인 경우가 많아요. 그러니 이 페이지에서는 부드럽게 시작할게요. 목표는 라이프타임의 모든 규칙을 익히는 게 아니라, 라이프타임이 무엇이고 컴파일러가 왜 그걸 요구하는지 알아보는 것뿐이에요.
라이프타임이 해결하는 문제
섹션 제목: “라이프타임이 해결하는 문제”대여에서 배웠듯이, 참조자(&)를 쓰면 값을 소유하지 않고도 사용할 수 있어요. 하지만 참조자는 그것이 가리키는 값이 존재하는 동안에만 유효해요. Rust는 이미 정리된 무언가를 가리키는 참조자가 절대 생기지 않도록 확인해야 해요. 이런 버그를 댕글링 참조(dangling reference)라고 하는데, C 같은 언어에서 프로그램이 죽는 대표적인 원인이에요.
대부분은 Rust가 코드의 스코프를 보고 알아서 판단하기 때문에, 여러분이 신경 쓸 일이 없어요(스코프 페이지에서 본 게 바로 이거예요). 하지만 가끔, 특히 함수에서는 컴파일러가 혼자서 알아낼 수 없어서 여러분에게 직접 적어 달라고 해요. 그게 바로 라이프타임 명시(lifetime annotation)예요.
Rust가 도움을 필요로 하는 함수
섹션 제목: “Rust가 도움을 필요로 하는 함수”두 문자열 슬라이스 중 더 긴 쪽을 반환하는 함수예요.
fn longest(a: &str, b: &str) -> &str { if a.len() > b.len() { a } else { b }}이 코드는 컴파일되지 않아요. 에러는 이렇게 나와요.
error[E0106]: missing lifetime specifier“라이프타임 지정자가 빠졌다”는 뜻이에요. 문제는 반환되는 참조자가 a에서 빌린 것일 수도 있고 b에서 빌린 것일 수도 있는데, Rust는 함수만 읽어서는 어느 쪽인지 알 수 없다는 거예요. 그래서 돌려받은 참조자를 쓸 때까지 그게 여전히 유효하다고 보장할 수 없어요.
라이프타임 명시 추가하기
섹션 제목: “라이프타임 명시 추가하기”라이프타임에 이름을 붙이고(관례적으로 'a처럼 소문자 한 글자를 써요), 반환값의 라이프타임이 두 매개변수와 연결되어 있다고 Rust에게 알려 주면 해결돼요.
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str { if a.len() > b.len() { a } else { b }}
fn main() { let s1 = String::from("hello"); let s2 = String::from("hi");
let result = longest(&s1, &s2); println!("더 긴 쪽: {result}");}실행 결과:
더 긴 쪽: hello여기서 문자열을 영어로 둔 건 .len()이 글자 수가 아니라 바이트 수를 비교하기 때문이에요. 한글은 한 글자가 3바이트라서 결과가 헷갈릴 수 있어요.
'a는 값이 아니라 이름표라고 생각하세요. 실제 기간을 정하는 게 아니에요. 그저 “이 참조자들은 모두 적어도 같은 코드 구간 동안은 유효해야 해”라고 Rust에게 알려 줄 뿐이에요. 그러면 컴파일러가 이 함수를 호출하는 모든 곳에서 그게 정말 맞는지 검사해요.
이게 왜 중요할까요?
섹션 제목: “이게 왜 중요할까요?”이 명시가 없으면 컴파일러는 longest가 돌려주는 참조자가 원래 문자열 중 하나보다 더 오래 살아남을지 정말로 알 수 없어요. 라이프타임 명시 덕분에 컴파일러는 돌려받은 참조자를 언제나 안전하게 쓸 수 있다는 걸 증명할 수 있어요. 그것도 컴파일할 때, 실행 비용 없이요.
참조자를 담는 구조체
섹션 제목: “참조자를 담는 구조체”다음으로 라이프타임을 만나게 되는 곳은 구조체예요. 지금까지는 구조체의 모든 필드가 String처럼 데이터를 소유했어요. 필드가 참조자일 수도 있지만, 그러면 구조체는 그 참조자가 가리키는 데이터가 살아 있는 동안에만 유효해요. Rust는 그걸 라이프타임으로 적게 해요.
struct Highlight<'a> { text: &'a str, // 다른 곳에 있는 String에서 빌려 와요}
fn main() { let book = String::from("Call me Ishmael. Some years ago..."); let first = Highlight { text: &book[..15] };
println!("하이라이트: {}", first.text); println!("책 전체는 {}바이트예요", book.len());}실행 결과:
하이라이트: Call me Ishmael책 전체는 34바이트예요Highlight<'a>는 “라이프타임 'a 동안 무언가를 빌리는 Highlight“라고 읽으면 돼요. &book[..15] 부분은 슬라이스예요. book의 처음 15바이트를 복사하지 않고 빌려 와요. 바이트 위치로 자르고 바이트 수를 출력하니까, 이 문장(『모비 딕』의 첫 문장이에요)은 영어로 두었어요. first는 빌리기만 하니까 그 뒤에도 book을 쓸 수 있어요.
라이프타임이 있기 때문에 Rust는 이런 실수를 잡아낼 수 있어요. 구조체가 아직 String 안을 가리키고 있는데 String이 해제되는 경우예요.
struct Highlight<'a> { text: &'a str,}
fn main() { let highlight; { let book = String::from("Call me Ishmael. Some years ago..."); highlight = Highlight { text: &book[..15] }; } // 여기서 book이 해제되는데...
println!("{}", highlight.text); // ...highlight는 아직 그 안을 가리키고 있어요}error[E0597]: `book` does not live long enough --> src/main.rs:9:40 | 8 | let book = String::from("Call me Ishmael. Some years ago..."); | ---- binding `book` declared here 9 | highlight = Highlight { text: &book[..15] }; | ^^^^ borrowed value does not live long enough10 | } // 여기서 book이 해제되는데... | - `book` dropped here while still borrowed11 |12 | println!("{}", highlight.text); // ...highlight는 아직 그 안을 가리키고 있어요 | -------------- borrow later used here“book이 충분히 오래 살지 못한다”는 에러예요. 10번째 줄에서 book이 해제되는데, 12번째 줄에서 여전히 빌린 값을 쓰고 있다고 알려 줘요.
실용적인 팁 하나: 구조체의 라이프타임이 자꾸 걸리적거린다면 &str 대신 소유하는 String을 저장하세요. 텍스트가 복사되긴 하지만, 구조체가 더 이상 다른 무언가에 기대지 않으니 라이프타임도 사라져요. 경험 많은 Rust 개발자들도 처음엔 이렇게 시작했다가, 이유가 생겼을 때만 참조자로 바꾸는 경우가 많아요.
좋은 소식
섹션 제목: “좋은 소식”여러분이 'a를 쓸 일은 이 페이지를 보고 짐작하는 것보다 훨씬 적어요. 컴파일러가 일상적인 상황 대부분에서 라이프타임을 추론할 수 있기 때문에, 명시적인 라이프타임이 전혀 필요 없는 Rust 코드도 아주 많아요. 주로 함수가 참조자를 반환하는데, 그게 어디서 왔는지 컴파일러가 혼자 알아내지 못할 때 만나게 돼요. 그럴 땐 이것만 기억하세요. 버그가 아니라, 컴파일러가 스스로 증명할 수 없는 것을 확인해 달라고 부탁하는 거예요.
연습 문제
섹션 제목: “연습 문제”1. 더 짧은 문자열
섹션 제목: “1. 더 짧은 문자열”문자열 슬라이스 두 개를 받아서 더 짧은 쪽을 반환하는 함수 shortest를 작성해 보세요. 길이가 같으면 첫 번째를 반환하세요. "elephant"와 "cat"으로 호출해 보세요.
힌트: 위의 longest와 모양이 같아요. 이름 뒤에 <'a>를 붙이고, 두 매개변수와 반환 타입 모두에 'a를 붙이세요.
정답 보기
fn shortest<'a>(a: &'a str, b: &'a str) -> &'a str { if a.len() <= b.len() { a } else { b }}
fn main() { let big = String::from("elephant"); let small = String::from("cat");
println!("더 짧은 쪽: {}", shortest(&big, &small)); println!("길이가 같으면 첫 번째: {}", shortest("dog", "cow"));}실행 결과:
더 짧은 쪽: cat길이가 같으면 첫 번째: dog반환되는 슬라이스는 a에서 올 수도 있고 b에서 올 수도 있어요. 그래서 Rust는 결과가 두 입력이 모두 살아 있는 동안에만 유효하다는 걸 'a로 알아야 해요. < 대신 <=를 쓰면 길이가 같을 때 a를 고르게 돼요.
2. 빌려 오는 구조체
섹션 제목: “2. 빌려 오는 구조체”&str 필드 두 개, text와 author를 가진 구조체 Quote를 만들어 보세요. text는 String "Fearless concurrency is a goal of Rust"의 처음 20바이트를 빌리고, author는 리터럴 "The Rust Book"이 되도록 하나 만드세요. 그리고 Fearless concurrency (The Rust Book)처럼 출력하세요.
힌트: 위의 Highlight처럼 구조체에 라이프타임이 필요해요. struct Quote<'a>로 선언하고, 두 필드 모두 &'a str로 하세요. &speech[..20]은 처음 20바이트의 슬라이스예요.
정답 보기
struct Quote<'a> { text: &'a str, author: &'a str,}
fn main() { let speech = String::from("Fearless concurrency is a goal of Rust"); let quote = Quote { text: &speech[..20], author: "The Rust Book", };
println!("{} ({})", quote.text, quote.author);}실행 결과:
Fearless concurrency (The Rust Book)quote는 speech에서 빌려 오기 때문에 speech보다 오래 살 수 없어요. 리터럴 "The Rust Book"은 프로그램 안에 박혀 있어서 실행되는 내내 살아 있으니, 'a에 필요한 만큼 충분히 오래 살아요. speech를 안쪽 { } 블록 안에서 만들고, 위의 에러 예제처럼 블록이 끝난 뒤에 quote를 출력해 보세요. 컴파일러가 막는 걸 직접 볼 수 있어요.
- 라이프타임은 참조자들이 서로 어떤 관계인지 컴파일러에게 알려 줘서, 댕글링 참조를 막을 수 있게 해요.
- 컴파일러가 스스로 추론할 수 없을 때만 직접 쓰면 돼요. 가장 흔한 경우는 참조자를 반환하는 함수예요.
'a는 기간을 정하지 않아요. 여러 참조자의 라이프타임을 연결해서 컴파일러가 검사할 수 있게 할 뿐이에요.- 참조자를 담는 구조체에도 라이프타임이 필요하고(
struct Highlight<'a>), 빌려 온 데이터보다 오래 살 수 없어요. 소유하는String을 저장하면 이 문제를 피할 수 있어요. - 모두 컴파일할 때 하는 검사라서, 실행 비용은 전혀 없어요.
이해도 점검
섹션 제목: “이해도 점검”이해도 점검
1. 라이프타임은 어떤 문제를 해결하나요?
2. fn longest(a: &str, b: &str) -> &str가 라이프타임 명시 없이 컴파일되지 않았던 이유는 무엇인가요?
3. 라이프타임 명시를 추가하면 실행할 때 비용이 드나요?
4. 이 페이지에 따르면, 명시적인 라이프타임을 가장 자주 써야 하는 때는 언제인가요?
점수: 0 / 4
더 알아보기
섹션 제목: “더 알아보기”- 라이프타임으로 참조자의 유효성 검증하기 — The Rust Programming Language 한국어판: 공식 책의 자세한 설명이에요. 구조체의 라이프타임, 라이프타임 생략 규칙,
'static도 다뤄요. - Functions — Rust by Example (영어): 라이프타임을 명시한 함수의 짧은 예제와, 컴파일러가 거부하는 경우를 보여 줘요.
- Structs — Rust by Example (영어): 참조자를 담는 구조체와 열거형 예제가 더 있어요.
- Lifetime elision — The Rust Reference (영어): 컴파일러가 라이프타임을 대신 채워 넣을 때 쓰는 정확한 규칙이에요. 언제 라이프타임을 생략해도 되는지 알 수 있어요.