Rust Iterator::map에서 Result가 실패하면 반복을 중단하고 에러 전파하기

1. 문제 정의

Rust에서 Iterator::map의 클로저가 Result<T, E>를 반환하는 시나리오는 매우 흔하다. 예를 들어 id 목록에서 각각을 조회하는 함수가 있다.

1
2
3
fn find(id: &Id) -> Result<Item, ItemError> {
    // ... 조회 로직
}

이를 다음과 같이 실행하면 문제가 생긴다.

1
2
3
4
let parent_items: Vec<Item> = parent_ids
    .iter()
    .map(|id| find(id).unwrap())
    .collect();

parent_ids 중 단 하나라도 조회에 실패하면 unwrap()은 즉시 panic을 일으켜 프로세스가 중단된다. 반대로 flat_map으로 바꾸면 실패가 잘못 무시된다. 이 글에서 다루는 질문은 다음과 같다.

“Iterator::map이 Result::Err를 반환할 때, 반복을 멈추고 에러를 반환하는 방법은 무엇인가?” (출처: StackOverflow 26368288)

2. 표면 증상과 원인 후보

표면 증상

접근 방식실패 시 동작에러 정보
.map(...).unwrap()즉시 panic → 프로그램 크래시panic이지만 복구 불가능, 직접 처리 불가
.flat_map(...)에러 요소를 조용히 건너뜀에러 정보 전부 유실
.collect::<Result<_,_>>()첫 Err에서 반복 중단 후 Err 반환에러를 위로 전파

원인 후보와 배제 과정

  • unwrap() / expect(): 에러를 절대 무시하지 않지만, Err일 때 panic을 일으켜 제어 흐름이 끊긴다. “반복을 멈추고 에러를 반환"이라는 요구를 충족하지 못한다. → 배제
  • flat_map(|id| find(id).into_iter()): Result::into_iter()ItemResult이므로 Ok(it)이면 1개 요소, Err이면 0개 요소를 낸다. 따라서 flat_map은 Err 요소를 무시(0개의 항목으로 취급)한다. 에러 정보가 유실되는 문제. → 배제 (핵심 함정)

3. 근본 원인 분석

핵심은 ResultFromIterator를 구현했다는 점이다.

Rust의 collect()는 타입이 FromIterator<T>를 구현하면 그 형태로 수집할 수 있다. Result<V, E>FromIterator<Result<A, E>>를 구현하며, 동작 방식은 다음과 같다.

  • 모든 요소가 Ok(a)이면 Ok(iter.collect::<V>())를 반환
  • 요소 중 하나라도 Err(e)이면 그 시점에 순회를 멈추고 해당 Err(e)를 그대로 반환

let items: Result<Vec<_>, _> = tasks.iter().map(find).collect(); 형태는 “첫 번째 실패에서 반복을 중단하고 에러를 위로 전파"라는 요구를 언어 차원에서 그대로 제공한다. 별도 루프나 상태 플래그가 필요하지 않다.

검증 환경 (재현 실험)

1
2
3
4
5
# Cargo.toml
[package]
name = "itermap_err"
version = "0.1.0"
edition = "2021"
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#[derive(Debug)]
struct Item;

type Id = String;

fn find(id: &Id) -> Result<Item, String> {
    // "1"은 성공, 그 외는 실패 — 첫 실패를 만들기 위한 재현 장치
    if id == "1" { Ok(Item) } else { Err(format!("not found: {:?}", id)) }
}

fn main() {
    let ids = vec!["1".to_string(), "2".to_string(), "3".to_string()];
    let items: Result<Vec<_>, _> = ids.iter().map(find).collect();
    println!("Result: {:?}", items);
    // => Result: Err("not found: \"2\"")
}

실행 결과: Err("not found: \"2\"")"2"에서 반복이 멈추고 에러가 반환된다. "3"는 절대 처리되지 않았기 때문에 반복 중단이 입증된다.

4. 코드 해결책 (Best Answer)

가장 간단하고 권장되는 방법은 collectResult 타입 힌트를 주는 것이다.

1
2
3
4
5
6
7
8
9
let items: Result<Vec<_>, _> = parent_ids
    .iter()
    .map(|id| find(id))
    .collect();

match items {
    Ok(items) => { /* 정상 */ }
    Err(e) => { /* 첫 번째 에러 처리 */ }
}

에러를 상위 함수로 그대로 전파하고 싶다면 ? 연산자와 함께 쓰는 것이 자연스럽다.

1
2
3
fn load_all(parent_ids: &[Id]) -> Result<Vec<Item>, ItemError> {
    parent_ids.iter().map(find).collect()
}

속도와 호환성: collect는 각 ResultFromIterator 구현을 통해 동작하므로 절대로 unwrap이나 flat_map의 오남용보다 더 안전하고, 핵심 실용 용례 대부분은 이를 표준 관용구로 채택한다.

해결책 비교

방식panic 유무에러 상실중단 시점권장
unwrap()panic하로 크래시없음(대신 크래시)실패 지점
flat_map없음있음 (에러 무시)없음 (계속)
collect::<Result<Vec<_>,_>>()없음없음첫 Err
collect + ?없음없음첫 Err, 상위 전파✓ (전파 시)

실패한 해결책 (초보 구현의 흔한 오류)

아래는 실무에서 흔히 보이지만 모두 원하는 동작을 만족하지 못하는 접근이다.

1
2
3
4
5
6
// ❌ 실패 시 panic → 크래시 (err_info 유실, 제어 흐름 붕괴)
let parent_items: Vec<Item> = ids.iter().map(|id| find(id).unwrap()).collect();

// ❌ 실패 시 에러를 조용히 버림 (err_info 유실)
// Result::into_iter()는 Err을 0개 요소로 처리하므로 flat_map이 건너뜀
let parent_items: Vec<Item> = ids.iter().flat_map(|id| find(id).into_iter()).collect();

unwrap은 테스트 환경에서만 허용되고, flat_map은 에러라는 운영 정보를 ‘성공만 남기기’ 때문에 두 방식 모두 실무 코드에서 사용하면 안 된다. 둘 다 “에러를 위로 전파"하라는 요구를 어긴다.

5. 향후 예방 조치 / 운영 예방책 / DevTrace 독자 결론

  • DevTrace 독자 결론: Result 요소를 순회할 때는 unwrapflat_map도 아니고, collect::<Result<Vec<_>,_>>()를 기본으로 선택한다. 이걸로 첫 실패에서 안전하게 중단하고 에러를 명시적으로 처리하며, ?만 붙이면 상위 함수로 전파까지 된다.
  • 체크리스트
    1. map 클로저가 Result를 반환하면 타입 어노테이션을 Result<Vec<_>, _>로 선언.
    2. 실패를 크래시로 만들지 말고 collect로 열거한다.
    3. 함수 경계에서는 ?로 전파, 최종 경계에서 match로 에러 응답.
    4. flat_map을 쓰고 있다면 실제로 에러가 ‘조용히’ 버려지고 있는지 점검.

출처: StackOverflow — How do I stop iteration and return an error when Iterator::map returns a Result::Err?