아이템 7 - 결과 부족이 발생할 경우 null과 Failure를 사용하라

ds_chanin

·

2022. 3. 2. 00:21


  • 다른 시스템에서 데이터를 인터넷 연결등의 이유로 받아오지 못하는 경우
  • 조건에 맞는 요소를 찾지 못하는 경우
  • 파싱을 시도했으나 형식이 맞지않아 실패하는 경우

위와 같이 함수가 원하는 결과를 만들어 내지 못할 때 예외를 정보를 전달하는 방법으로 사용해서는 안된다. 다음과 같은 방식을 고려하자.

  • 충분히 예측할 수 있는 범위의 오류
    • null, 실패를 나타내는 sealed 클래스를 반환한다.
      • null을 반환하는 경우: 추가적인 정보를 전달하지 않아도 되는 경우
      • sealed 클래스를 반환하는 경우: 추가적인 정보를 전달해야 하는 경우
  • 예측하기 어려운 경우
    • 예외를 throw 한다.

예외를 정보를 전달하는 방법으로 사용하면 다음과 같은 문제가 발생하기 때문이다.

  • 개발자가 exception이 전파되는 과정을 제대로 추적하지 못한다.
  • 코틀린의 모든 exception은 uncheck exception라서 클라이언트가 처리하지 않을 수 있고, 문서에도 제대로 드러나지 않아 파악하기 힘들다.
  • try-catch 블럭에 코드를 작성하면 컴파일러의 최적화가 제한된다.
    • try-catch 가 비용이 드는 이유: 컴파일 타임에 코드를 정렬하고 최적화해야 하는데, 이는 exception이 어느 시점에서 발생하더라도 일련의 순서대로 코드가 실행된 것처럼 보여져야하는 작업이다. 그리고 try-catch 블럭이 있을때 이러한 작업은 비용이 많이 들 수 있고 최적화가 되지 않는 경우가 있다고 한다.

가장많은 점수를 받은 답변을 보니 오류가 발생할 때 해당 오류를 catch 하는 try블럭을 탐색하는데 비용이 많이 든다고 한다. 즉, try-catch 블럭이 있으면 excepcatch 블럭이 존재하든

예제 코드

AS-IS

fun chargeWithTryCatch() {
    try {
        chargeThrowException()
        //충전 성공을 기록한다.
    } catch (exception: IllegalStateException) {
        //충전 실패를 기록한다.
    }
}

fun chargeThrowException() {
    throw IllegalStateException("충전실패")
}

TO-BE

fun chargeWithOutTryCatch() {
    val result = chargeReturnThrow()

    when (result) {
        is ChargeResult.Failure -> {
            //충전 실패를 기록한다.
        }
        is ChargeResult.Success -> {
            //충전 성공을 기록한다.
        }
    }
}

fun chargeReturnThrow(): ChargeResult {
    return ChargeResult.Failure(IllegalStateException("충전실패"))
}

sealed class ChargeResult {
    class Success() : ChargeResult()
    class Failure(val throwable: Throwable) : ChargeResult()
}

엘비스(Elvis) 연산자를 사용하기

개발자는 항상 Collection에서 요소를 안전하게 추출할 것이라 예상한다. 따라서 nullable한 요소가 Collection에서 나오면 안된다. 이 경우 null이 발생할 수 있다는 경고를 주려면 getOrNull을 사용하거나 ?: 를 사용하여 명시적으로 리턴되는 값을 알려주도록 하자.