본문 바로가기
공부/이펙티브코틀린

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

by 띵커베르 2024. 3. 31.
728x90
  • 함수가 원하는 결과를 만들어 낼 수 없을 때가 있는데 이러한 상황을 처리하는 매커니즘은 크게 다음과 같이 두 가지가 있다.
  • null 또는 '실패를 나타내는 sealed 클래스(일반적으로는 Failure 라는 이름을 붙입니다)'를 리턴한다
  • 예외를 throw 한다.

 

  • 일단 예외는 정보를 전달하는 방법으로 사용해서는 안된다
  • 예외는 잘못된 특별한 상황을 나타내야 하며, 처리되어야 한다
  • 예외는 예외적인 상황이 발생했을 때 사용하는 것이 좋다.
    • 많은 개발자가 예외가 전파되는 과정을 제대로 추적하지 못한다.
    • 코틀린의 모든 예외는 unchecked 예외 입니다.따라서 사용자가 예외를 처리하지 않을 수도 있다.
    • 예외는 예외적인 상황을 처리하기 위해서 만들어졌으므로 명시적인 테스트 만큼 빠르게 동작하지 않습니다.
      • 일반적인 코드흐름에 비해 느리다 라는것을 의미하는듯 (if-else 같은..코드에 비해)
      • 예외 생성 비용 - 예외 객체를 생성할 때, 스택 트레이스와 관련 정보를 캡처하는 과정이 필요하며, 이는 리소스 소모를 뜻한다
    • try-catch 블록 내부에 코드를 배치하면, 컴파일러가 할 수 있는 최적화가 제한됩니다.
      • try-catch 블록은 일반적인 실행 경로와 다른 제어 흐름을 가지고 있다
      • 예외가 발생할 경우에만 catch 블록이 실행된다 이러한 동적인 실행 경로 때문에 컴파일러가 최적화 하기 어렵다
      • 예외처리 코드는 일반적으로 복잡한 로직을 포함하기때문에 그만큼 복잡해 진다
    • 아래는 Reulst 와같은 공용체(union type) 리턴방식 코틀린 코드
      • 공용체 타입: 여러 다른 타입 중 하나를 가질 수 있는 타입을 의미
      • Result -> 성공 또는 실패의 결과를 나타내는 두가지 가능한 값을 포함
fun calculateSum(a: Int, b: Int): Result<Int> = runCatching {
    if (a + b > 100) throw Exception("Sum is greater than 100")
    else a + b
}

또는

fun calculateSum(a: Int, b: Int): Result<Int> {
    return if (a + b > 100) {
        Result.failure(Exception("Sum is greater than 100"))
    } else {
        Result.success(a + b)
    }
}

fun mainResult() {
    val result = calculateSum(50, 51)
    when {
        result.isSuccess -> println("Sum is ${result.getOrNull()}")
        result.isFailure -> println("Error: ${result.exceptionOrNull()?.message}")
    }
}
  • try-catch 와 runCatching 비교 코드
try {
    val result = riskyOperation()
    println("Operation succeeded: $result")
} catch (e: Exception) {
    println("Operation failed: ${e.message}")
}


riskyOperation().runCatching {
    println("Operation succeeded: $this")
}.onSuccess {
    // onSuccess 블록은 runCatching 블록의 결과가 성공적일 때 실행됩니다.
    // 'it'은 runCatching 블록에서 반환된 결과값입니다.
    println("The result was successful: $it")
}.onFailure {
    // 'it'은 발생한 예외 객체입니다.
    println("Operation failed: ${it.message}")
}

 

 

  • 추가적인 정보를 전달해야 한다면 sealed result 를 사용하고, 그렇지 않으면 null 을 사용하는 것이 일반적이다.
sealed class Result<out T> {
    data class Success<out T>(val data: T) : Result<T>()
    data class Failure(val exception: Exception) : Result<Nothing>()
}

fun calculateSum(a: Int, b: Int): Result<Int> {
    return try {
        Result.Success(a + b)
    } catch (e: Exception) {
        Result.Failure(e)
    }
}

@Test
fun mainResult() {
    val result = calculateSum(50, 51)
    when (result) {
        is Result.Success -> println("Sum is ${result.data}")
        is Result.Failure -> println("Error: ${result.exception.message}")
    }
}
  • runCatching 과 같이 코틀린 표준 라이브러리를 써도되고, 복잡한 결과를 좀 더 세밀하게 처리하고 싶다면 sealed Result 를 만들어서 처리하는것도 괜찮다. 아래처럼 파일열기, 읽기, 변환, 저장 등의 복잡한 상황에서의 핸들링을 할 수 있는 코드를 적어봤다
sealed class ProcessResult<out T> {
    data class Success<out T>(val data: T) : ProcessResult<T>()
    sealed class Failure : ProcessResult<Nothing>() {
        object FileNotFound : Failure()
        object PermissionDenied : Failure()
        data class ParsingError(val error: String) : Failure()
        data class ApiError(val code: Int, val message: String) : Failure()
    }
}

fun processFile(fileName: String): ProcessResult<String> {
    // 파일 처리 로직 구현
    return ProcessResult.Failure.FileNotFound // 예시
}

@Test
fun sealedResultTest() {
    val result = processFile("example.txt")

    when (result) {
        is ProcessResult.Success -> println("File processed: ${result.data}")
        is ProcessResult.Failure.FileNotFound -> println("File not found")
        is ProcessResult.Failure.PermissionDenied -> println("Permission denied")
        is ProcessResult.Failure.ParsingError -> println("Parsing error: ${result.error}")
        is ProcessResult.Failure.ApiError -> println("API error ${result.code}: ${result.message}")
    }    
}

 

  • 예상할 수 있는 상황과 예상할 수 없는 상황
    • 특정 작업을 수행할때 예상할 수 있는 상황과 예상할 수 없는 상황이 있을 수 있다, list 컬렉션으로 예를 들어보자
    • 아래의 코드와 같이 
@Test
fun listTest() {
    val list = listOf(1, 2, 3, 4, 5)

    val get = list[0] // list.get(0)
    println("get: $get")

    // 리스트에서 특정 위치의 요소를 반환하지만, 요청한 인덱스가 범위를 벗어날 경우 null을 반환합니다.
    val orNull = list.getOrNull(10) // list.get(10) or null
    println("orNull: $orNull")
    list.getOrElse(0) { 0 } // list.get(0) or 0

    // Elvis 연산자 사용
    val first = list.firstOrNull() ?: 0
    println("first: $first")

    val last = list.lastOrNull() ?: 0
    println("last: $last")

    // get: 1
    // orNull: null
    // first: 1
    // last: 5
}
  • nullable 을 리턴하면 안되며, null 이 발생할 수 있다는 경고를 주려면, getOrNull 등을 사용해서 무엇이 리턴되는지 예측할 수 있게 하는 것이 좋다

 


좋은 내용인거 같고, try-catch 말고 요즘 runCatching 을 사용하니 좀 더 나은거 같기도 하고, sealed 클래스를 Result 와 섞을 생각을 못해봤는데, 나중에 디테일한 failure 핸들링할때 사용해 볼 예정이다. 실패에 따른 메세지를 보낸다거나, 로그를 쌓는다거나 등등

728x90

댓글