[원문] https://medium.com/@alexeynovikov_89393/stop-using-exceptions-in-java-456f7db46ea4
Stop using Exceptions in Java
All of us know that GOTO was deprecated and is considered absolute evil. Moreover, any jumping instruction is bad because it breaks the…
medium.com
GOTO 문법은 더이상 사용되지 않고, 나쁜 것으로 취급받고 있다. GOTO뿐만 아니라, 점프를 하는 동작은 코드의 순차적인 논리 흐름을 깨는 것이기 때문에 좋지 않다. 특정한 위치로 점프를 하는 것은 집중력을 잃게하고, 전체적인 흐름을 읽는 것을 어렵게 만든다. 왜냐하면 우리는 변수나 컨텍스트의 현재 상태를 추적하면서 이해하기 때문이다.
그렇다면, 우리는 비즈니스 로직에서 왜 Exception을 사용할까?
에러와 관련된 클래스들의 상속 관계부터 하나씩 살펴보자.
이미 익숙하겠지만, Error는 특별한 케이스다. Error는 더 이상 정상적인 진행이 불가능할 때, 또는 JVM을 멈추고 싶을 때 사용한다. Error를 상속받은 클래스들은 위 그림에서 초록색으로 표시했다.
또한, Exception을 상속받은 RuntimeException과 자식 클래스들도 초록색으로 표시했고, 나머지는 빨간색이다.
초록색 클래스들은 unchecked exception으로, 코드 내에서 잡아서 명시적으로 처리해서는 안된다. 빨간색 클래스들은 checked exception으로, 메소드 선언에서 표시하거나 명시적으로 처리를 해야한다.
대부분의 유저들은 RuntimeException을 상속받아 unchecked exception을 사용하기를 선호한다. 심지어 어떤 API가 메소드 선언에서 checked exception을 선언한 경우에도 Lombok의 SneakyThrows 어노테이션으로 무시하기도 한다. Spring Web은 exception들을 처리하기 위한 handler를 만들어주는 ControllerAdvice를 제공한다.
이쯤되니 궁금해진다. 비즈니스 로직 구현에서 GOTO를 사용해서 점프하는 것과 Exception handler를 사용하는 것이 뭐가 달라?
실제로, 예외적인 상황이 발생하지 않는다면, 우리는 왜 Exception을 사용하는가? 예를 들어, HTTP API 요청을 했을 때 DB에서 레코드를 찾을 수 없으면 EntityNotFoundException을 던지고, ControllerAdvice에서는 exception을 처리하여 클라이언트에게 404 NotFound 에러 코드를 보내는 것은 흔하다. 이러한 경우에 exception이 진짜 필요할까? 이것이 예외적인 경우일까? 아니면 웹 어플리케이션에서 흔한 경우일까?
왜 exception이 발생한 코드를 보는 것이 아니라, handler를 찾기 위해서 코드 전체를 뒤져야 하나?
게다가, Throwable의 기본 생성자는 스택 트레이스 정보를 수집하기 위해 네이티브 메소드를 호출하고, 그것을 객체에 저장한다. 단순히 엔티티를 찾을 수 없다는 정보를 알려주기 위해서 엄청난 CPU와 메모리 낭비가 발생하는 것이다.
[참고] https://www.baeldung.com/java-exceptions-performance
벤치마크 테스트는 exception을 사용하는 것이 단순히 어떤 값을 반환하는 것보다 몇 배로 느리다는 것을 보여준다.
참 별로다. 내가 해결책을 제시해 보겠다.
새롭게 발명할 것은 없다. 비교적 최근의 언어들은 비즈니스 로직에서 발생할 수 있는 예외들을 처리하는 구닥다리 방법에서 새로운 방식으로 바뀌었기 때문이다. 그러한 방법들은 공통점이 많다.
Scala나 Rust, Go를 보면 공통적인 컨셉을 가지고 있다는 것을 쉽게 눈치챌 수 있다. 이러한 언어들은 예외를 함수의 결과값으로 반환한다. Go는 여러 가지 반환값을 지원하고, Rust는 Result<R, E>같은 구조체가 있다.
Java도 이미 비슷한 것을 가지고 있다. Optional 클래스가 바로 그것인데, 딱 하나의 값을 가지거나, 값을 가지지 않는다. 왜 Java 개발자들은 Rust의 Result같은 것을 가질 수 없겠는가? 우리도 할 수 있다! 이게 바로 내가 제안하는 해결책이다.
monad를 도입하는 것은 일반적인 코드를 작성할 때나 예외를 다룰 때 모두 유용하다. 아래 글을 참고하자.
public class Result<T> {
private Optional<T> value;
private Optional<String> error;
private Result(T value, String error) {
this.value = Optional.ofNullable(value);
this.error = Optional.ofNullable(error);
}
public static <U> Result<U> ok(U value) {
return new Result<>(value, null);
}
public static <U> Result<U> error(String error) {
return new Result<>(null, error);
}
public boolean isError() {
error.isPresent();
}
public T getValue() {
return value.get();
}
public String getError() {
return error.get();
}
}
우리는 이것을 스트림 API나 Spring Web의 ResponseEntity에서 쉽게 사용할 수 있다. 기존 코드의 흐름과 의도는 전혀 바뀌지 않는다. 대신 코드가 더 직관적이고, 성능상의 문제도 없어진다.
나는 이러한 접근법을 선호하고, Optional이 도입될 때 왜 표준 라이브러리에 Result같은 클래스가 도입되지 않았는지 이해할 수 없다.
어쨌든, 코드를 어떻게 작성할 것인가는 본인의 판단이다. 그러나 나는 개인적으로 Java의 세계를 개선하고 싶다. 긴 글을 읽어줘서 고맙다.
'기타 > 번역' 카테고리의 다른 글
[Java] Java 19는 게임 체인저다 (0) | 2023.03.09 |
---|