프로그래밍/Java

Java/Kotlin에서의 예외처리에 관하여

Lou Park 2024. 1. 28. 10:52

Java Exception Hierarchy

출처: https://thistechnologylife.com/how-to-effectively-handle-exceptions

 

Throwable은 모든 Exception 객체들의 부모 클래스다.
크게는 Error와 Exception으로 구분할 수 있고, Exception은 또 다시 Checked Exception과 Unchecked(Runtime) Exception으로 구분 된다.

 

Error

Error는 회복불가능한(non-recoverable) 오류다. 런타임에 Error가 발생하면 핸들링할 수 없다. 대표적으로는 메모리 부족(Out of Memory), 네트워크 포화(Network Saturation), 하드웨어 결함 등이 있다.

  • Error를 인스턴스화 하거나 상속하거나, 직접 예외를 던지거나, 처리를 하려해서는 안된다.
  • 위로 전파하는 것이 가장 좋다.

Exception - Checked Exceptions

프로그램에서 처리해야하는 예외들이 Checked Exception에 속한다. 대표적으로는 IOException, SQLException, TimeoutException 등이 있다.

  • Checked Exception을 던지면 같은 메서드에서 처리하거나, 호출스택을 따라가면서 예외를 처리하는 방법을 취해야한다.

Exception - Runtime (Unchecked) Exceptions

런타임 중에 발생하는, 컴파일러가 체크하지 못한 예외들은 Unchecked Exception이다. 프로그래밍 실수로 인해 발생한다. 대표적으로는 NullPointerException, ArithmeticException 등이 있다.

  • Unchecked Exception은 메소드 시그니처에 명시할 필요가 없다.

 

예외처리의 Best Practice

예외처리시 좋은 습관들을 이 포스트에 있는 것을 가져왔다.

 

가능한 구체적인 예외를 사용하기

다른 개발자는 내 코드가 어떻게 짜여져있는지 제대로 알지 못한다. 그래서 가능한 구체적인 예외를 던짐으로서 충분한 정보를 주는 것이 좋다. 파일을 찾지못한 오류를 예로 든다면 IOException 보다는 FileNotFoundException이 더 구체적인 힌트를 줄 수 있을 것이다.

fun avoidThis() {
    throw IOException()
}

fun preferThis() {
    throw FileNotFoundException()
}

나아가서 예외가 발생하는 상황과 그 종류에대한 주석을 적어주는 것이 좋다.

/**
    @throws FileNotFoundException ~할때 발생
**/
@Throws(FileNotFoundException::class)
fun preferThis() {
    throw FileNotFoundException()
}

 

Throwable을 Catch하지 않기

모든 예외와 에러는 Throwable의 서브 클래스다. Throwable을 Catch할 경우 Application 레벨에서 처리하면 안되는 Error까지 핸들링 해버린다.

fun neverCatchThrowable() {
    try {
        ...
    } catch (t: Throwable) {
        ...
    }
}

 

예외를 간과하지 않기

종종 특정 Exception이 절대 발생하지 않을 거라고 아무것도 처리하지 않는 경우가 있다. 하지만 런타임에 어떤 일이 발생할지 모르기 때문에 이러한 예외가 발생했을때 어떤 액션을 취해야하는지 최소한 로깅정도는 해두는게 좋다.

fun neverIgnoreExceptions() {
    try {
    } catch (e: FileNotFoundException) {
        // this will never happen
    }
}
fun atLeastLogException() {
    try {
    } catch (e: FileNotFoundException) {
        log.error("This should not be happend ${e}")
    }
}

 

예외를 소비하지 않고 다른 예외로 감싸기

특정 상황에서는 표준 예외를 Catch하고 애플리케이션에서 사용하는 커스텀 예외로 래핑하는 것이 더 정보를 제공할 수 있다. 하지만 이때 원래 예외는 cause으로서 유지되어 Stack Trace와 원래 메세지를 보존할 수 있도록 해야한다.

fun preferThis() {
    try {
        ...
    } catch (e: ArithmeticException) {
        throw CustomException("An error occurred during example function", e)
    }
}

fun avoidThis() {
    try {
        ...
    } catch (e: ArithmeticException) {
        throw Exception("An error occurred during example function")
    }
}

 

예외를 GoTo처럼 사용하지 않기

예외를 던지고 잡는 것은 비용이 많이드는 작업이다. 그래서 "예외적인" 상황에만 사용하는 것이 좋다. 어떤 작업을 하다가 제어흐름을 바꾸어서 작업 Catch 문에서 마저 작업하기 위해 예외를 던지는 것을 피해야한다.

fun doSomething() {
    try {
        throw MyException()
    } catch (e: MyException) {
        // still try to do something
    }
}

 

fillInStackTrace 활용하기

예외 생성 비용은 비싸다. 생성에 소요되는 시간은 Stack Depth에 따라 80~4000 nano second까지도 차이가 난다. 비즈니스 로직의 제어를 위해 만든 Custom Exception의 경우에 StackTrace가 크게 중요하지 않다. 이 경우 Throwable.fillInStackTrace 메소드를 오버라이드하여 아무런 trace도 가지지 않도록 만들면 생성비용을 아낄 수 있다.

override fun fillInStackTrace(): Throwable {  
    return this  
}

 

Exception 캐싱하기

앞서 언급했던 fillinStackTrace를 활용한 Exception의 경우 상수로 선언해두고 던지면 매번 생성하는 것보다 효율적이다.

object CustomExceptions {
    const val InvalidNickname = CustomException("Invalid Nickname")
}

throw CustomExceptions.InvalidNickname

 

출처
https://thistechnologylife.com/how-to-effectively-handle-exceptions
https://meetup.nhncloud.com/posts/47