[Rust] 라이프타임
지난 포스팅에서는 제네릭에 대해 알아보았습니다. 이번 포스팅에서는 라이프타임에 대해 알아보도록 하겠습니다.
라이프타임(Lifetime)
라이프타임은 참조자가 유효한 범위를 정의하는 개념입니다. 모든 변수와 함수는 라이프타임을 갖습니다. 컴파일러는 모든 참조자의 라이프타임을 컴파일 시점에 분석하여, 참조자의 유효성을 보장하고 댕글링 참조를 방지합니다.
라이프타임은 보통 'a, 'b와 같은 형태로 표기합니다. 다음 예시를 보겠습니다.
fn main() {
let a; // ---------+-- 'a
// |
{ // |
let b = 10; // -+-- 'b |
a = &b; // | |
} // -+ |
// |
println!("a: {}", a); // |
} // ---------+
a의 라이프타임은 'a, b의 라이프타임은 'b로 표시했습니다. 내부 스코프에서 a는 b의 참조를 대여 받고, 내부 스코프가 닫힌 후에 a를 출력합니다. 하지만 위 예시는 다음과 같은 에러가 발생합니다.

a와 b는 서로 다른 라이프타임을 가지고 있습니다. 내부 스코프가 닫히는 시점에서 b가 drop되기 때문에 a를 출력하는 시점에서 b의 라이프타임은 더 이상 유효하지 않게 됩니다.
이를 해결하기 위해서는 다음과 같이 b의 라이프타임이 a의 라이프타임보다 크거나 같음을 보장해야합니다.
fn main() {
let b = 10; // ----------+-- 'b
// |
let a = &b; // --+-- 'a |
// | |
println!("a: {}", a); // | |
// --+ |
} // ----------+
라이프타임 'b는 'a보다 더 길기 때문에 a가 출력되는 시점에도 b에 대한 참조가 유효하게됩니다.
라이프타임 표기(Lifetime Annotation)
함수, 메서드, 구조체, 열거형 등에서 라이프타임을 명시적으로 지정할 수도 있습니다. 라이프타임은 어퍼스트로피 ( ' ) 뒤에 심볼을 표기하여 명시합니다.
enum Status<'a> {
Success(&'a u32),
Failed
}
struct Example<'a> {
x: &'a i32
}
fn example<'a>(x: &'a i32) -> &'a i32 {
x
}
impl <'a> Example<'a> {
fn ex(&self) -> &i32 {
self.x
}
}
라이프타임을 사용하기 위해서는 제네릭 문법과 같이 꺽쇠(<>) 안에 제네릭 라이프타임 매개변수를 선언 한 후,
참조자의 (&) 뒤에 위치하여 사용합니다.
라이프타임을 명시적으로 지정해주는 이유는 컴파일러가 참조의 유효성을 추론할 수 없는 특정 상황에서 컴파일러에게 해당 참조자가 안전하다는 것을 알려주기 위함입니다.
라이프타임을 명시적으로 지정해야하는 경우는 크게 두 가지 경우가 있습니다.
1. 함수의 반환값이 특정 라이프타임에 의존하는 경우
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
두 개의 문자열 참조를 인자로 받고 크기가 더 큰 문자열을 반환하는 함수 입니다.
위 코드를 실행시키면 다음과 같은 에러가 발생합니다.

