Java 8 Stream에서 예외 처리 깔끔하게 하기
문제 상황
레거시 시스템을 Java 8로 마이그레이션하면서 Stream API를 적극 도입했다. 그런데 람다식 내부에서 checked exception을 던지는 메서드를 호출할 때마다 try-catch 블록으로 감싸야 해서 코드가 지저분해졌다.
list.stream()
.map(item -> {
try {
return someMethodThrowsException(item);
} catch (Exception e) {
throw new RuntimeException(e);
}
})
.collect(Collectors.toList());
해결 방법
1. Wrapper 함수 작성
가장 직관적인 방법은 checked exception을 unchecked exception으로 변환하는 래퍼를 만드는 것이다.
@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 -> someMethodThrowsException(item)))
.collect(Collectors.toList());
2. 예외를 Optional로 변환
예외 발생 시 해당 항목을 건너뛰고 싶다면 Optional을 활용할 수 있다.
private <T, R> Function<T, Optional<R>> lifting(ThrowingFunction<T, R> f) {
return t -> {
try {
return Optional.of(f.apply(t));
} catch (Exception e) {
logger.warn("Exception during processing: {}", e.getMessage());
return Optional.empty();
}
};
}
// 사용
list.stream()
.map(lifting(item -> someMethodThrowsException(item)))
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());
프로젝트 적용
실제 프로젝트에서는 첫 번째 방법을 유틸리티 클래스로 만들어 공통으로 사용했다. Consumer, Predicate, Supplier 등 다른 함수형 인터페이스도 같은 방식으로 처리했다.
코드 가독성이 확실히 개선됐고, 팀원들도 Stream API를 더 적극적으로 사용하게 됐다.