Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

24장. 실패를 표현하는 여러 방법

23장에서 예외를 배웠습니다.
하지만 실패를 표현하는 방법이
예외 하나만 있는 것은 아닙니다.

  • null로 “값이 없음“을 표현할 수도 있고
  • 예외를 던질 수도 있고
  • Result라는 특별한 상자에 담을 수도 있고
  • Sealed Type으로 결과를 모델링할 수도 있습니다.

이 장에서는 이 방법들을 하나씩 살펴보고,
“어떤 상황에 무엇을 쓰면 좋은지“를 정리합니다.


24.1 Nullable로 실패 표현하기

가장 간단한 방법은 null입니다.

“찾지 못했다“는 실패를
null로 돌려줄 수 있습니다.

fun findUser(id: Long): User? {
    return users[id]   // 없으면 null
}

val user = findUser(1) ?: return

이 방법은 “실패 이유가 하나뿐“일 때 좋습니다.
그저 “없다“만 표현하면 될 때입니다.

하지만 한계가 있습니다.
null은 “왜 실패했는지“를 말해 주지 못합니다.

회원이 없어서인지,
권한이 없어서인지 구분할 수 없습니다.


24.2 Exception으로 실패 표현하기

실패 이유가 여러 가지이고,
그 실패가 “정말 예외적인 상황“이라면
예외가 적절합니다.

fun withdraw(account: Account, amount: Int) {
    if (amount > account.balance) {
        throw IllegalStateException("잔액 부족")
    }
    account.balance -= amount
}

예외의 장점은
“정상 흐름“과 “실패 흐름“을
확실히 분리한다는 점입니다.

단, 예외는 비용이 있고
흐름을 끊어 버립니다.
“자주 일어나는, 평범한 실패“에는
예외가 과할 수 있습니다.


24.3 Result

코틀린에는 Result라는 타입이 있습니다.

성공이면 값을,
실패면 예외를 담는 특별한 상자입니다.

val result: Result<Int> = runCatching {
    "123".toInt()
}

Result는 실패를 “값처럼” 다룰 수 있게 해 줍니다.
예외처럼 흐름을 끊지 않고,
결과를 손에 쥔 채로 이어서 처리할 수 있습니다.


24.4 runCatching

Result를 만드는 가장 쉬운 방법이
runCatching입니다.

runCatching 블록 안에서
예외가 나면 자동으로 실패 Result가 됩니다.

val result = runCatching {
    riskyOperation()
}

result
    .onSuccess { println("성공: $it") }
    .onFailure { println("실패: ${it.message}") }

try-catch를 직접 쓰지 않아도
성공과 실패를 깔끔하게 나눠 처리할 수 있습니다.


24.5 getOrNull, getOrElse, getOrThrow

Result에서 값을 꺼내는 방법은 여러 가지입니다.

val result = runCatching { "abc".toInt() }

result.getOrNull()          // 실패면 null
result.getOrElse { -1 }     // 실패면 기본값 -1
result.getOrThrow()         // 실패면 예외를 다시 던짐
함수실패했을 때 동작
getOrNullnull 반환
getOrElse기본값 반환
getOrThrow예외를 다시 던짐

상황에 맞게 골라 쓰면 됩니다.


24.6 Sealed Type으로 결과 표현하기

가장 표현력이 좋은 방법은
Sealed Type입니다. (10장 참고)

실패의 “종류“까지 타입으로 나눌 수 있습니다.

sealed interface PaymentResult {
    data class Success(val paymentId: Long) : PaymentResult
    data class Failed(val reason: String) : PaymentResult
    data object Pending : PaymentResult
}

when으로 처리하면
모든 경우를 빠짐없이 다루게 됩니다.

fun handle(result: PaymentResult) = when (result) {
    is PaymentResult.Success -> "결제 완료: ${result.paymentId}"
    is PaymentResult.Failed  -> "결제 실패: ${result.reason}"
    PaymentResult.Pending    -> "처리 중"
}

else 없이도
컴파일러가 “빠진 경우가 없는지” 검사해 줍니다.


24.7 어떤 방식을 선택해야 하는가

이제 정리해 봅시다.
아래 상황들을 예로 비교해 보겠습니다.

회원 없음
결제 실패
외부 API 오류
잘못된 요청
일시적인 시스템 오류

각 상황에 어울리는 방식을 정리하면 이렇습니다.

상황어울리는 방식
회원 없음Nullable
잘못된 요청Exception
결제 실패(여러 사유)Sealed Type
외부 API 오류Result
일시적 시스템 오류Exception

큰 기준은 이렇게 잡을 수 있습니다.

실패 이유가 하나면 Nullable,
정말 예외적이면 Exception,
여러 결과를 구분해야 하면 Sealed Type,
실패를 값으로 이어서 다루고 싶으면 Result.

정답이 하나로 정해져 있지는 않습니다.
중요한 것은 “실패도 설계 대상“이라는 생각입니다.


24장을 마치며

이 장에서 우리는 다음을 배웠습니다.

  • null, 예외, Result, Sealed Type으로 실패를 표현하는 네 가지 방법
  • runCatching으로 예외를 값으로 감싸는 법
  • getOrNull, getOrElse, getOrThrow의 차이
  • 상황별로 적절한 실패 표현을 고르는 기준

이것으로 8부(예외와 오류 설계)를 마칩니다.
다음 9부에서는
자바 생태계와 코틀린을 함께 쓰는 법을 다룹니다.