Java의 Checked Exception은 실수다?

EP·2023년 2월 10일
11

Overview


자바를 처음 이해할 때 다들 꽤나 어려움을 겪었던 내용이 있습니다. 메서드 시그니처에 선언(declaration)하는 throwstry-catch 문(statement)의 차이입니다. 우선 둘의 차이를 비교하기전에 기본적인 내용을 짚고 가겠습니다.

자바에서는 발생할 수 있는 실패를 Error와 Exception으로 정의했습니다. 또 Exception을 Checked Exception과 Unchecked Exception으로 구분했죠. 애플리케이션을 개발하는 경우 Error를 다루는 경우는 거의 없습니다. 시스템적인 에러를 가지고 있기 때문입니다. 다만 IOException, SQLException 등으로 정의되어있는 Checked Exception과 NullPointerException, IllegalArgumentException 등으로 정의되어있는 Runtime Exception(Unchekced Exception)은 자바 개발자에게 익숙합니다.

Checked Exception은 애플리케이션이 예상하고 복구해야하는 예외적인 조건입니다. 가령 존재하지 않는 파일을 FileReader에게 읽으라는 명령을 했을 때 FileNoteFoundException이 발생할 것이고, 파일을 읽는 로직에서 흔하게 발생할 수 있느 예상이 되고 복구가 필요한 예외입니다. 자바는 이러한 예상 가능한 예외의 복구를 강제하기 위해 메서드 시그니처에 throws 키워드를 명시하는 기능을 추가했습니다.

Runtime Exception(Uncheked Exception)은 애플리케이션 내부적으로 예외적인 상황이므로 일반적으로 이 예외를 예상하거나 복구할 수 없습니다. 따라서 Runtime Exception은 언어 수준에서 복구를 강제하지 않습니다. 그림을 보면서 다시 확인하겠습니다.

The Three Kinds of Exceptions - Oracle

자바에서는 메서드 내 발생할 수 있는 Checked Exception을 throws하면 그 메서드를 호출하는 쪽에서 예외처리를 강제합니다. 처리를 하려면 그 메서드를 호출하는 쪽으로 또 throws를 하거나 try-catch 등을 해줘야 합니다.

하위 메서드에서 throws한 예외는 상위메서드에서 안잡으면 컴파일 에러가 발생한다.하위 메서드에서 넘어온 checked 예외는 또 다른 상위 메서드로 넘기거나
try-catch를 해줘야 한다.물론 하위 메서드에서도 throws나 try-catch를 하지 않으면 에러가 발생한다.

이런 언어의 기능을 이해하는데 크게 어렵지 않았습니다. 하지만 저희가 새로운 예외를 정의할 때, 언제 CheckedException을 상속받아야할지, 언제 Unchekced Exception을 상속받아야할지 고민을 많이 됐었습니다. 그런데 막상 실무에 들어서니 Checked Exception을 정의하는 일은 없었고 심지어 throws 해주는 경우도 점점 줄어들었습니다. 자연스레 Checked Exception에 대한 관심도 덜해졌죠.

그런데 코틀린을 배우면서 알게된 점은 코틀린은 checked exception 처리를 강제하지 않는다는 점이었습니다.

음? 이게 뭔가하고 ‘코틀린 인 액션’ 책을 찾아봤죠.

코틀린은 exception을 자바에게 상속받았음에도 불구하고 자바와 동작을 달리한다. 다른 최신 언어들처럼 코틀린 개발자도 Checked Exception을 포함하지 않기로 했다. - Kotlin In Action

마음에 걸리던 내용은 ‘다른 최신 언어들처럼’ 이라는 문구였습니다. Java는 Checked Exception을 언어적인 기능으로 넣었지만 그 이후 사회적 합의가 있었고 Kotlin은 그 내용을 반영해서 제외했다는 뜻으로 추론할 수 있었습니다. 다음은 코틀린 공식 문서입니다.


checked exception - kotlinlang.org

작은 프로젝트에서는 Checked Exceiption이 효과적일 수 있지만 큰 규모의 프로젝트에서는 생산성을 하락시키고 코드 퀄리티도 나아지지 않는다는 이야기였습니다. 추가적으로 아래의 두 글을 참조했습니다.

강한 어조로 checked exception을 반대하는 글이었습니다. 하지만 코틀린은 상호운용성을 위해 Checked Exception을 사용할 수 있는 방법(@Throws)을 제공하고 있습니다. Swift와 Objective-C에서도 상호운용을 할 수 있게 적용한다 하는데 이 언어들도 관련이 있나봅니다.

