[Kotline Coroutine] OnFailure에 로그적기 귀찮아

안녕하세요! 오늘은 Result<T>를 사용할 때 실패 시마다 반복해서 적던 로그 코드를 줄이기 위해 만든 확장 함수를 소개하려고 합니다. 사실 정말 간단해요ㅎㅎ

1.  왜 만들었어요?

제가 참여중인 Spoony에서는 런캐칭으로 감싸 뷰모델에서 onSuccess와 onFailure로 결과값을 처리하고 있었어요. 그러나 작업자마다 로그를 찍는 게 일관적이지 않았고 매번 실패 시에 Timber.e(e)를 적는 게 귀찮게 다가왔어요. 사용되는 부분이 많다 보니 매번 팀버 코드를 작성해야 했고 확장함수로 만드면 어떻냐는 팀원의 아이디어가 나왔어요.

그래서 바로 작업에 들어갔습니다.

2.  Result 실패 처리 반복

현재 ViewModel에서 다음과 같이 runCatching과 Result<T>를 조합해 API 호출의 성공/실패를 처리하고 있어요:

viewModelScope.launch {
    repository.exampleMethod()
    .onSuccess {
        // 성공 처리
    }.onFailure { e ->
        Timber.e(e)
        // 실패 처리
    }
}

그런데 위 코드에서 Timber.e(e)는 매번 중복되며, 실수로 빠뜨릴 수도 있어요. 코드 양도 늘어나고 실수의 여지도 생기죠.

3.  확장 함수로 공통 처리 묶기

그래서 다음과 같은 확장 함수를 만들었어요.

suspend inline fun <T> Result<T>.onLogFailure(
    crossinline action: suspend (exception: Throwable) -> Unit
): Result<T> =
    onFailure { e ->
        Timber.e(e)
        action(e)
    }

이제 .onFailure { Timber.e(it) }를 일일이 쓰지 않아도. onLogFailure {... }만 쓰면 돼요.

4.  확장 함수 코드 뜯어보기

되게 간단해 보이지만 자세히 보면 좋아요ㅎㅎ

📌 suspend inline fun <T> Result<T>.onLogFailure(...)

  • suspend: 코루틴 내부에서도 사용할 수 있도록 해요. 즉, action 블록에서 suspend 함수를 호출할 수 있어요.
  • inline: 성능 최적화를 위해 함수 본문을 호출 지점에 인라인으로 삽입해요. 람다를 인라인 처리해서 불필요한 객체 생성을 줄일 수 있어요.
  • <T>: 제네릭을 사용해서 어떤 타입의 Result <T>든 사용할 수 있게 만들었어요.

📌 crossinline action: suspend (Throwable) -> Unit

  • crossinline: action 람다가 비지역 반환(non-local return)을 하지 못하게 막아줘요. 이는 람다를 인라인 하면서 생길 수 있는 제어 흐름 오류를 방지해 줘요.
  • suspend: action 내부에서 네트워크 호출, 디스크 IO 등 suspend 함수 호출이 가능하게 해줘요.

📌 onFailure { e -> ... }

  • Kotlin 표준 라이브러리의 Result.onFailure() 확장 함수예요. 내부적으로 Result.isFailure가 true일 때만 실행돼요.
  • e는 Throwable 타입으로, 실패 원인을 담고 있어요.

📌 Timber.e(e)

  • 실패 시 자동으로 Timber 로그를 남겨요. Throwable을 그대로 넘기면 스택 트레이스도 같이 출력돼 디버깅에 좋아요. 혹은 본인의 프로젝트에서 사용하는 다른 로그 라이브러리를 사용해도 좋아요.

📌 action(e)

  • 호출한 곳에서 정의한 실패 핸들링 로직을 실행해요. 로그 이후 추가 처리를 할 수 있어요 (예: 사용자에게 토스트 띄우기 등).

5.  사용 예시

기존 코드

fun selectPlace(place: Place) {
        viewModelScope.launch {
            repository.checkDuplicatePlace(
                latitude = place.latitude,
                longitude = place.longitude
            ).onSuccess {
                }
            }.onFailure {
		            Timber.e(e)
                _sideEffect.emit(RegisterSideEffect.ShowSnackbar("장소 선택 중 오류가 발생했습니다"))
            }
        }

이제는 이렇게 줄일 수 있어요

fun selectPlace(place: Place) {
        viewModelScope.launch {
            repository.checkDuplicatePlace(
                latitude = place.latitude,
                longitude = place.longitude
            ).onSuccess { 
                }
            }.onLogFailure {
                _sideEffect.emit(RegisterSideEffect.ShowSnackbar("장소 선택 중 오류가 발생했습니다"))
            }
        }

깔끔하죠?

결론

작은 확장 함수 하나지만, 많은 Result 처리 로직에 적용되면 전반적인 코드 품질과 생산성이 높아져요. 로그 실수도 줄이고, 에러 핸들링을 더 명시적으로 표현할 수 있어서 실무 프로젝트에 적극 추천드려요!

읽어 주셔서 감사합니다 🙌