본문 바로가기
Kotlin 공부 노트

[Kotlin] Result 알아보기! - 예제를 통한 사용법

by 지게요 2025. 9. 22.
728x90
반응형

이번 포스팅은 전편 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에 대해 공부하면서 글을 작성해 봤는데 생각보다 지원하고 있는 함수와 예외에 따른 처리를 해줄 때 정말 유용하게 사용할 수 있을 거 같다는 것을 느꼈다.

 

 

반응형