Kotlin

Kotlin Result : 결과와 예외 처리하기

정자이노 2023. 7. 9. 18:38

Kotlin 표준 라이브러리에서는 Result 클래스를 제공한다. Result는 실행한 동작의 결과를 Result로 감싸서 처리를 다른 클래스에 위임하는 것이 목적인 클래스이다. Result와 Result의 확장함수를 통해 유연하게 성공 및 실패를 처리할 수 있다. 작업을 수행하기 위한 방법으로는 대표적인 try-catch도 있고, Kotlin 1.3v에서 추가된 Result를 반환하는 inline 함수, runCatching도 있다. 이번 포스팅에서는 Result, 여러가지 Result 함수를 소개한다.

 

사전 지식 : runCatching

runCatching은 코틀린 1.3v에서 도입된 캡슐화 블록이다. runCatching 블록 안에서 성공/실패 여부가 캡슐화된 Result<T> 형태로 반환한다. try-catch로직과 유사하며, Result로 감싸서 반환하는 것이 특징이다.

 

Result 소개 : "Hello, Result!"

Result는 실행의 결과를 캡슐화한 클래스로, 성공시에는 성공한 결과로 캡슐화를, 실패시에는 Throwable한 예외와 함께 실패를 캡슐화하는  discriminated union(구별되는 조합)이다.

Result runCatching 활용하면, 실행한 결과를 캡슐화 하여 다른 클래스에 결과 처리를 위임할 있다.

 

Result 프로퍼티

Result에는 두 가지 Properties, isSuccess 와 isFailure 가 있다.

public val isSuccess: Boolean get() = value !is Failure

public val isFailure: Boolean get() = value is Failure

만약 Result가 failure로 캡슐화 되어 있다면, isFailure = true / isSuccess = false가 된다.

반대로 suceess로 캡슐화 되어 있다면, isSuccess = true / isFailure = false가 된다. 

 

Result 멤버 함수 

Result는 총 세 가지 멤버 함수를 가지고 있다. 

    public inline fun getOrNull(): T? =
        when {
            isFailure -> null
            else -> value as T
        }

    public fun exceptionOrNull(): Throwable? =
        when (value) {
            is Failure -> value.exception
            else -> null
        }

    public override fun toString(): String =
        when (value) {
            is Failure -> value.toString() // "Failure($exception)"
            else -> "Success($value)"
        }

해당 코드를 확인하면,

  • getOrNull은 성공은 일반적인 경우와 동일하고, 실패  null 반환한다.
  • exceptionOrNull은 실패는 일반적인 경우와 동일하고, 성공시 null을 반환한다
  • toString은 성공 시 "Success(value.toString())"을 반환하고, 실패 시 "Failure(exception.toString())"을 반환한다.예제를 통해 동작을 확인할 수 있다.

아래의 예시를 통해 동작을 확인할 수 있다. 

fun main() {
    runCatching{
        "jainoException".toInt()
    }.getOrNull() ?: println("get null if it is failure")
    
    val resultExample = runCatching{
        "Hello Result"
    } // > get null if it is failure
    
    resultExample.exceptionOrNull() ?: println("get null if it is success") // > get null if it is success
    
    println(resultExample.toString()) // > Success(Hello Result)
}

 

Result 확장 함수 

Result의 확장함수는 다양한 기능을 제공하는데, 필자가 임의로 나눈 타입에 따라서 소개하겠다.

 

1. Result 실패시 사용할 수 있는 확장함수.

fun <T> Result<T>.getOrThrow(): T

fun <R, T : R> Result<T>.getOrDefault(defaultValue: R): R

inline fun <R, T : R> Result<T>.getOrElse(onFailure: (exception: Throwable) -> R): R

inline fun <R, T: R> Result<T>.recover(transform: (exception: Throwable) -> R): Result<R>

inline fun <R, T: R> Result<T>.recoverCatching(transform: (exception: Throwable) -> R): Result<R>

inline fun <T> Result<T>.onFailure(action: (exception: Throwable) -> Unit): Result<T>
확장함수는 아래와 같은 기능을 한다. 
  • getOrThrow : 실패 시, 발생한 예외를 밖으로 버린다. 
  • getOrDefault : 실패 시, 지정한 Default 파라미터를 반환한다. 
  • getOrElse : 실패 시, 지정한 Else를 반환한다. Exception을 반환받고, 예외 처리를 할 수 있다.
    • getOrDefault와 같은 작업을 수행하며, getOrElse는 Exception에 따라 유연하게 처리할 수 있다는 장점이 있다.
  • recover : 실패 시, 예외를 반환 받고, 예외와 관련된 처리를 할 수 있다. 반환 시에 캡슐화 된 타입과 같아야 하며, 반환된 값은 성공으로 간주된다.
  • recoverCatching : recover과 동작은 차이가 없다. 그러나 변환 시에 에러가 발생한 경우, recover는 실패를 외부로 throw하고, recoverCatching은 실패로 처리한다. 
  • onFailure : 실패 시에만, 호출되는 함수로 예외를 받아, 예외 처리를 진행할 수 있다. 

예제를 통해 동작을 확인할 수 있다. 

fun main() {
    val resultExample = runCatching{
        "Hello Result".toInt()
    }
    
    // getThrow() 실패 시 throw 
    try {
        resultExample.getOrThrow()
    } catch (e: Exception) {
        println("0") // > 0
    }
    
    // getOrDefault() 실패 시 지정한 default값 반환
    resultExample.getOrDefault(1).let { println(it) } // > 1
    
    // getOrElse() 실패 시 지정한 else 반환
    resultExample.getOrElse { println(2) } // > 2
}
class JainoException : Exception()

