코틀린에서는 자바의 try catch
기능도 제공하지만, runCatching
을 이용하면 다른 방법으로 에러를 핸들링할 수 있다.
만약 특정 에러 코드가 발생하면 예외를 발생시키지 않고 null을 반환하고, 그 외 모든 에러 상황에서는 예외를 발생시켜야 된다고 해보자.
try catch 를 사용했을 때
try {
loginApiClient.login(request)
} catch (e: LoginException) {
if (e.errorCode == "INVALID_PASSWORD") {
return null
} else {
throw e
}
}
위 코드를 runCatching
을 사용하면 아래와 같이 표현할 수 있다.
runCatching 을 사용했을 때
return runCatching {
loginApiClient.login(request)
}.onFailure { e ->
if (e.errorCode != "INVALID_PASSWORD") throw e
}.getOrNull()
onFailure 함수와 getOrNull 함수를 이용하여 간단하게 표현할 수 있다.
runCatching 은 어떻게 동작하는지 함수의 원형을 살펴보자!
runCatching
함수의 원형을 보면 아래와 같다.
@InlineOnly
@SinceKotlin("1.3")
public inline fun <R> runCatching(block: () -> R): Result<R> {
return try {
Result.success(block())
} catch (e: Throwable) {
Result.failure(e)
}
}
runCatching 함수는 try catch
로직을 그대로 사용하지만 Result
클래스로 감싸서 반환하는 것을 알 수 있다.
runCatching 함수가 반환하는 Result 란 뭘까?
Kotlin 1.3 표준 라이브러리의 코드를 살펴보자~
@SinceKotlin("1.3")
@JvmInline
public value class Result<out T> @PublishedApi internal constructor(
@PublishedApi
internal val value: Any?
) : Serializable {
public val isSuccess: Boolean get() = value !is Failure
public val isFailure: Boolean get() = value is Failure
/* ... */
public companion object {
@Suppress("INAPPLICABLE_JVM_NAME")
@InlineOnly
@JvmName("success")
public inline fun <T> success(value: T): Result<T> =
Result(value)
@Suppress("INAPPLICABLE_JVM_NAME")
@InlineOnly
@JvmName("failure")
public inline fun <T> failure(exception: Throwable): Result<T> =
Result(createFailure(exception))
}
internal class Failure(
@JvmField
val exception: Throwable
) : Serializable {
/* ... */
}
}
Result 의 value는
Result가 제공하는 함수들은 다음과 같다.
@SinceKotlin("1.3")
@JvmInline
public value class Result<out T> @PublishedApi internal constructor(
@PublishedApi
internal val value: Any?
) : Serializable {
public val isSuccess: Boolean get() = value !is Failure
public val isFailure: Boolean get() = value is Failure
/* ... */
public companion object {
@Suppress("INAPPLICABLE_JVM_NAME")
@InlineOnly
@JvmName("success")
public inline fun <T> success(value: T): Result<T> =
Result(value)
@Suppress("INAPPLICABLE_JVM_NAME")
@InlineOnly
@JvmName("failure")
public inline fun <T> failure(exception: Throwable): Result<T> =
Result(createFailure(exception))
}
internal class Failure(
@JvmField
val exception: Throwable
) : Serializable {
/* ... */
}
}
runCatching
은 Result<T>
를 반환하게 되는데, Result
가 제공하는 함수를 이용해서 다양하게 활용할 수 있다.
에러를 무시하고 null 반환
val response = runCatching {
login()
}.getOrNull()
기본값 반환
val response = runCatching {
login()
}.getOrDefault(emptyList())
에러 발생 시 다른 동작 수행
val response = runCatching {
login()
}.getOrElse { ex ->
logger.warn(ex) { "에러 발생" }
// 에러를 던지고 싶다면
throw ex
}
에러가 발생한 경우에만 해당 에러 객체 반환
val exception = runCatching {
login()
}.exceptionOrNull()
// 위에서 받은 에러로 로직 수행
when (exception) {
/* ... */
}
에러가 발생하는지 아닌지만 확인하고 싶을 때에도 유용할 수 있다.
val isValidCredential = runCatching { tryLogin() }.exceptionOrNull() != null
성공/에러 시 각각 특정 동작 수행 후 에러 던지기
val response = runCatching {
login()
}.onSuccess {
logger.info("성공!")
}.onFailure {
logger.info("실패!")
}.getOrThrow()
runCatching으로 try finally 구현하기
runCatching {
request()
}.also {
doSomething()
}.getOrThrow()
Result에 대한 처리를 즉시 하지 않고 함수의 반환 값으로 반환하게 된다면, Result에 대한 핸들링을 다른 클래스에 위임할 수도 있다.
정리하자면 Result(runCatching)은 다음의 용도에서 사용할 수 있다.
참고