람다 Checked Exception 처리

Stream API와 람다식을 사용하면 for, if 를 사용한 코드를 간결하게 변경할 수 있다.
하지만 Checked Exception이 껴있으면 try-catch 문을 사용해서 핸들링을 해줘야하기 때문에 코드가 지저분해 보인다.
그래서 개인적으로 람다식에 Checked Exception을 던지는 메서드를 호출하는 것을 꺼린다.
아예 for 문을 사용할 때도 있고 또는 Unchecked Exception을 throw 하거나 catch 만하고 아무것도 처리하지 않는 wrapping method를 추가해 사용할 때가 있다.

이번 글은 이를 어떻게 처리해야 좋을 지에 대해 생각하면서 알아본 것들을 정리해본다.

왜 Checked Exception을 람다식에서 throw 할 수 없는지 그리고 어떤식으로 Checked Exception을 던지는 메서드를 람다식에서 사용할 지에 대해 알아보자.

왜 Checked Exception을 throw 할 수 없을까?

예제 코드를 보자, 파일의 경로를 받아와 해당 파일을 삭제하는 로직이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) {
List<String> filePaths = getFilePaths();
filePaths.stream()
.map(Paths::get)
.forEach(path -> {
try {
Files.deleteIfExists(path);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}

private static List<String> getFilePaths() {
return Arrays.asList(
"/some/directory/1",
"/some/directory/2",
"/some/directory/3",
"/some/directory/4");
}

코드는 정말 간단한데 try-cath 문이 추가되서 코드가 지저분해 진다.
왜 이렇게 Checked Exception을 핸들링해야하는 걸까?

java.util.function 패키지에 있는 주로 filter에 쓰이는 Predicate.test, 주로 forEach에 쓰이는 Consumer.accept 모두 Exception을 throw 하고 있지 않다.

1
2
3
4
5
6
7
8
9
10
11
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
// ...
}

@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
// ...
}

그렇기 때문에 Stream API를 사용할 때 람다식에 Checked Exception을 던지는 메서드의 경우 핸들링을 해줘야만 한다.

그렇기 때문에 Stream API를 사용할 때는 람다식에서 Checked Exception 핸들링을 해줘야하는 것이지
람다식을 사용하기 때문에 핸들링을 해줘야하는 것은 아니다.

람다식을 사용하더라도 람다식 내에서 핸들링하지 않아도 되는 예제 코드를 만들어봤다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// #1
@FunctionalInterface
public interface ThrowableFunctionalInterface {
void doSomething() throws Exception;
}

public class ThrowableAction {
static void doSomething(ThrowableFunctionalInterface throwableFunction) {
try {
throwableFunction.doSomething();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

public class Main {
public static void main(String[] args) {
ThrowableAction.doSomething(() -> Files.deleteIfExists(Paths.get("/some/directory/1")));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// #2
@FunctionalInterface
public interface ThrowableFunctionalInterface {
void doSomething() throws Exception;
}

public class ThrowableAction {
static void doSomething(ThrowableFunctionalInterface throwableFunction) throws Exception {
throwableFunction.doSomething();
}
}

public class Main {
public static void main(String[] args) throws Exception {
ThrowableAction.doSomething(() -> Files.deleteIfExists(Paths.get("/some/directory/1")));
}
}

여기서 ThrowableFunctionalInterface.doSomething도 Exception을 throw 하지 않으면 람다식에서 Files.deleteIfExists 메서드를 핸들링해야한다.


어떻게 Checked Exception을 람다식에서 처리할까?

앞서 예제로 들었던 첫번째 ThrowableFunctionalInterfaceConsumer를 조합한다고 생각해보자.
그러면 아래와 같이 Exception을 다루는 custom ConsumerWithException 인터페이스를 만들 수 있다.

1
2
3
4
@FunctionalInterface
public interface ThrowableFunctionalInterface {
void doSomething() throws Exception;
}
1
2
3
4
@FunctionalInterface
public interface ConsumerWithException<T, E extends Exception> {
void accept(T t) throws E;
}

다음으로 첫번째 예제의 ThrowableAction.doAction에 해당되는 Exception Wrapping class를 만들어보자.
개인적으로 나만의 Wrapper 클래스를 아래와 같이 만들었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ConsumerExceptionWrapper {
@FunctionalInterface
public interface ConsumerWithException<T, E extends Exception> {
void accept(T t) throws E;
}

public static <T, E extends Exception> Consumer<T> wrapper(ConsumerWithException<T, E> consumer) {
return arg -> {
try {
consumer.accept(arg);
} catch (Exception e) {
throw new RuntimeException(e);
}
};
}
}

이제 이 ConsumerExceptionWrapper 클래스를 사용하여 맨 처음의 예제를 아래와 같이 변경할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import static foo.bar.ConsumerExceptionWrapper.wrapper;

public static void main(String[] args) {
getFilePaths().stream()
.map(Paths::get)
.forEach(wrapper(Files::deleteIfExists));
}

private static List<String> getFilePaths() {
return Arrays.asList(
"/some/directory/1",
"/some/directory/2",
"/some/directory/3",
"/some/directory/4");
}

이를 토대로 Function, Predicate 등 여러 Exception Wrapper를 만들 수 있다.


나뿐만 아니라 다른 개발자들도 이러한 생각을 했다는 것이 신기하기도 했고
다른 개발자들의 고민의 결과를 보니 아직 갈 길이 멀었구나 생각이 든다.

이 포스팅에는 여러 블로그들을 참고해서 나만의 Wapper를 만들었기 때문에 아래 링크를 추가로 참고해서 본인만의 답을 찾아보길 바란다.

참고:

Share