[코틀린] 코틀린의 예외 처리 runCatching

berry·2023년 4월 3일
0

코틀린에서는 자바의 try catch 기능도 제공하지만, runCatching 을 이용하면 다른 방법으로 에러를 핸들링할 수 있다.

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 은 어떻게 동작하는지 함수의 원형을 살펴보자!

kotlin.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 클래스로 감싸서 반환하는 것을 알 수 있다.

  • 에러가 발생하지 않았을 때에는 Result.success 반환
  • 에러가 발생했을 때에는 Result.failure 반환

runCatching 함수가 반환하는 Result 란 뭘까?

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는

  • 성공일 경우 T를 타입으로 하는 값을 가지게 되고
  • 실패일 경우 Failure를 wrapper class로 하는 exception 을 값으로 가지게 된다.

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 {
    /* ... */
  }
}

Result 사용 예시

runCatchingResult<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에 대한 핸들링을 다른 클래스에 위임할 수도 있다.

결론

정리하자면 Result(runCatching)은 다음의 용도에서 사용할 수 있다.

  • 외부 서비스에 의존하는 로직이라 예외 발생 가능성이 빈번한 컴포넌트
  • 해당 컴포넌트에서 에러가 발생할 수 있다는 것을 클라이언트에게 알려주고 싶을 때, 에러 핸들링을 다른 컴포넌트에 강제하고 위임하고 싶을 때
  • try catch를 쓰고 싶지 않을 때

참고

profile
공부 내용 기록

0개의 댓글