Rust Lifetime
Rust에서는 함수 시그니처에 파라미터가 2개 이상일 때, lifetime을 명시적으로 표기해야 한다.
(규칙에 따라 생략 가능한 경우도 있다)
아무튼, 함수 입력 파라미터에 있는 참조값과 반환 참조값의 수명 관계에 대해서 생각해보다가
떠오른 모순적인 상황
fn longest<'a, 'b: 'a>(x: &'a str, y: &'b str) -> &'a str {
if x.len() > y.len() {
x
} else {
y // 'b의 라이프타임이 'a보다 크거나 같아야 함
}
}
일단 문자열 리터럴 a와 b를 비교해서 긴 쪽을 반환하는 함수를 가정해 보자.
이 함수에 두 가지 파라미터가 들어왔을 때, 수명과 문자열 길이(반환되는 조건)는 서로 관련이 없다.
즉, 어떤 게 반환될지를 우리는 이 시점에 알 수 없다.
난 이 '함수 시그니처에 수명 표기하기'를 봤을 때, 뭔가 추상적인 반례 가닥 같은 게 떠올랐다.
예를 들어, 수명이 더 긴 y가 x보다 길이가 더 길어서 y를 반환하게 되었다고 생각해 보자.
그리고 이 반환값을 y의 수명(기니까)에 맞게 코드 내에서 계속 이용하려 한다.
그런데 어느 순간 x의 짧은 수명이 다 되어 drop() 돼버렸다.
그때 같은 ‘a 수명을 가진 반환값도 함께 사라졌다.
y의 수명은 더 길어서 아직 이용할 부분이 더 남았는데도 불구하고 갑자기 수명이 다 된 것이다.
근데 이러면 안 되잖아?
뭔가 추상적으로 반례가 있을 것 같아서, 이런 상황이 생길 수 있지 않을까 계속 고민했다.
그런데 사실은 개념에만 집중한 나머지, 본질적인 의미(전체적인 그림)를 잊었던 거다.
우선 나는 x와 y를 함수 fx()에 넣어 나온 반환 값이 더 쓸 곳이 남았는데도 불구하고 사라지는 문제를 고민하고 있었다.
하지만 이 고민은 잘못되었다.
왜냐하면 그런 상황은 있을 수 없기 때문이다.
‘a, ‘b 수명이 어떻든 함수를 호출하는 사람은 x와 y 중 어떤 것이 반환될지 알 수 없다.
그럼 수명이 짧은 쪽이 나오게 될 수 있음에도, 짧은 수명을 가진 참조를 넘어서는 반환값이 계속해서 쓰이는 상황을 당연히 만들 수 없다는 의미다.
그리고 반환값 수명 개념 하나에만 몰두해서 기능적인 맥락을 잊고 있었다.
애초에 그 함수를 기능적으로 왜 썼을까?
x와 y를 가지고 그 참조들과 밀접한 관계를 가진(의미가 있는) 값을 받으려 했던 거다.
그런데 반환받은 값이 가리키는 원본이 사라지는 시점엔 반환값은 더 이상 의미가 없다.
내가 떠올린 불편한 상황 요약
- 수명이 더 짧은 x가 나온다 → 이건 뭐 문제없지!
- 수명이 더 긴 y가 나온다 → y만큼 살아야 해, 근데 x만큼 살면 이후에 못 쓰잖아!
이건 2번을 반증하면 내 찝찝함을 완벽하게 해결할 수 있다.
호출 시점엔 함수의 반환값이 x가 될지, y가 될지는 알 수 없다.
그럼 파라미터(x or y)와 밀접하게 관련 있는 반환값의 수명은 당연히 짧은 쪽을 기준으로 맞춰야 한다.
그 함수를 쓴 의미와 목적을 잘 생각해 보면, 반환값이 쓰이는 지점이 짧은 쪽 수명을 넘기는 건 이상하다.
치킨집 비유로 함수 파라미터 lifetime 설명
치킨의 수명은 20분(집에 있는 사람이 다 먹어치우는 시간)이라 가정하자.
멕시카나 치킨은 배달이 20분 걸리고, 교촌 치킨은 배달이 40분 걸린다.
이런 상황에서 9시에 멕시카나랑 교촌 중에 먹고 싶은 거 아무거나 동생한테 주문하라 해놓고,
어느 게 올지도 모르면서 ‘난 9시 40분 넘어서 치킨 먹어야지~’하면,
멕시카나 치킨이 이미 20분에 도착해서 동생이 다 먹어버린 뒤일 수도 있다는 거다.
어떤 집 치킨이 9시 20분에 도착할지, 9시 40분에 도착할지 모르는 데,
특정 치킨만 생각하면서 아~ 난 40분에 먹을 거임ㅋㅋ 하는 건 불확실하다는 말이다.
안전빵으로 내가 먹고 싶은 치킨을 먹으려면, 20분에 가는 게 맞아~ 안 맞아?
‘40분 넘어서 멕시카나 먹을 거야~’ 생각하는 게 말이 돼~ 안 돼?
결국 그건 답정너, 확증편향처럼 어떤 값이 나올지 정해두고(물론 결과는 정해지진 않음) 값을 쓰겠다는 의미니까,
한쪽 drop() 이후에도 반환값을 이용하는 코드를 만드는 이 상황 자체가 모순이라는 것.
그런 상황이 생기더라도 컴파일러가 참조 오류를 잡아낼 테고 말이다.
또 다른 예시
오늘은 10월 3일, 10월까지 장사하는 멕시카나, 12월까지 장사하는 교촌 매장이 있다고 가정하자.
동아리원들이랑 행사 뒤풀이 회식에서 치킨을 먹기로 했다.
그리고 배달비를 아끼기 위해서 한 매장에서만 치킨을 시키기로 했다.
어떤 치킨을 주문할지 고르기 위해서 함수를 사용하기로 했다.
매장의 전화번호 각 자릿수 합을 비교해서 더 큰 매장의 전화번호를 반환하는 함수를 만들었다.
근데 행사 뒤풀이 회식이 11월이다?
그래서 애초에 잘못된 가정이었다고 하는 거다.
11월에 주문할 매장을 선정하면서, 10월까지만 하는 멕시카나 매장을 선택지에 두면 안 되니까.
만약, 멕시카나 매장의 전화번호가 반환되었다고 하더라도, 11월에 그 전화번호는 아무런 의미가 없다.
멕시카나 매장은 11월이 되기 전에 이미 drop()되었기 때문이다.
그래서 본질적으로 의미도 없어지고, 참조가 가리키는 원본이 없어 컴파일러가 오류로 잡아낸다.
Borrow checker와 Lifetime의 관계 설명
호출하는 사용자 입장에선 A와 B 중 어떤 것의 수명이 더 긴지 고려해서 넣을 필요는 없다.
Borrow checker가 자동으로 수명을 비교해서 알맞게 넣어주기 때문이다.
Rust에서 lifetime 개념이 있는 건 2가지 목적이 있다.
- Borrow Checker가 참조를 검사하고 수명을 파악해서 안전하게 메모리 관리를 하려고.
=> Rust에 가비지 컬렉터가 필요 없는 이유 - 댕글링 포인터같은 참조 오류가 생기지 않게 하려고.
결론&요약
자 그럼 오늘의 결론,
‘둘 중 어떤 값이 반환될지 모르는 상황에서 그 둘보다 길게 살아남을 수 없다’
무조건 짧은 수명에 맞춰서 반환하는 게 맞다는 것.
그 반환값을 쓰는 이유와 함수를 쓰는 목적에 대해 곰곰이 생각해 보면 알 수 있다.
그리고 어디까지나 lifetime '표기'일 뿐, 우리가 참조의 실제 수명을 '변경'할 수는 없다.
하지만 'static로 프로세스가 끝날 때까지 유효한 참조를 가리킬 수 있지 않냐고?
'static도 가리키는 데이터가 그만큼 살아남을 수 있을 때만 쓸 수 있다.
결국 lifetime 자체는 데이터 소유권이랑 관련 있다는 말이지~
'Rust 연구 노트 > Rust 이야기' 카테고리의 다른 글
[Rust/도서 리뷰] <이지 러스트> (1) | 2025.03.31 |
---|---|
[Rust/도서 리뷰] <러스트 프로페셔널 코드> (6) | 2024.09.10 |
[Rust] Rust 소개, 설치 및 IDE 환경 세팅 + Cargo (0) | 2024.07.15 |