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를 더 적극적으로 사용하게 됐다.