이번 포스팅은 전편 runCatching에 이어서 알아보면 좋은 Result를 알아보겠다.
# Result
쉽게 한 문장으로 정리하자면 성공 값 또는 실패(예외)를 담는 value class이다.
@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 {
/* ... */
}
}
@PublishedApi
@SinceKotlin("1.3")
internal fun createFailure(exception: Throwable): Any =
Result.Failure(exception)
해당 위 코드는 Result 클래스의 내부 코드이다. 내부코드를 살펴보면서 하나하나 알아보자!
🟩 Kotlin 버전 1.3에 처음 등장 (전에 알아본 runCatching는 1.4)
🟩 isSuccess, isFailure 두 개의 상태를 판별할 수 있는 프로퍼티를 가지고 있다.
🟩 companion object에 있는 코드를 보면 value는 성공 -> T 타입, 실패 -> createFailure(exception)의 반환값(Failure 객체) 타입으로 저장된다.
크게는 위와 같은 특징을 가지고 있고 Result가 제공하는 함수도 여러 가지가 있다.
## Result가 제공하는 함수
### 생성 함수
✅ Result.success(value)
- 성공 Result를 생성한다.
val result = Result.success("OK") // 성공 Result 생성
✅ Result.failure(exception)
- 실패 Result를 생성한다.
val result = Result.failure(Exception("에러")) // 실패 Result 생성
✅ runCatching
- 전편에서 알아본 함수로 예외가 발생하면 자동으로 Result.failure로, 성공하면 Result.success로 변환한다.
// 예외가 발생할 수 있는 코드를 안전하게 실행
val result1 = runCatching {
"10".toInt()
} // Result.success(10)
val result2 = runCatching {
"abc".toInt()
} // Result.failure(NumberFormatException)
### 상태 확인
✅ result.isSuccess
- 해당 result 결과가 성공적인 결과인지 판단한다. (성공 - true 반환)
val result = Result.success("OK") // 성공 Result 생성
val isSuccess = result.isSuccess
print(isSuccess) // 성공이니 ture 출력
✅ result.isFailure
- 해당 result 결과가 실패 결과인지 판단한다. (실패 - true 반환)
val result = Result.failure(Exception("에러")) // 실패 Result 생성
val isFailure = result.isFailure
print(isFailure) // 실패이니 ture 출력
### 값 추출
✅ result.getOrNull()
- 해당 result가 성공이면 T 값 반환 실패면 null 반환한다.
val successResult = Result.success("OK") // 성공 Result 생성
val failureResult = Result.failure(Exception("에러")) // 실패 Result 생성
val failure = failureResult.getOrNull() // 실패라서 null 반환
val success = successResult.getOrNull() // 성공이라서 "OK" 반환
✅ result.exceptionOrNull()
- 해당 result가 실패한 경우 예외 반환 성공인 경우 null 반환한다. (getOrNull()과 반대 개념)
val successResult = Result.success("OK") // 성공 Result 생성
val failureResult = Result.failure(Exception("에러")) // 실패 Result 생성
val failure = failureResult.exceptionOrNull() // 실패라서 Exception 반환
val success = successResult.exceptionOrNull() // 성공이라서 null 반환
✅ result.getOrThrow()
- 해당 result가 성공하면 값을 반환하고 실패하면 예외를 반환.
val successResult = Result.success("OK") // 성공 Result 생성
val failureResult = Result.failure(Exception("에러")) // 실패 Result 생성
val failure = failureResult.getOrThrow() // 실패라서 Exception 반환
val success = successResult.getOrThrow() // 성공이라서 "OK" 반환
✅ result.getOrDefault()
- 해당 result가 실패하면 미리 정해둔 기본값 반환.
val successResult = Result.success("OK") // 성공 Result 생성
val failureResult = Result.failure(Exception("에러")) // 실패 Result 생성
val failure = failureResult.getOrDefault("기본값") // 실패라서 "기본값" 반환
val success = successResult.getOrDefault("기본값") // 성공이라서 "OK" 반환
✅ result.getOrElse()
- 해당 result가 실패하면 람다 실행 결과를 반환.
- getOrDefault() 차이점
| 구분 | getOrDefault | getOrElse |
| 계산 시점 | 성공해도 미리 계산됨 😵💫 | 실패시에만 계산 실행 ✅ |
| 예외 정보 활용 | 불가능 ❌ | 가능 ✅ |
val successResult = Result.success("OK") // 성공 Result 생성
val failureResult = Result.failure(Exception("에러")) // 실패 Result 생성
// 실패라서 "기본값 에러" 반환
val failure = failureResult.getOrElse { exception ->
"기본값 ${exception.message}" // 람다에서 예외 정보 활용 가능
}
val success = successResult.getOrElse { "기본값" } // 성공이라서 "OK" 반환
## 그래서 Result는 언제 사용해?
⭕️ 사용해야 하는 경우
- 예외에 따른 처리를 커스텀할 때
Ex) 서버 API를 통해 유저 정보를 가져오는 경우
fun fetchData(): Result<Data> = runCatching {
apiService.getData()
}
// 사용
fetchData().onFailure { error ->
when (error) {
is SocketTimeoutException -> print("연결 시간 초과, 다시 시도해주세요")
is UnknownHostException -> print("인터넷 연결을 확인해주세요")
is HttpException -> print("서버 오류가 발생했습니다")
}
}
위와 같은 경우 서버에서 데이터를 가져올 때 여러 가지 예외가 발생할 수 있는데 예외마다 다른 처리를 해줘야 할 때 유용하다!
- 성공과 실패에 대한 체이닝으로 간결한 코드가 필요할 때
Ex) 서버 API를 통해 유저 정보를 가져오고 변환 작업을 안전하게 하는 경우
val name = runCatching { apiService.getUser() }
.map { user -> user.name } // 성공 시 변환
.getOrElse { "게스트" } // 실패 시 기본값
위 경우 서버에서 유저 정보를 가져올 때 성공했을 시 유저의 이름을 추출하는 작업이다.
만약 Result를 사용 안 하고 해당 작업을 하려면 아래와 같이 try-catch에서 따로 작업을 해줘야 한다.
val name = try {
val user = apiService.getUser()
user.name
} catch (e: Exception) {
"게스트"
}
확실히 Result의 체이닝으로 짧고 선언적으로 표현이 가능해진다!
❌ 사용하지 말아야 하는 경우
- 직렬화가 필요한 데이터
Ex) Result를 포함하고 있는 Response 직렬화가 필요한 경우
// ❌ 잘못된 예시 - Result는 직렬화하기 복잡함
@Serializable
data class ApiResponse(
val userData: Result<User> // 직렬화 문제 발생
)
이 경우 Result 클래스는 @Serializable 애노테이션이 없어서 kotlinx.serialization이 어떻게 직렬화할지 모르기 때문에 컴파일 타임 때 오류를 발생시킨다.
따라서 가장 간단하고 추천하는 방법은 아래처럼 직렬화 가능한 구조로 변경하는 것
// ✅ 올바른 예시 - 직렬화 가능한 구조
@Serializable
data class ApiResponse(
val success: Boolean,
val data: User? = null,
val errorMessage: String? = null
)
- 여러 UI 상태(Loading, Empty 등)를 표현해야 할 때
Ex) 게시글 목록을 가져오는 경우
만약 아래와 같은 조건이 있다고 치자
- 데이터를 가져오기 전에는 Loading
- 데이터가 하나도 없으면 Empty
- 정상적으로 가져오면 Success
- 네트워크나 서버 오류가 발생하면 Error
// ❌ 잘못된 예시 - Result로 상태를 모두 표현하려는 경우
val state: Result<List<Post>>
하지만 위와 같이 Result로 상태를 모두 표현하려니까 Loading, Empty 상태를 나타낼 수 있는 방법이 없다. 나타내더라도 결국 다른 변수를 추가해야 한다.
이처럼 여러 UI 상태를 표현해야 할 때 사용하지 말아야 한다.
그럼 이런 경우에는 어떤 식으로 표현해야 할까? 정답은 sealed class로 명확하게 상태를 표현해야 한다!
// ✅ 올바른 예시 - 다양한 상태를 나타내는 sealed class
sealed class PostListState {
object Loading : PostListState()
object Empty : PostListState()
data class Success(val posts: List<Post>) : PostListState()
data class Error(val throwable: Throwable) : PostListState()
}
위와 같이 명확하게 표현해 주면 여러 상태 UI 핸들링이 가능하다.
이처럼 Result에 대해 공부하면서 글을 작성해 봤는데 생각보다 지원하고 있는 함수와 예외에 따른 처리를 해줄 때 정말 유용하게 사용할 수 있을 거 같다는 것을 느꼈다.
'Kotlin 공부 노트' 카테고리의 다른 글
| [Kotlin] 예외처리 runCatching를 알아보자! - try-catch 차이점 (6) | 2025.08.10 |
|---|---|
| [Kotlin Flow] StateFlow vs SharedFlow 예제와 함께 차이점 알아보기! (0) | 2024.11.30 |
| [Kotlin Flow] 코틀린 Flow란? - 예제와 함께 알아보기 (2) | 2024.09.17 |
| [Kotlin-Coroutine] coroutine Mutex(상호배제) 예제를 통한 사용법 (2) | 2024.06.09 |
| [Android-Kotlin] Groovy DSL -> Kotlin DSL Migration(코틀린 DSL로 의존성 관리 마이그레이션), Kotlin DSL이란? (2) | 2023.10.29 |