- Published on
Rust Iterator 체인 lifetimes 오류 6가지 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
Rust에서 iter()와 map()/filter()/flat_map()/collect()를 엮다 보면, 소유권보다 더 골치 아픈 게 lifetime입니다. 특히 Iterator 체인은 중간 단계가 익명 타입이고, 클로저가 환경을 캡처하며, 임시값이 섞이기 쉬워서 컴파일러가 내는 에러가 한 번에 이해되기 어렵습니다.
이 글은 Iterator 체인에서 자주 터지는 lifetime 오류를 6가지 대표 패턴으로 분류하고, 각 패턴마다
- 어떤 상황에서 발생하는지
- 컴파일러가 보통 어떤 메시지를 내는지
- 안전하게 고치는 방법(가능하면 여러 옵션)
을 코드로 정리합니다.
참고로 Rust의 동시성 이슈도 결국 수명과 소유권이 얽히는 경우가 많습니다. async 코드에서 비슷한 감각을 잡고 싶다면 Rust async 데드락? Tokio Mutex·spawn 원인 7가지도 같이 보면 도움이 됩니다.
1) 임시값에서 빌린 참조를 Iterator로 반환
증상
함수 안에서 String/Vec 같은 임시 소유 값을 만들고, 그 안을 iter()로 빌려서 Iterator를 반환하려고 하면 거의 100% 터집니다.
fn words_iter() -> impl Iterator<Item = &str> {
let s = String::from("hello rust");
s.split_whitespace()
}
흔한 에러 형태
cannot return value referencing local variable류returns a value referencing data owned by the current function
원인
반환하려는 Iterator가 s를 빌립니다. 그런데 s는 함수가 끝나면 drop 됩니다. 즉, 반환된 Iterator가 가리킬 대상이 사라집니다.
해결 1: 소유값을 반환하고, 호출자가 빌리게 하기
fn words_owner() -> String {
String::from("hello rust")
}
fn main() {
let s = words_owner();
let it = s.split_whitespace();
for w in it {
println!("{w}");
}
}
해결 2: 참조를 입력으로 받아서 lifetime을 외부로 연결
fn words_iter<'a>(s: &'a str) -> impl Iterator<Item = &'a str> {
s.split_whitespace()
}
fn main() {
let s = String::from("hello rust");
for w in words_iter(&s) {
println!("{w}");
}
}
해결 3: 참조 대신 소유 아이템을 만들기
fn words_vec() -> Vec<String> {
let s = String::from("hello rust");
s.split_whitespace().map(|w| w.to_string()).collect()
}
Iterator를 꼭 반환해야 한다면, 빌린 참조를 반환하지 말고 소유 아이템으로 바꿔서 반환하는 게 가장 단순합니다.
2) map에서 지역 변수의 참조를 만들어 반환
증상
map 클로저에서 String을 만들고 그 참조를 &str로 반환하려고 하면 터집니다.
fn main() {
let v = vec!["a", "bb", "ccc"];
let it = v.iter().map(|s| {
let tmp = s.to_string();
tmp.as_str()
});
for x in it {
println!("{x}");
}
}
원인
tmp는 클로저 호출이 끝나면 drop 됩니다. 그런데 tmp.as_str()는 tmp 내부를 가리키는 참조입니다. 즉, 각 요소마다 dangling reference가 됩니다.
해결 1: 소유 타입으로 바꾸기
fn main() {
let v = vec!["a", "bb", "ccc"];
let it = v.iter().map(|s| s.to_string());
let out: Vec<String> = it.collect();
println!("{out:?}");
}
해결 2: Cow로 필요할 때만 할당
use std::borrow::Cow;
fn main() {
let v = vec!["a", "bb", "ccc"];
let out: Vec<Cow<'_, str>> = v
.iter()
.map(|s| {
if s.len() >= 2 {
Cow::Owned(s.to_string())
} else {
Cow::Borrowed(*s)
}
})
.collect();
println!("{out:?}");
}
Iterator 체인에서 참조를 유지하려면, 참조가 가리키는 원본의 수명이 체인 밖까지 살아 있어야 합니다. 클로저 내부 지역 변수 참조는 거의 항상 금지라고 생각해도 됩니다.
3) flat_map에서 임시 컨테이너를 만들고 그 iter()를 반환
증상
flat_map은 내부에서 또 다른 Iterator를 반환해야 하는데, 그 Iterator가 임시 Vec/String을 빌리면 lifetime 에러가 납니다.
fn main() {
let nums = vec![1, 2, 3];
let it = nums.iter().flat_map(|n| {
let tmp = vec![n, n];
tmp.iter()
});
for x in it {
println!("{x}");
}
}
원인
tmp.iter()는 tmp를 빌립니다. 그런데 flat_map이 요구하는 내부 Iterator는 클로저 밖에서도 소비될 수 있으므로 tmp가 살아있어야 합니다. 하지만 tmp는 클로저 호출 종료 시 drop.
해결 1: 내부 Iterator가 소유하도록 만들기
가장 쉬운 방법은 vec![..].into_iter()처럼 소유 Iterator를 반환하는 것입니다.
fn main() {
let nums = vec![1, 2, 3];
let out: Vec<i32> = nums
.iter()
.flat_map(|n| vec![*n, *n].into_iter())
.collect();
println!("{out:?}");
}
해결 2: std::iter::once/repeat 등으로 할당 자체를 제거
use std::iter;
fn main() {
let nums = vec![1, 2, 3];
let out: Vec<i32> = nums
.iter()
.flat_map(|n| iter::once(*n).chain(iter::once(*n)))
.collect();
println!("{out:?}");
}
flat_map에서 임시 컨테이너를 만들었다면, 90%는 iter()가 아니라 into_iter()가 정답입니다.
4) collect 결과를 참조로 받거나, 중간 Vec를 바로 iter()로 연결
증상 A: collect() 결과를 참조로 받기
fn main() {
let v = vec![1, 2, 3];
let r: &Vec<i32> = &v.iter().map(|x| x + 1).collect();
println!("{}", r.len());
}
원인
collect()로 만들어진 Vec는 임시값이고, 그 임시값에 대한 참조를 r에 저장하려 합니다. 문장이 끝나면 임시값이 drop 됩니다.
해결
중간 결과를 변수에 소유로 저장하세요.
fn main() {
let v = vec![1, 2, 3];
let tmp: Vec<i32> = v.iter().map(|x| x + 1).collect();
let r: &Vec<i32> = &tmp;
println!("{}", r.len());
}
증상 B: collect한 뒤 바로 iter()로 체인
fn main() {
let v = vec![1, 2, 3];
let it = v
.iter()
.map(|x| x + 1)
.collect::<Vec<i32>>()
.iter();
for x in it {
println!("{x}");
}
}
원인
iter()가 빌리는 대상 Vec가 임시값이라 바로 drop 됩니다.
해결
역시 중간 Vec를 변수로 빼거나, 애초에 중간 Vec를 만들지 않도록 로직을 바꾸세요.
fn main() {
let v = vec![1, 2, 3];
let tmp: Vec<i32> = v.iter().map(|x| x + 1).collect();
for x in tmp.iter() {
println!("{x}");
}
}
Iterator 체인에서 collect()는 수명 경계를 만드는 연산입니다. collect()로 만든 값을 빌릴 거면 반드시 소유 변수에 저장한 뒤 빌려야 합니다.
5) impl Iterator 반환에서 서로 다른 lifetime을 섞기
증상
입력 슬라이스를 필터링해서 반환하려는데, 조건에 따라 다른 버퍼를 섞거나(예: fallback) 외부 참조를 섞는 순간 lifetime이 꼬입니다.
예를 들어, 조건에 따라 원본에서 빌린 &str 또는 새로 만든 String의 &str을 섞어 반환하려는 시도는 구조적으로 불가능합니다.
fn choose<'a>(input: &'a [&'a str], fallback: &'a str) -> impl Iterator<Item = &'a str> {
input.iter().copied().map(|s| {
if s.is_empty() {
let tmp = fallback.to_string();
tmp.as_str()
} else {
s
}
})
}
원인
반환 타입이 &'a str이면 모든 요소가 'a 동안 유효해야 합니다. 그런데 tmp.as_str()는 클로저 내부 임시 String을 가리키므로 'a를 만족할 수 없습니다.
해결 1: 반환 아이템을 Cow로 바꾸기
use std::borrow::Cow;
fn choose<'a>(input: &'a [&'a str], fallback: &'a str) -> impl Iterator<Item = Cow<'a, str>> {
input.iter().copied().map(move |s| {
if s.is_empty() {
Cow::Owned(fallback.to_string())
} else {
Cow::Borrowed(s)
}
})
}
fn main() {
let input = vec!["a", "", "c"];
let out: Vec<Cow<'_, str>> = choose(&input, "x").collect();
println!("{out:?}");
}
해결 2: 아예 소유 String만 반환
fn choose_owned(input: &[&str], fallback: &str) -> impl Iterator<Item = String> + '_ {
input.iter().map(move |s| {
if s.is_empty() { fallback.to_string() } else { (*s).to_string() }
})
}
핵심은 한 Iterator의 Item은 한 가지 수명 규칙을 가져야 한다는 점입니다. 빌림과 소유를 섞고 싶으면 Cow 같은 합성 타입이 정석입니다.
6) 클로저 캡처로 인해 FnMut와 lifetime이 충돌하는 패턴
증상
Iterator 체인에서 외부 참조를 캡처한 뒤, 그 참조를 다시 빌리거나(가변/불변 혼합) 다른 단계에서 이동시키면 lifetime 관련 에러가 복합적으로 터집니다.
대표적으로 filter에서 외부 String을 빌려 쓰면서, 같은 스코프에서 그 String을 변경하려는 경우입니다.
fn main() {
let mut needle = String::from("a");
let v = vec!["a", "ab", "b"];
let it = v.iter().filter(|s| s.contains(needle.as_str()));
needle.push('x');
for s in it {
println!("{s}");
}
}
원인
filter 클로저가 needle.as_str()를 캡처하면서 needle을 불변으로 빌린 상태가 됩니다. 그런데 그 다음 줄에서 needle.push()로 가변 빌림이 필요해 충돌합니다. Iterator는 지연 평가이므로, it가 살아있는 동안 빌림이 유지됩니다.
해결 1: 필요한 값을 미리 복사해 클로저에 move로 넘기기
fn main() {
let mut needle = String::from("a");
let v = vec!["a", "ab", "b"];
let needle_snapshot = needle.clone();
let it = v.iter().filter(move |s| s.contains(needle_snapshot.as_str()));
needle.push('x');
let out: Vec<&&str> = it.collect();
println!("{out:?}");
}
해결 2: Iterator 소비를 먼저 끝내고 이후에 변경
fn main() {
let mut needle = String::from("a");
let v = vec!["a", "ab", "b"];
let out: Vec<&&str> = v.iter().filter(|s| s.contains(needle.as_str())).collect();
needle.push('x');
println!("{out:?}");
}
지연 평가 Iterator는 “클로저가 참조를 캡처하면 그 참조가 오래 산다”가 기본 전제입니다. 따라서 캡처할 값은 스냅샷으로 복제하거나, 소비 시점을 앞당겨 빌림을 빨리 끝내는 게 실무적으로 가장 안전합니다.
정리: Iterator 체인 lifetime 디버깅 체크리스트
Iterator 체인에서 lifetime 오류가 나면 아래를 순서대로 점검하면 해결 속도가 빨라집니다.
- 반환하려는 참조가 지역 변수(임시값)를 가리키는가
map/flat_map클로저 안에서 만든String/Vec의iter()를 반환하고 있지 않은가collect()결과를 바로 빌려서 다음 체인으로 넘기고 있지 않은가impl Iterator의Item이 빌림과 소유를 섞고 있지 않은가- 지연 평가 때문에 캡처된 참조의 수명이 생각보다 길어지는 지점을 놓치지 않았는가
- 필요하면
Cow/소유 타입으로 전환하거나,into_iter()로 소유 Iterator를 만들었는가
Rust의 lifetime 에러는 처음엔 장벽이지만, Iterator 체인에서는 결국 한 문장으로 귀결됩니다. “참조는 원본보다 오래 살 수 없다.” 체인 중간에 임시값이 끼는 순간 원본이 사라지므로, 소유로 바꾸거나 수명 연결을 외부로 끌어내는 방식으로 설계를 바꾸는 게 정답입니다.
추가로 Rust의 복잡한 실행 흐름(특히 async)에서 문제가 커지는 양상은 다르지만, 원리 자체는 비슷합니다. 필요하면 Rust async 데드락? Tokio Mutex·spawn 원인 7가지도 함께 참고하세요.