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

문제 상황

레거시 코드를 Stream API로 리팩토링하던 중 컴파일 에러를 마주쳤다.

List<String> urls = Arrays.asList("https://api.example.com/1", "https://api.example.com/2");

// 컴파일 에러 발생
List<Response> responses = urls.stream()
    .map(url -> httpClient.get(url)) // IOException 발생 가능
    .collect(Collectors.toList());

httpClient.get() 메서드가 IOException을 던지는데, Function 인터페이스는 Checked Exception을 허용하지 않았다.

해결 방법

1. try-catch로 감싸기

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

List<Response> responses = urls.stream()
    .map(url -> {
        try {
            return httpClient.get(url);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    })
    .collect(Collectors.toList());

2. Wrapper 함수 작성

재사용 가능한 유틸리티를 만들었다.

@FunctionalInterface
public interface ThrowingFunction<T, R> {
    R apply(T t) throws Exception;
}

public 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<Response> responses = urls.stream()
    .map(wrap(url -> httpClient.get(url)))
    .collect(Collectors.toList());

3. Optional로 실패 처리

예외 발생 시 빈 Optional을 반환하는 방식도 고려했다.

List<Response> responses = urls.stream()
    .map(url -> {
        try {
            return Optional.of(httpClient.get(url));
        } catch (IOException e) {
            logger.error("Failed to fetch: " + url, e);
            return Optional.<Response>empty();
        }
    })
    .filter(Optional::isPresent)
    .map(Optional::get)
    .collect(Collectors.toList());

선택

프로젝트에서는 Wrapper 함수 방식을 채택했다. 코드 재사용성이 높고 Stream의 가독성을 해치지 않았다. 다만 예외가 발생하면 전체 Stream이 중단되므로, 부분 실패를 허용해야 하는 경우에는 Optional 패턴을 사용하기로 했다.