본문 바로가기

카테고리 없음

결과 부족이 발생할 경우 null과 failure를 사용하라

함수가 원하는 결과를 만들어 낼 수 없을 때가 많습니다. 몇 가지 예를 들면 

  • 서버로부터 데이터를 읽어 들이려고 햇는데, 인터넥 연결 문제로 읽어 들이지 못한 경우
  • 조건에 맞는 첫 번째 요소를 찾으러 했는데, 조건에 맞는 요소가 없는 경우
  • 텍스트를 파싱해서 객체를 만들려고 했는데, 텍스트 형식이 맞지 않는 경우

이러한 상황을 처리하는 매커니즘은 크게 다음과 같이 두가지 있습니다

  • null 또는 실패를 나타내는 sealed 클래스(일반적으로 failure라는 이름을 붙입니다)를 리턴한다
  • 예외를 throw 한다.

이러한 두가지는 중요한 차이점이 있습니다. 일단 예외는 정보를 전달하는 방법으로 사용해서는 안됩니다. 예외는 잘못된 특별한 상황을 나타내야 하며 처리되어야 합니다. 예외는 예외적인 상황이 발생 햇을때 나타내는게 좋습니다. 이러한 이유를 정리하면 다음과 같습니다.

  • 많은 개발자가 예외가 전파되는 과정을 제대로 추적하지 못합니다.
  • 코틀린의 모든 예외는 uncheked 예외 입니다. 따라서 예외를 처리하지 않을 수도 잇으며 이와 관련된 내용은 문서에도 재대로 들어나지 않습니다. 실제로 API를 사용할 때 예외와 관련된 사항을 단순하게 메서드 등을 사용하면서 파악하기 힘듭니다.
  • 예외는 예외적인 상황을 처리하기 위해서 만들어졌으므로 명시적인 테스트(explicit test)만큼 빠르게 동작하지 않습니다
  • try-catch 블록 내부에 코드를 배치하면, 컴파일러가 할 수 있는 최적화가 제한됩니다.

반면 첫 번째로 설명햇던 null과 Faiilure는 예상되는 오류를 표현할 대 굉장히 좋습니다. 이는 명시적이고, 효율적이며 간단한 방법으로 처리할 수 있습니다. 따라서 충분히 예측할 수 있는 범위의 오류는 null과 failure를 사용하고 예측하기 어려운 예외적인 범위의 오류는 예외를 throw해서 처리하는 것이 좋습니다. 간단한 예를 살펴 보면

 


inline fun <reified T> String.readObjectOrNull(): T?{
  // ...
  if(incorrectSign){
    return null
  }
  // ...
  return result
}

inline fun<reified T> String.readObject(): Result<T>{
  // ...
  if(incorrectSign){
    return Failure(JsonParsingException())
  }
  // ...
  return Success(result)
}

sealed class Rsult<out T>
class Success<out T>(val result:T): Result<T>()
class Failure(val throwable: Thowable): Result<Nothing>()

class JsonParsingException: Exception()

 

이렇게 표시되는 오류는다루기 쉬우며 놓치기 어렵습니다. null을 처리해야 한다면. 사용자는 안전 호출(safe call)또는 Elvis 연산자 같은 다양한 널 안전성(null-safety) 기능을 활용합니다. 


val age = userText.readObjectOrNull<Person>()?.age ?: -1

 

Result와 같은 공용체(union type)를 리턴하기로 했다면, when 표현식을 사용해서 이를 처리할 수 있습니다.

val age = userText.readObjectOrNull<Person>()?.age ?: -1
val age = when(person) {
  is Success -> person.age
  is Failure -> -1
}

 

이러한 오류 처리방식은 try-catch 블록보다 효율적이며, 사용하기 쉽고 더 명확합니다. 예외는 놓칠 수도 있으며, 전체 애블리케이션을 중지시킬 수도 있습니다. null값과 sealed result 클래스는 명시적으로 처리해야 하며 애플리케이션의 흐름을 중지하지도 않습니다. 

null 값과 sealed result 클래스의 차이점이 궁금할 수 있는데 추가적인 정보를 전달해야 한다면 sealed result를 사용하고, 그렇지 않으면 null을 사용하는 것이 일반적입니다. Failure는 처리할 때 필요한 정보를 가질 수 있습니다

일반적으로 두 가지 형태의 함수를 사용합니다. 하나는 예상할 수 있을 때, 다른 하나는 예상할 수 없을 때 사용합니다. List는 두가지를 모두 갖고 있으므로 이를 기반으로 살펴봅시다

  • get: 특정 위치에 있는 요고를 추출할 때 사용합니다. 만약 요소가 해당 위치에 없다면 IndexOutOfBoundsException을 발생시킵니다.
  • getOrNull: out of range 오류가 발생할 수 있는 경우에 사용하며 발생한 경우에는 null을 return 합니다.

이외에도 일부 상황에 유용한 getOrDefault와 같은 다른 선택지도 있습니다. 하지만 일반적으로 getOtnull 또는 Elvis 연간자(?:)를 사용하는 것이 쉽습니다.

개발자는 항상 자신이 요소를 안전하게 추출할 거라 생각합니다. 따라서 nullable을 리턴하면 안 됩니다. 개발자에게 null이 발생할 수 있다는 경고를 주려면, getOrNull 등을 사용해서 무엇이 리턴되는지 예측할 수 있게 하는 것이 좋습니다.

 

728x90