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

문제 상황

레거시 코드를 Java 8로 마이그레이션하면서 Stream API를 적용하던 중 checked exception 처리가 막혔다. 파일을 읽는 로직을 Stream으로 변환하려 했는데, IOException이 람다 시그니처와 맞지 않아 컴파일 에러가 발생했다.

// 컴파일 에러 발생
files.stream()
    .map(file -> new String(Files.readAllBytes(file)))
    .collect(Collectors.toList());

해결 방법

1. Wrapper 메서드 사용

가장 간단한 방법은 checked exception을 unchecked로 감싸는 것이다.

private String readFile(Path file) {
    try {
        return new String(Files.readAllBytes(file));
    } catch (IOException e) {
        throw new UncheckedIOException(e);
    }
}

files.stream()
    .map(this::readFile)
    .collect(Collectors.toList());

2. Function 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);
            }
        };
    }
}

// 사용
files.stream()
    .map(ThrowingFunction.wrap(file -> 
        new String(Files.readAllBytes(file))))
    .collect(Collectors.toList());

3. Optional 활용

예외 발생 시 빈 값으로 처리하고 싶다면 Optional을 사용했다.

files.stream()
    .map(file -> {
        try {
            return Optional.of(new String(Files.readAllBytes(file)));
        } catch (IOException e) {
            return Optional.<String>empty();
        }
    })
    .filter(Optional::isPresent)
    .map(Optional::get)
    .collect(Collectors.toList());

결론

실무에서는 Wrapper 유틸을 만들어 공통 라이브러리에 추가했다. Vavr 같은 라이브러리도 있지만, 팀 내 외부 의존성 추가가 까다로워 직접 구현했다. Stream API가 편리하지만 예외 처리는 아직 아쉬운 부분이다.