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 패턴을 사용하기로 했다.