반복되는 에러처리 리팩토링하기!

주효은·2024년 12월 16일
0

오늘은 RepositoryImpl에서 API 요청할 때 반복되는 코드를 한 번에 해결하는 방법을 공유하려고 합니다. 이 부분도 코드리뷰로 달렸던 부분 중 하나인데요! 사실 생각해보니 서버 붙일 때는 항상 똑같은 일을 하고 있다고만 생각했지; 이를 개선해볼 생각을 따로 하진 않았네요..(반성) 비슷한 코드를 반복해서 작성하다 보면 지치고, 유지보수할 때는 더 고생스럽잖아요. 그런 고생을 미리 막기 위해 간단한 글을 작성해봤습니다!

class MyPageRepositoryImpl @Inject constructor(
    private val myPageService: MyPageService
) : MyPageRepository {

    override suspend fun getMyHobby(token: String): Result<ResponseMyHobbyModel> = runCatching {
        val response = myPageService.getMyHobby(token)

        response.result?.let {
            ResponseMyHobbyModel(hobby = it.hobby)
        } ?: throw Exception("데이터를 불러오는데 실패했습니다")
    }

제가 과제로 제출했던 코드 중 일부인데

  • API 호출 (myPageService.getMyHobby(token))
  • 결과가 있는지 확인 (response.result?.let)
  • 성공하면 모델 매핑 (ResponseMyHobbyModel(hobby = it.hobby))
  • 실패하면 예외 발생 (throw Exception)

이런 과정으로 대부분의 API에서 반복된 경험 있지 않으신가요!!

반복은 시간이 갈수록 코드 가독성을 해치고, 유지보수를 어렵게 만듭니다.

이렇게 반복됐던 경험이..

그럼 이제 바꿔보자!


1. 공통 로직 함수로 추출하기

‘반복되는 흐름 → 함수로 추출’이라는건 너무 자명한 사실이죵

공통 로직을 하나의 함수로 추출해서 많은 API 호출에서 재사용해보려고 합니당

suspend fun <T, R> apiCall(
    call: suspend () -> T?,
    transform: (T) -> R
): Result<R> = runCatching {
    call()?.let(transform) ?: throw Exception("데이터를 불러오는데 실패했습니다")
}

이제 직접 함수에 적용해보면?

override suspend fun getMyHobby(token: String): Result<ResponseMyHobbyModel> =
    apiCall(
        call = { myPageService.getMyHobby(token).result },
        transform = { ResponseMyHobbyModel(hobby = it.hobby) }
    )

2. Result 확장함수

API 요청마다 성공 시 데이터 변환, 실패 시 예외 처리를 반복하면 코드가 지저분해질 수 있습니다. 이걸 깔끔하게 처리하려면 Result 확장 함수를 쓰면 됩니다!

inline fun <T, R> Result<T>.mapResult(transform: (T) -> R): Result<R> =
    this.mapCatching {
        it?.let(transform) ?: throw Exception("데이터를 불러오는데 실패했습니다")
    }

리팩터링 전

runCatching {
    val response = apiService.getSomething()
    response.result?.let { ResponseModel(it.data) } ?: throw Exception("데이터 없음")
}

리팩터링 후

runCatching { apiService.getSomething().result }
    .mapResult { ResponseModel(it.data) }
    

실제 적용 결과는?

override suspend fun getMyHobby(token: String): Result<ResponseMyHobbyModel> =
    runCatching { myPageService.getMyHobby(token).result }
        .mapResult { ResponseMyHobbyModel(hobby = it.hobby) }

추가) 에러 형태가 다르면요??

코드가 반복되지 않게 개선했지만, 에러 처리나 메시지가 상황에 따라 달라야 할 때도 있겠죠?

1. 에러 메시지 커스터마이징

suspend fun <T, R> apiCall(
    call: suspend () -> T?,
    transform: (T) -> R,
    errorMessage: String = "데이터를 불러오는데 실패했습니다"
): Result<R> = runCatching {
    call()?.let(transform) ?: throw Exception(errorMessage)
}

2. CustomException 활용

class CustomException(message: String) : Exception(message)

suspend fun <T> apiCall(call: suspend () -> T): Result<T> = runCatching {
    call() ?: throw CustomException("커스텀 에러 메시지!")
}

3. EmptyList는 따로 빼자

suspend fun <T> apiCallWithEmptyList(
    call: suspend () -> List<T>?,
    errorMessage: String = "데이터를 불러오는데 실패했습니다"
): Result<List<T>> = runCatching {
    call() ?: emptyList()
}.onFailure {
    println("Error: $errorMessage, ${it.message}")
}

0개의 댓글