x와 y가 서로 다른 라이프타임을 가지는 경우 컴파일러가 컴파일 시점에 어떤 라이프타임에 종속되는지 판단할 수 없기 때문입니다.
따라서 다음과 같이 라이프타임을 명시해주어야 합니다.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
x와 y, 그리고 반환 값에 라이프타임을 지정함으로써 반환 값이 최소한 'a 라이프타임 동안은 유효합니다. 함수의 반환 값이 x와 y 중 하나를 가르킬 것이며, 반환되는 참조가 입력값들보다 더 오래 살지는 않을 것이다라는걸 컴파일러에게 알려주는 것입니다. 또한 컴파일러는 'a를 x와 y 중 라이프타임이 더 짧은 쪽으로 간주합니다.
2. 구조체나 열거형이 참조를 가지는 경우
struct Example<'a> {
x: &'a i32
}
fn main() {
let x = 10;
let ex = Example {
x: &x
};
println!("{}", ex.x);
}
구조체, 열거형도 라이프타임을 지정할 수 있습니다. 이는 해당 인스턴스가 참조를 가지는 필드가 가르키는 원본 데이터보다 오래 살 수 없다는 의미입니다. 만약 인스턴스가 참조보다 오래 유지된다면 댕글링 참조가 발생할 수 있습니다.
라이프타임 생략(Lifetime Elision)
보통 참조를 다룰 때 라이프타임이 명시되지 않는 것을 알 수 있습니다. 라이프타임은 코드를 복잡하게 만드는 요소 중 하나이기 때문에 아래 규칙들을 충족하면 라이프타임을 생략할 수 있습니다.
1. 함수나 메서드의 각 매개변수는 자체적인 고유한 라이프타임 매개변수를 가집니다.
다음과 같이 함수의 매개변수에 라이프타임 명시가 생략된 경우 컴파일러는 각 매개변수에 고유한 라이프타임 매개변수를 할당합니다.
fn example(x: &str, y:&str) {}
위 함수는 컴파일러에 의해 다음과 같이 처리됩니다.
fn example<'a, 'b>(x: &'a str, y:&'b str) {}
2. 매개변수가 하나 인 경우 해당 매개변수의 라이프타임을 모든 반환 값에 적용합니다.
fn example<'a>(x: &'a str) -> &'a str {}
위 함수는 매개변수가 하나 이기 때문에 반환 값의 라이프타임은 x와 동일한 라이프타임을 가집니다.
3. 메서드가 2개 이상의 매개변수를 가지고, 그 중 &self 혹은 &mut self를 매개변수로 가진다면 self의 라이프타임을 모든 반환 값에 적용합니다.
impl<'a> Example {
fn foo(&'a self, x: &str) -> &'a str {}
}
x와 self의 라이프타임은 서로 다르지만, 3번 규칙에 따라 self의 라이프타임이 반환 값의 라이프타임으로 자동으로 지정됩니다.
다음 예시를 보겠습니다. 위에서 예시로 들었던 longest 함수 입니다.
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
위 3가지 생략 규칙을 적용해보면서 longest 코드가 에러가 나는 이유에 대해 알아보도록 하겠습니다.
longest 함수에 첫번째 규칙을 적용하면 다음과 같이 변경됩니다.
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
두 개의 매개변수에 각각 다른 라이프타임 매개변수가 할당됩니다. 매개변수가 2개이기 때문에 두 번째 규칙은 적용할 수 없습니다. 또한 longest 함수는 메서드가 아니기 때문에 세 번째 규칙도 적용할 수 없습니다. 모든 규칙을 적용했음에도 컴파일러는 반환 값의 라이프타임을 알아내지 못했습니다. 따라서 위 코드는 컴파일 에러가 발생하게 됩니다. 라이프타임을 명시해주어야 하는 경우가 생기는 이유는 생략규칙을 적용했음에도 모든 참조자의 라이프타임을 알아내지 못했기 때문입니다.
정적 라이프타임(Static Lifetime)
정적 라이프타임은 해당 참조자가 프로그램이 전체 실행 기간 동안 유효함을 의미합니다. 정적 라이프타임은 Rust에서 가장 긴 라이프타임이기 때문에 어떠한 라이프타임으로도 변환될 수 있습니다. 정적 라이프타임은 다음과 같이 'static 형태로 명시하여 사용합니다.
let s: &'static str = "Hello, Rust";
문자열 리터럴(&str)은 컴파일 시점에 프로그램의 실행 파일 내부에 직접 저장됩니다. 따라서 모든 문자열 리터럴은 'static 라이프타임을 가지며, 'static 표기를 생략할 수 있습니다.
또한 static 키워드로 정의된 상수 역시 'static 라이프타임을 가집니다.
static CONST: i64 = 10;
fn main() {
let i: &'static i64 = &CONST;
println!("{i}");
}
만약 구조체가 'static 라이프타임을 가지려면 구조체에 있는 모든 참조 필드가 'static 라이프타임이여야 합니다.
struct Data<'a> {
x: &'static str,
y: &'a str,
}
fn print_data(data: &'static Data<'static>) {
println!("{}, {}", data.x, data.y);
}
fn main() {
print_data(&Data {x: "Hello", y: "Rust"}); // success
print_data(&Data {x: "Hello", y: &String::from("Rust")}); // failed
}
x, y가 모두 문자열 리터럴을 가지는 경우 구조체의 모든 참조 필드가 'static 라이프타임이기 때문에 print_data가 정상적으로 호출됩니다. 하지만 String 타입은 'static 라이프타임보다 짧은 라이프타임을 가지기 때문에 해당 구조체는 String 타입 데이터의 라이프타임을 가지게 되어 print_data 호출 시 에러가 발생하게 됩니다.
하위 유형화(Subtyping)
하위 유형화는 하위 타입이 상위 타입을 대체하는 것을 말합니다. Rust는 객체지향 언어의 상속과 같은 명시적인 하위 유형화를 지원하지 않습니다. 대신 제네릭, 라이프타임, 트레이트를 통한 유연한 하위 유형화를 제공합니다. Rust는 서로 다른 라이프타임을 가진 참조 타입간 하위 유형화를 제공하는데, 긴 라이프타임을 가진 타입이 짧은 라이프타임을 가진 타입의 하위 유형이되는 개념입니다.
예를 들어, 서로 다른 라이프타임을 가진 &'a T 와 &'b T 가 있다고 가정했을 때, 'a 가 'b 보다 긴 경우 &'a T는 &'b T의 하위 유형(&'a T ⊆ &'b T) 관계가 됩니다. 이는 &'a T 타입의 값이 &'b T가 필요한 모든 곳에 사용될 수 있다는 것을 의미합니다.
fn bar<'a>() {
let s: &'static str = "hi";
let t: &'a str = s;
}
'static은 모든 라이프타임 중 가장 긴 라이프타임입니다. 'a는 'static 보다 라이프타임이 짧거나 같을 수 있지만 'static 보다 절대 길어질 수는 없습니다. 따라서 위 코드에서는 더 긴 라이프타임을 가진 &'static str이 &'a str 의 하위 유형이 됩니다.
변성(Variance)
변성은 하위 유형화의 규칙에 대한 속성입니다. 하위 유형화는 변성에 따라 다르게 동작하는데, 변성에는 공변성(Covraiant), 반공변성(Contravariant), 불변성(Invariant) 세 가지 유형이 있습니다.
1. 공변성(Covariant)
공변성은 T가 U의 하위 유형일 때 F<T>가 F<U>의 하위 유형임을 의미합니다. 공변성은 주로 데이터를 읽는 경우에 발생합니다.
fn covariant<'a>(a: &'a str, b: &'a str) {
println!("{a}, {b}");
}
fn main() {
let hello: &'static str = "hello";
{
let world = String::from("world");
let world = &world;
covariant(hello, world);
}
}
- 매개변수 a와 b는 'a라는 동일한 라이프타임을 가집니다.
- 서로 다른 라이프타임을 가진 hello('static) 와 world('world)가 함수의 인자로 전달됩니다.
- 'static <: 'world 이므로 &'static str은 &'world str의 하위 유형이 됩니다.
- 이때 hello는 &'static str에서 &'world str로 다운그레이드 됩니다.
2. 반공변성(Contravariant)
반공변성은 T가 U의 하위 유형일 때 F<U>가 F<T>의 하위 유형임을 의미합니다. 반공변성은 함수의 매개변수와 같이 주로 데이터를 쓰는 경우에 발생합니다.
fn print(input: &'static str) {
println!("{input}");
}
fn contravariant<'a>(input: &'a str, f: fn(&'a str)) {
f(input);
}
fn main() {
let hello: &'static str = "hello";
contravariant(hello, print);
{
let world = String::from("world");
contravariant(&world, print); // `world` does not live long enough
}
}
- 'static <: 'a 이므로 fn(&'static str)은 fn(&'a str)의 하위 유형이라고 가정해봅시다.
- fn(&'a str)이 fn(&'static str)로 대체 될 것입니다.
- fn(&'static str)에 호출되는 곳에 'a 라이프타임을 가진 참조가 인자로 전달됩니다.
- 긴 라이프타임('static)이 짧은 라이프타임('a)을 대체할 순 있지만 그 반대의 경우는 성립하지 않습니다.
- 따라서 hello는 'static 을 갖기 때문에 정상적으로 함수를 호출하지만, world는 'static 보다 짧은 'world를 갖기 not live long enough 에러가 발생합니다.
- 이러한 이유로 함수의 매개변수에서는 하위 유형 관계가 반대로 동작합니다.
따라서 위 코드가 정상적으로 동작하기 위해서는 'static을 'a로 대체하면 됩니다.
fn print<'a>(input: &'a str) {
println!("{input}");
}
fn contravariant<'a>(input: &'a str, f: fn(&'a str)) {
f(input);
}
fn main() {
let hello: &'static str = "hello";
contravariant(hello, print);
{
let world = String::from("world");
contravariant(&world, print);
}
}
'long <: 'short 관계일 때 fn(&'long T) 가 fn(&'short T)의 하위 유형인 경우 'short 라이프타임을 가진 참조가 인자로 넘어오면 댕글링 포인터와 같은 문제가 발생할 수 있기 때문에 반대로 fn(&'short T)가 fn(&'long T)의 하위 유형이 되는 것입니다.
3. 불변성(Invariant)
불변성은 T에 대해 하위 유형화 관계가 아님을 의미합니다.
fn invariant<T>(input: &mut T, val: T) {
*input = val;
}
fn main() {
let mut hello: &'static str = "hello";
{
let world = String::from("world");
invariant(&mut hello, &world); // `world` does not live long enough
}
println!("{hello}");
}
- T가 공변성이라고 가정했을 때, &'static str은 &'world str로 다운그레이드 되고 hello에 world가 할당됩니다.
- println!("{hello}") 을 호출하는 시점에서 world는 drop된 상태인데 hello는 world를 가르키고 있으므로 댕글링 포인터가 발생할 수 있습니다.
- 이러한 이유로 &mut T는 불변성을 가집니다.
변성은 아래 타입에 대해 다음과 같이 자동으로 결정됩니다.
| Type | Variance in 'a | Variance in T |
| &'a T | covariant | covariant |
| &'a mut T | covariant | invariant |
| *const T | covariant | |
| *mut T | invariant | |
| [T] and [T; n] | covariant | |
| fn() -> T | covariant | |
| fn(T) -> () | contravariant | |
| std::cell::UnsafeCell<T> | invariant | |
| std::marker::PhantomData<T> | covariant | |
| dyn Trait<T> + 'a | covariant | invariant |
이번 포스팅에서는 라이프타임에 대해 알아보았습니다. 다음 포스팅에서는 트레이트에 대해 알아보도록 하겠습니다.
참고
- https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html
- https://doc.rust-lang.org/stable/reference/lifetime-elision.html
- https://doc.rust-lang.org/nomicon/subtyping.html
- https://doc.rust-lang.org/reference/subtyping.html
- https://wikidocs.net/blog/@laniakea/1991/#158-subtyping
- https://wikidocs.net/blog/@laniakea/2017/#164-variance