본문 바로가기
Kotlin 공부 노트

[Kotlin] 예외처리 runCatching를 알아보자! - try-catch 차이점

by 지게요 2025. 8. 10.
728x90
반응형

이번 공부 노트는 코틀린 예외처리 사용 시 유용하게 쓰이는 runCatching의 대해 적어보겠다.

 

JAVA에서는 보통 예외처리 시 try-catch문을 통해 예외처리를 한다. 하지만 코틀린에서는 함수형 프로그래밍을 살린(코틀린다운) runCatching을 제공한다.

 

# 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)
    }
}

 

자세히 보면 사실 내부에서도 try-catch를 사용하는 것을 알 수 있다. 

따라서 runCatching함수는 혁신적인 새로운 방법이 아닌 기존 try-catch를 함수형 스타일로 사용할 수 있게 해주는 유틸리티 함수라고 볼 수 있다!

## try-catch / runCatching 차이점

### 1. 반환 방식의 차이

- try-catch: 직접 값 반환 또는 예외 발생

- runCatching: Result 객체로 감싸서 반환

자세히 예제를 통해 알아보자!

아래 예제는 10 / 0을 실행했을 때 ArithmeticException 발생시켜 보는 예제이다.

 

🔶try-catch

// try-catch: 직접 값 반환
val tryResult = try {
   10 / 0  // ArithmeticException 발생
} catch (e: Exception) {
   -1
}
println("try-catch 결과: $tryResult")  // -1

이런 식으로 try-catch는 직접 값을 반환하는 것을 볼 수 있다.

 

🔷 runCatching

// runCatching: Result 객체 반환
val catchingResult = runCatching { 10 / 0 }
println("runCatching 결과: 타입 ${catchingResult::class} 값 ${catchingResult}")  // Failure(ArithmeticException)
println("실제 값: ${catchingResult.getOrElse { -1 }}")  // -1

반면 runCatching는 Result 객체를 반환한다.

Result 객체에 대해는 다음 글에 적어놓을 예정이다. 모르는 분은 알고 오면 좀 더 이해가 수월할 것이다.

 

이처럼 runCatching의 가장 큰 특징은 예외를 Result 타입으로 감싸서 안전하게 처리할 수 있다는 점이다.

 

### 2. 함수형 체이닝 차이

- try-catch: 중첩 구조로 복잡해짐 

- runCatching: map, flatMap 등을 통한 체이닝 가능

이번도 예제를 통해 자세히 알아보자!

아래 예제는 여러 단계에서 예외가 발생할 수 있는 경우 예외처리 과정이다.

 

🔶try-catch

// try-catch: 중첩 지옥
fun processUserData(userIdStr: String): String {
    return try {
        val userId = userIdStr.toInt()  // 1단계: 파싱 실패 가능
        try {
            val userData = fetchUserFromDB(userId)  // 2단계: DB 조회 실패 가능
            try {
                val processedData = validateAndProcess(userData)  // 3단계: 검증 실패 가능
                "성공: $processedData"
            } catch (e: Exception) {
                "데이터 처리 실패"
            }
        } catch (e: Exception) {
            "사용자 조회 실패"
        }
    } catch (e: Exception) {
        "ID 파싱 실패"
    }
}

 

이 경우 물론 단계별로 함수를 만들 수 있지만 함수를 만들어도 결국 중첩 try-catch를 사용하게 된다. 그래서 코드의 가독성이 떨어지고 복잡해진다.

 

🔷 runCatching

fun processUserData(userIdStr: String): String {
    return runCatching { userIdStr.toInt() }
        .mapCatching { fetchUserFromDB(it) }      // 각 단계마다 예외 처리
        .mapCatching { validateAndProcess(it) }
        .map { "성공: $it" }
        .getOrElse { "처리 실패: ${it.message}" }
}

 

이처럼 체이닝을 통해 가독성도 높이고 복잡한 코드도 줄어들었다!

 


## runCatching 사용법

사용법이라고 할 거는 크게 없다.

우선 runCatching 스코프 안에 예외가 발생할 수 있는 작업을 실행시켜 주고 반환된 Result형으로 원하는 결과 처리를 해주면 된다!

예제를 통해 알아보자.

이번 예제는 보통 ViewModel에서 API 호출 시 사용하는 예제이다.

// API 호출 후 결과에 따른 처리
runCatching { 
    // API 호출 로직 
    apiCall() 
}.onSuccess { data -> 
    // 성공 시 로직
    updateUI(data)
}.onFailure { error -> 
    // 실패 시 로직
    logError(error)
}

 

이와 같이 Result타입의 확장함수 중 onSuccess, onFailure를 사용해서 원하는 상황에 맞게 로직을 작성해 주면 끝이다!

 

다음 예제는 기본적인 단일 작업 처리 방법이다.

// 예외가 발생할 수 있는 작업을 안전하게 실행
val result = runCatching {
    "123".toInt()  // 문자열을 정수로 변환
}

// 결과 확인
if (result.isSuccess) {
    println("성공: ${result.getOrNull()}")
} else {
    println("실패: ${result.exceptionOrNull()?.message}")
}

 

 

이 방법은 성공/실패에 따라 서로 다른 로직을 실행해야 할 때 유용하다.

하지만 단순히 값만 필요하다면 getOrElse { 기본값 }을 사용하는 것이 더 간편하다.

 


# 정리

runCatching은 기존 try-catch를 함수형 스타일로 사용할 수 있게 해주는 유틸리티다.

복잡한 예외 처리나 여러 단계의 작업에서 특히 유용하며, Result 타입을 통해 안전하고 깔끔한 코드를 작성할 수 있다.

반응형