우선, 과거에 양립했었던 Checked Exception의 필요 유무에 대한 논쟁을 들여다 보겠습니다.

Checked Exception은 필요하다


2003년에 James Gosling을 Checked Exception에 대한 주제로 인터뷰한 내용입니다. 이 당시 일부 개발자는 Checked Exception은 강력한 애플리케이션을 구축하는 데 도움이 된다고 생각하고 반대로 생산성을 저해한다는 의견이 분분했던 시절입니다.

제임스 고슬링

  • C++ 프로그래밍에서 모든 함수는 return을 할 때 Error가 발생하는지 확인을 해야만 했다.
  • 그는 더 나은 방법을 필요하다고 판단하여 자바에 exceptions 이라는 개념을 포함했다.
  • 그의 Checked Exception의 의도는 가능한 개발자가 예외를 처리하도록 강제하는 것이었다.
  • Checked Exception은 메서드 시그니쳐에 선언하거나 처리(handle) 해야한다.
  • 이는 신뢰성(reliability)와 복원력(resilience)를 높이기 위한 것이었다.
  • 이는 우발적인 상황에 발생한 에러를 복구(recover) 하려는 의도가 있었다.

Failure and Exceptions - A Conversation with James Gosling

이번에는 2015년에 나온 ‘엘레강트 오브젝트’에 나온 내용입니다. 엘레강트 오브젝트는 다소 개발자들 사이에서 논란이 많은 책이기도 합니다. 하지만 저자가 제시하는 의견이 일리가 있으니 내용도 확인해볼만 합니다.

엘레강트 오브젝트

  • 언체크 예외를 사용하는 것은 실수이며 모든 예외는 체크 예외여야 한다.
  • 체크(checked) 예외를 잡기 위해 체크 예외는 항상 가시적인(visible) 이유가 있다. 만일 예외가 없다면 우리는 해롭고 안전하지 않은 메서드를 다루고 있는 것이다.
  • 대조적으로 언체크(unchecked) 예외는 무시할 수 있으며 예외를 잡지 않아도 무방하다. 언어가 예외처리를 강조하지 않는다. 체크 예외는 항상 가시적이지만 언체크 예외는 공개적이지 않다.

Checked vs. Unchecked Exceptions: The Debate Is Not Over

다음은 조슈아 블로크의 ‘이펙티브 자바’에 담긴 내용입니다. checked exception의 사용성을 인정하고 런타임 예외와의 사용법을 구분하였습니다.

이펙티브 자바

  • 복구 가능한 조건에 대해 체크된 예외를 사용하고 프로그래밍 오류에 대해서는 런타임 예외를 사용해야 한다. (아이템 70)
    • 호출하는 쪽에서 복구하리라 여겨지는 상황에서는 Checked Exception을 사용해야 한다.
    • 복구에 필요한 정보를 알려주는 메서드를 제공해야 한다.
  • 발생한 예외를 프로그래머가 처리하여 안정성을 높게끔 해준다. (아이템 71)
    • 어떤 메서드가 Checked Exception을 던질 수 있다고 선언됐다면 이를 호출하는 코드에서는 catch 블록을 두어 그 예외를 붙잡아 처리하거 더 바깥으로 던져 문제를 전파해야 한다.

Spring Framework

스프링 Transactional API는 checked exception은 에러로 잡지 않습니다.

In its default configuration, the Spring Framework’s transaction infrastructure code marks a transaction for rollback only in the case of runtime, unchecked exceptions. That is, when the thrown exception is an instance or subclass of RuntimeException. ( Error instances also, by default, result in a rollback). Checked exceptions that are thrown from a transactional method do not result in rollback in the default configuration.

  • 스프링 프레임워크의 트랜잭션 인프라 코드는 오직 런타임에서 발생하는 unchecked 예외에서만 롤백마크를 찍는다.
  • Checked Exception는 롤백을 발생시키지 않는다.

This is defined behaviour. From the docs:

Any RuntimeException triggers rollback, and any checked Exception does not.

This is common behaviour across all Spring transaction APIs. By default, if a RuntimeException is thrown from within the transactional code, the transaction will be rolled back. If a checked exception (i.e. not a RuntimeException) is thrown, then the transaction will not be rolled back.

The rationale behind this is that RuntimeException classes are generally taken by Spring to denote unrecoverable error conditions.
This behaviour can be changed from the default, if you wish to do so, but how to do this depends on how you use the Spring API, and how you set up your transaction manager.

  • 그 이유는 RuntimeException 클래스가 일반적으로 Spring에서 복구 불가능한 오류 조건을 나타내기 위해 사용하기 때문이다.
  • Checked Exception는 수동으로 설정을 해줘야지 롤백으로 만들 수 있다.
  • EJB 시절부터 있었던 관습이라고 한다.

