Java 8 Stream API에서 예외 처리 패턴

문제 상황

레거시 시스템을 Java 8로 마이그레이션하면서 Stream API를 적극 활용하고 있다. 그런데 람다 표현식 내부에서 checked exception을 던지는 메서드를 호출할 때마다 컴파일 에러가 발생했다.

list.stream()
    .map(item -> item.process()) // IOException을 던지는 메서드
    .collect(Collectors.toList());

시도한 방법들

1. try-catch로 감싸기

가장 단순한 방법이지만 코드가 지저분해진다.

list.stream()
    .map(item -> {
        try {
            return item.process();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    })
    .collect(Collectors.toList());

2. Wrapper 유틸리티 작성

재사용 가능한 유틸 클래스를 만들었다.

@FunctionalInterface
public interface ThrowingFunction<T, R> {
    R apply(T t) throws Exception;
    
    static <T, R> Function<T, R> wrap(ThrowingFunction<T, R> f) {
        return t -> {
            try {
                return f.apply(t);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        };
    }
}

// 사용
list.stream()
    .map(ThrowingFunction.wrap(item -> item.process()))
    .collect(Collectors.toList());

3. 비즈니스 예외는 Optional로

실패가 예상되는 경우 Optional을 반환하도록 메서드를 설계했다.

list.stream()
    .map(this::safeProcess)
    .filter(Optional::isPresent)
    .map(Optional::get)
    .collect(Collectors.toList());

private Optional<Result> safeProcess(Item item) {
    try {
        return Optional.of(item.process());
    } catch (IOException e) {
        logger.warn("Failed to process: {}", item.getId(), e);
        return Optional.empty();
    }
}

결론

완벽한 정답은 없지만, 예외의 성격에 따라 패턴을 선택했다. 복구 불가능한 예외는 RuntimeException으로 wrapping하고, 비즈니스 로직상 실패 가능성이 있는 경우 Optional이나 Result 타입을 활용하는 방식으로 정리했다.