fun throwJainoException(): String = throw JainoException()

fun main() {
   // 일반적인 recover, recoverCatching
   runCatching { throwJainoException() }
            .recoverCatching { "Success Jaino" } 
            .onSuccess { println(it) } // > "Success Jaino"
            
    // map : 변환과정 중 실패 시 외부로 throw      
    try {
        runCatching { throwJainoException() }
                .recover { throwJainoException() } // 변환시 에러 발생 
                .onSuccess { println(it) } // 호출되지 않음.
                .onFailure { println(it) } // 호출되지 않음.
    } catch (e: JainoException) {
        println(e) // > JainoException
    }

    // mapCatching : 변환과정 중 실패 시 내부의 onFailure로 처리
    runCatching { throwJainoException() }
            .recoverCatching { throwJainoException() } // 변환시 에러 발생
            .onFailure { println(it) } // > JainoException
}

 

2. Result 성공시 사용할 수 있는 확장 함수 

inline fun <R, T> Result<T>.map(transform: (value: T) -> R): Result<R>

inline fun <R, T> Result<T>.mapCatching(transform: (value: T) -> R): Result<R>

inline fun <T> Result<T>.onSuccess(action: (value: T) -> Unit): Result<T>
  • map : 성공 시 성공한 값을 반환하며, 해당 값에 대해서 추가적인 변환을 수행하는 함수이다. 
  • mapCatching : map과 동일한 작업을 수행하며, 만약 변환 시에 에러가 발생한 경우, map은 외부로 throw하고, recoverCaching은 실패로 처리하고, onFailure로 받을 수 있다. 
  • onSuccess : 성공시에만 호출되는 함수이며, 성공한 값을 반환 받는다.

예제를 통해 확인할 수 있다.

class JainoException : Exception()

fun throwJainoException(): String = throw JainoException()

fun main() {
    // 일반적인 mapCatching 성공 시 변환
    runCatching{ "Success Jaino" }
    	    .mapCatching{ "Hello Jaino" }
            .onSuccess{ println(it) } // > Hello Jaino
    
    // map : 변환과정 중 실패 시 외부로 throw
    try {
        runCatching { "Success Jaino" }
                .map { throwJainoException() }
                .onSuccess { println(it) } // 호출되지 않음.
                .onFailure { println(it) } // 호출되지 않음.
    } catch (e: JainoException) {
        println(e) // > JainoException
    }
    
    // mapCatching : 변환과정 중 실패 시 onFailure로 처리 
    runCatching { "Success Jaino" }
            .mapCatching { throwJainoException() }
            .onFailure { println(it) } // > JainoException
}

 

3. 성공과 실패시 사용할 수 있는 확장 함수

inline fun <R, T> Result<T>.fold(onSuccess: (value: T) -> R, onFailure: (exception: Throwable) -> R): R
  • fold : 작업이 성공했을 경우에는 인수 onSuccess로 정의한 동작을 수행하고, 실패했을 경우에는 인수 onFailure에 정의한 동작을 실행한다.

예제를 통해 동작을 확인할 수 있다.

class JainoException : Exception()

fun throwJainoException(): String = throw JainoException()

fun main() {    
    // fold 성공과 실패 모두 정의
    runCatching { throwJainoException() }.fold(
        onSuccess = { println("Success Jaino") },
        onFailure = {  println("Failure Jaino") } // > Failure Jaino
    )
    
    runCatching { "Hello Jaino" }.fold(
        onSuccess = { println("Success Jaino") },
        onFailure = {  println("Failure Jaino") } // > Success Jaino
    )
}

 

마지막으로

Result와 같은 결과 및 예외를 처리하는 클래스는 실제로 사용자가 직접 구성하고 구현할 수 있다. 하지만 추가적인 작업에 대한 함수를 구현하는 것은 큰 리소스를 필요로 한다. Result의 경우 다양한 상황에서 필요로하는 작업(에러 핸들링 위임, 이벤트 흐름 처리)을 미리 정의된 함수로 간편하게 처리할 수 있다. 구글에서도 코루틴을 처리위한 방법으로 Result를 추천하고 있고 기회가 된다면 실제 프로젝트에 도입하는 것을 추천한다. 

 

참고 문헌

https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/

 

Result - Kotlin Programming Language

 

kotlinlang.org

https://dev-repository.tistory.com/105

 

Kotlin Result 에러 핸들링

Result와 runCatching은 오류를 처리할 수 있는 방법 중에 하나다. Result는 동작이 성공하든 실패하든 동작의 결과를 캡슐화해서 나중에 처리될 수 있도록 하는 것이 목적이다. 이 Result와 함께 사용할

dev-repository.tistory.com

https://medium.com/harrythegreat/kotlin-runcatching%EA%B3%BC-result-%ED%83%80%EC%9E%85-ab261f47efa8

 

[Kotlin] runCatching과 Result 타입

runCatching은 코틀린 1.3버전부터 도입된 Result 캡슐화 함수입니다. runCatching 블록 안에서 성공/실패 여부 캡슐화된 Result 형태로 리턴합니다. 스위프트의 Result, 자바스크립트의 Promise와 유사하며…

medium.com

https://rannte.tistory.com/entry/kotlinruncatching

 

[번역][Kotlin] Try-catch를 쓰기 힘들다면 runCatching을 써보자! - Qiita

ㅅException이 Throw여부에 따라 각각 어떤 코드를 실행하고 싶을때가 있죠. try-catch(-finally)로 구현하기 힘들때가 있습니다. try-catch(-finally)는 Exception이 Throw되었을때의 처리(또는 throw여부에 관

rannte.tistory.com