Transaction Management - Spring Framework

Checked Exception은 필요하지 않다


코틀린에서 참조했던 2003년도의 Rod Waldhoff의 글입니다.

Java's checked exceptions were a mistake  - Rod Waldhoff

  • 자바는 checked exception에 대한 실험을 했고 실패했다. 따라서 Ruby나 C# 처럼 자바의 영향을 받았던 언어에서도 checked exception을 사용하지 않는다.
  • checked exception은 애플리케이션 아키텍처의 연결 부분에서 치명적이다.
    • 중간부분의 API는 낮은 수준에서 발생할 수 있는 특정 타입의 장애에 대해 알 필요가 없거나 알고 싶어하지 않는다.
  • jakarta api는 checked exception에 대한 예외를 허용하지 않는다.
    • 어쩔 수 없는 경우 checked exception을 조용히 무효화하거나 runtime exception으로 wrapping 한다.
  • 예외가 발생해서 이에 대한 복구를 강제한다면 의미 있는 방식으로 처리가 되는지 컴파일타임에 알아야 하는데 그렇지 않다.

Java's checked exceptions were a mistake (Rod Waldhoff)

The Trouble with Checked Exceptions (Anders Hejlsberg)

앞서 말했듯이 코틀린에서는 checked exception이 없습니다. 코틀린 공식 문서에서는 자바를 제외한 다른 언어는 checked exception이 없고 코틀린도 이를 따른다며 checked exception이 없는 이유를 서술했습니다.

Kotlin

Examination of small programs leads to the conclusion that requiring exception specifications could both enhance developer productivity and enhance code quality, but experience with large software projects suggests a different result – decreased productivity and little or no increase in code quality.
Bruce Eckel (Thinking in Java의 저자)

  • 소규모 프로그램을 검사하면 예외 사항을 요구하는 것이 개발자의 생산성과 코드 품질을 향상시킬 수 있다는 결론으로 이어진다.
  • 그러나 대규모 소프트웨어 프로젝트에서는 생산성이 저하되고 코드 품질이 전혀 향상되지 않았다는 결과가 나타났다.
try {
    log.append(message)
} catch (IOException e) {
    // Must be safe
}
  • 애플리케이션 개발자는 checked exception 을 복구할 때 대부분 catch 블럭을 비어놓는다.

Java's checked exceptions were a mistake (and here's what I would like to do about it)

artima - The Trouble with Checked Exceptions

다음은 “checked exception은 실수다”라며 다소 자극적인 제목의 블로그 글의 내용입니다.

Checked exceptions: Java’s biggest mistake

  • Checked exception의 의도는 플래그를 지정하고 개발자가 가능한 예외를 처리하도록 강제하는 것이다.
    • Checked exception은 메서드 시그니처에서 선언하거나 직접 처리해야한다.
  • 소프트웨어 안정성 및 복원력을 장려하기 위한 것이다.
    • 예측 가능한 결과인 우발적인 상황에서 ‘복구’ 하려는 의도가 있다.
    • 하지만 ‘복구’가 실제를 무엇을 수반하는지에 대한 명확성이 부족했다.

Checked Exception의 단점

  • Runtime Exception랑 Checked Exception는 기능적으로 동일하다.
  • Checked exception을 반대하는 의견의 큰 주장은 대부분의 예외는 복원할 수 없다는 점이다.
    • 우리는 에러가 발생한 코드나 서브 시스템을 소유하지 않고 구현을 볼수도 알 책임도 없다는 것 이다.
    • 수정 가능한 비상 사태를 식별하는 것이 아니라 수정이 불가능한 시스템 신뢰성 문제를 계속해서 선언해야 하는것이다.
    • 예외를 던지는 것은 모든 하위 메서드, 호출 트리에 누적이 된다.
      • EJB 개발자는 이것을 경험했다. 선언된 예외외에 다른 예외가 있는 메서드를 호출하려면 수십 개의 메서드를 조정해야 햇다.
      • 기능하지 않는 catch-throw 블록의 엄청난 수(프로젝트 당 2000개 이상)이 필요하게 되었다.
      • 예외 삼키기 ,원인 숨기기, 이중 로깅, 초기화되지 않는 데이터 반환이 만연해졌고 잘못된 코드가 성행했다.
      • 결국 개발자들은 이러한 기능하지 않는 catch 블록에 대해 반란을 일으켰다.
  • 자바 8의 함수형 인터페이스도 checked exception을 선언하지 않는다. java의 변경된 방향성의 의도를 확인할 수 있다.

