Uchun
Uchun Android developer

runCatching을 이용한 kotlin에서 exception처리 방법.


runCatching을 이용한 kotlin에서 exception처리 방법.

kotlin 1.3부터는 exception이 발생할 수 있는 상황을 처리하기 위해 runCatching이라는 inline function을 제공하고 있습니다.

설명에 앞서 아래와 같이 랜덤하게 과일 이름을 출력하는 함수가 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Throws(Exception::class)
fun getRandomFruit(): String {
    val fruitName = listOf(
        "Avocado", "Blueberries", null,
        "Clementine", "Durian", "Guava"
    ).shuffled().first()

    return when (fruitName) {
        "Guava" -> throw IllegalStateException("Out of stock")
        null, "" -> throw NullPointerException()
        else -> fruitName
    }
}

여기서 구아바는 재고가 없어서 exception을 발생하게 했고.
null이거나 empty여도 exception을 발생하게 했습니다.

getRandomFruit에서 발생하는 exception을 처리하기 위해서 아래와 같이 try-catch를 사용하여 처리할 수 있습니다.

1
2
3
4
5
val fruitName = try {
    getRandomFruit()
} catch (throwable: Throwable) {
    ""
}

kotlin 에서 try-catch는 expression이어서 java보다 편해졌습니다만
try-catch만으로는 실행되는 영역과 결괏값을 변화시키는 영역, exception이 처리되는 영역, 최종적으로 값이 사용되는 영역의 구분이 번거롭습니다.

runCatching 이용해보기

kotlin 에서 제공하는 runCatching은 아래와 같습니다. 공식 문서

1
2
3
4
5
6
7
public inline fun <R> runCatching(block: () -> R): Result<R> {
    return try {
        Result.success(block())
    } catch (e: Throwable) {
        Result.failure(e)
    }
}

그리고 앞에서 사용한 try-catch를 runCatching으로 바꾸어 보면 아래와 같이 표현할 수 있습니다.

1
2
3
4
val fruitResult = runCatching {
    getRandomString()
}
val fruitName = fruitResult.getOrNull()

runCatching의 return 값으로 전달받은 Result는 아래의 method와 properties를 제공합니다.

1
2
3
4
if (fruitResult.isSuccess) { }
if (fruitResult.isFailure) { }
val fruitName = fruitResult.getOrNull()
val throwable = fruitResult.exceptionOrNull()

isSuccess, isFailure 는 특별한 설명이 필요 없다고 생각하고,
getOrNull으로 exception이 발생하지 않는 경우 value를, 그 외는 null을 받을 수 있고,
exceptionOrNull은 그 반대입니다.

map과 recover를 이용하여 값 변환하기

여기까지만 보면 조금 시시할 수 있습니다.
하지만 Result에 대해 아래와 같은 extension 을 제공하고 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Result<T>.getOrThrow(): T
Result<T>.getOrElse(onFailure: (exception: Throwable) -> R): R
Result<T>.getOrDefault(defaultValue: R): R
Result<T>.onSuccess(action: (value: T) -> Unit): Result<T>
Result<T>.onFailure(action: (exception: Throwable) -> Unit): Result<T>
Result<T>.fold(
    onSuccess: (value: T) -> R,
    onFailure: (exception: Throwable) -> R
): R

Result<T>.map(transform: (value: T) -> R): Result<R>
Result<T>.mapCatching(transform: (value: T) -> R): Result<R>
Result<T>.recover(transform: (exception: Throwable) -> R): Result<R>
Result<T>.recoverCatching(transform: (exception: Throwable) -> R): Result<R>

onSuccess, onFailure, fold로 성공과 실패(exception이 발생한 경우)를 따로 처리가 가능합니다.
또는 getOrXXX로 exception이 발생하지 않고 잘 수행된 경우 원하는 결괏값을, 아닌 경우는 XXX 에 해당되는 동작을 하게 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
fruitResult.onSuccess {
    // 성공 시 받은 결괏값에 대한 처리
}.onFailure {
    // 실패 시 발생한 throwable을 처리
}

fruitResult.fold({
    // 성공 시 받은 결괏값에 대한 처리
}, {
    // 실패 시 발생한 throwable을 처리  
})

var fruitName: String? = null
// 실패 시 default 값을 반환
fruitName = fruitResult.getOrDefault("")
// 실패 시 else block의 결괏값을 반환
fruitName = fruitResult.getOrElse {
    when(it) {
        is IllegalStateException -> "Sold out"
        is NullPointerException -> "null"
        else -> throw it
    }
}
// 실패시 throwable이 다시 throw 됩니다.
fruitName = fruitResult.getOrThrow()

또한 map과 recover를 이용해 성공과 실패 시 원하는 값으로 바꿀 수 있습니다.
둘 다 xxxCatching 을 제공하는데 map과 recover시 발생할 수 있는 exception 으로부터 안전하기 위해 runCatching으로 감싸저 있습니다.
Result로 반환하므로 chaining 하여 이용 가능합니다.
또한 getOrElse 대신 아래와 같이 recover를 이용해 볼 수도 있습니다.

1
2
3
4
5
6
7
8
9
10
11
fruitResult.map {
    it.toUpperCase()
}

fruitResult.recover {
    when(it) {
        is IllegalStateException -> "Sold out"
        is NullPointerException -> "null"
        else -> throw it
    }
}

위에 얘기한 내용을 엮어보면 아래와 같은 방법으로 써볼 수 있습니다.
(map 경우는 큰 의미는 없지만 예제목적으로 mapCatching 을 사용하였습니다.)

1
2
3
4
5
6
7
8
9
10
11
val fruitName = runCatching {
    getRandomFruit()
}.mapCatching {
    it.toUpperCase()
}.recoverCatching {
    when(it) {
        is IllegalStateException -> "Sold out"
        is NullPointerException -> "null"
        else -> throw it
    }
}.getOrDefault("")

이 경우는 성공한 경우만 uppercase가 동작합니다 recover를 map보다 먼저 해주면 recover된 값 또한 map에서 변환될 수 있으니 작성 시 의도에 맞게 순서를 잘 정할 필요가 있습니다.

그 외 기타 사항

Result는 return 하려고 하면 ‘kotlin.Result’ cannot be used as a return type 이라는 에러가 발생합니다.
사용하고 싶은경우 다음의 stackoverflow 글에서 gradle 설정법에 대하여 설명 되어 있습니다.
다만 위의 stackoverflow 답변에 있는 내용 대로 왜 막아두었는지 여기여기에 잘 설명되어 있습니다.

stackoverflow 및 관련 자료 링크

글 작성시 참고 한 글

comments powered by Disqus