why does transaction roll back on RuntimeException but not SQLException

Checked exceptions: Java's biggest mistake

Checked Exceptions are Evil

비교적 최근에 이야기를 다룬 ‘클린코드’ 책에서는 비교적 단정적인 어조로 checked exception에 대해 이야기를 하고 있습니다.

Clean Code - 7장 오류 처리 (미확인(uncheked) 예외를 사용하라)

  • 논쟁은 끝났다. 여러 해 동안 자바 개발자들은 checked exception의 장단점을 놓고 논쟁을 벌여왔다.
  • 처음으로 자바가 공개되었을 때 checked exception을 멋진 아이디어로 생각했다.
  • 하지만 지금은 안정적인 소프트웨어를 제작하는 요소로 확인된 예외가 반드시 필요하지 않다는 사실이 분명해졌다.
    • C#은 checked exception을 지원하지 않는다.
    • C++도 checked exception을 지원하지 않는다.
    • 파이썬과 루비도 checked exception을 지원하지 않는다.
  • checked exception이 치르는 비용에 상응하는 이익을 제공하는지 철저히 따져봐야 한다.
    • OCP(Open Closed Principle)을 위반한다.
      • 메서드에서 checked exception을 던졌는데 catch 블록이 세 단계 위에 있다면 그 사이 메서드 모두가 선언부에 해당 예외를 정의해야 한다.
        • 하위 단계에서 코드를 변경하면 상위 단계 메서드 선언부를 전부 고쳐야 한다.
      • 모듈과 관련된 코드가 전혀 바뀌지 않았더라도 (선언부가 바뀌었으므로) 모듈을 다시 빌드한 다음에 배포해야 한다.
    • 대규모 시스템에서 호출이 일어나는 방식
      • 최상위 함수가 아래 함수를 호출한다. 아래 함수는 그 아래 함수를 호출한다. 단계를 내려갈수록 호출하는 함수 수는 늘어난다. 최하위 함수에서 checked exception을 추가하면 그 함수를 호출하는 함수에 모두 throws를 추가해야 한다.
      • throws 경로에 위치하는 모든 함수가 최하위 함수에서 던지는 예외를 알아야 하므로 캡슐화가 깨진다.
    • 때로는 확인된 예외도 유용하다. 하지만 일반적인 애플리케이션은 의존성이라는 비용이 이익보다 크다.

Conclusion


로버트 C. 마틴의 클린코드 내용과 같이 이 논쟁은 checked exception을 사용하지 않자는 결론으로 일단락되었습니다.

추가적으로 Josh Bloch(Java Collections 프레임워크), Rod Johnson(Spring Framework), Anders Hejlsberg(C#의 아버지), Gavin King 및 Stephen Coebourn(JodaTime)과 같은 인물은 모두 checked 예외에 반대했습니다. Spring, Hibernate 등의 자바 프레임워크/벤더는 런타임 예외만 사용합니다.

자바의 예외는 이전 언어에 비해 안정성 및 오류 처리면에서 장점이 있었습니다. checked exception은 ‘실패’가 아닌 ‘우발적인 상황’을 처리하려는 시도였습니다. 예측가능한 예외를 강조하고 개발자가 이를 처리하게 하는 것이었습니다.

하지만 광범위한 시스템과 복구 불가능한 실패를 강제로 선언하는 것에 대해서는 생각을 하지 못했습니다. 이러한 실패는 checked exception으로 선언될 수 없었습니다. 실패는 일반적인 코드에서 가능하며 EJB, Swing/AWT 컨테이너는 가장 바깥쪽에서 예외 핸들러를 두어 이를 처리했습니다. 트랜잭션을 롤백하고 오류를 반환하는 것이었습니다.

java 8 이후에서는 람다는 앞으로의 근본적인 단계입니다. 이러한 기능은 내부의 기능적 작업에서 ‘제어 흐름’을 추상화합니다. checked exception과 ‘즉시 선언 또는 처리’에 대한 요구 사항을 무용지물로 만듭니다.

지금까지 checked exception에 대한 논쟁을 알아보았습니다. 이미 지나간 논쟁이기도 해서 개인적인 의견은 최대한 배제한체 논거있는 의견을 모아서 정리해보았습니다. 다만 오역이 있을 내용이 있으니 글을 읽으신분들께서 주시는 피드백은 최대한 반영하겠습니다 :)

profile
Hello!

1개의 댓글

comment-user-thumbnail
2023년 12월 7일

좋은 글 잘보고 갑니다.

답글 달기