[Android] Http μ—λŸ¬

πŸ’‘λ„€νŠΈμ›Œν¬ μš”μ²­ 쀑에 λ°œμƒν•  수 μžˆλŠ” μ—λŸ¬μ— λŒ€ν•΄ μ•Œμ•„λ³΄κ³ , ν•΄κ²° λ°©μ•ˆμ„ κΈ°λ‘ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

 

HTTP

  • Androidμ—μ„œ HTTP μ—λŸ¬λŠ” 주둜 λ„€νŠΈμ›Œν¬ μš”μ²­ 쀑에 λ°œμƒν•˜λ©°, μ΄λŠ” μ„œλ²„ κ°„μ˜ 톡신이 μ‹€νŒ¨ν•˜κ±°λ‚˜ μ„œλ²„κ°€ μš”μ²­μ„ μ²˜λ¦¬ν•˜μ§€ λͺ»ν•  λ•Œ λ‚˜νƒ€λƒ…λ‹ˆλ‹€.
  • μ΄λŠ” HTTP μƒνƒœ μ½”λ“œ, λ„€νŠΈμ›Œν¬ μ—°κ²° 문제, μ„œλ²„μ˜ 응닡 처리 였λ₯˜ λ“±μ˜ μ΄μœ κ°€ μžˆμ„ 수 μžˆμŠ΅λ‹ˆλ‹€.
  • μ•„λž˜μ™€ 같은 일반적인 였λ₯˜λ‘œ λΆ„λ₯˜ν•  수 있고, JSON νŒŒμ‹±μ΄λ‚˜ SSL μΈμ¦μ„œ 였λ₯˜λ„ λ°œμƒν•  수 μžˆμŠ΅λ‹ˆλ‹€.

4XX

  • HTTP μƒνƒœ μ½”λ“œ 4XXλ₯Ό 가지면 ν΄λΌμ΄μ–ΈνŠΈ 였λ₯˜λ‘œ λΆ„λ₯˜ν•©λ‹ˆλ‹€.
  • 400 : Bad Resquest
  • 401 : Unauthorized
  • 403 : Forbidden
  • 404 : Not Found

5XX

  • HTTP μƒνƒœ μ½”λ“œ 5XXλ₯Ό 가지면 μ„œλ²„ 였λ₯˜λ‘œ λΆ„λ₯˜ν•©λ‹ˆλ‹€.
  • 500 : Internal Server Error
  • 502 : Bad Getway
  • 503 : Service Unabilable
  • 504 : Getway Timeout

λ„€νŠΈμ›Œν¬ 였λ₯˜

  • λ„€νŠΈμ›Œν¬ 연결이 λΆˆμ•ˆμ •ν•˜κ±°λ‚˜, Wi-Fi λ˜λŠ” λͺ¨λ°”일 데이터가 λŠκ²Όμ„ λ•Œ λ„€νŠΈμ›Œν¬ μ—°κ²° μ‹€νŒ¨ μ—λŸ¬κ°€ λ°œμƒν•©λ‹ˆλ‹€.
  • μ„œλ²„κ°€ 일정 μ‹œκ°„ 내에 μ‘λ‹΅ν•˜μ§€ μ•ŠμœΌλ©΄ νƒ€μž„ 아웃이 λ°œμƒν•©λ‹ˆλ‹€.

λΆ€μ μ ˆν•œ μ˜ˆμ™Έμ²˜λ¦¬

  • μ•ˆλ“œλ‘œμ΄λ“œμ—μ„œλŠ” HTTP μš”μ²­μ„ ν•  λ•Œ 주둜 Retrofitμ΄λ‚˜ OKHttp와 같은 라이브러리λ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€.
  • μ΄λŸ¬ν•œ 라이브러리λ₯Ό 톡해 HTTP μš”μ²­ 및 μ—λŸ¬λ₯Ό μ²˜λ¦¬ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  • view와 데이터λ₯Ό μ—°κ²°ν•˜λŠ” ViewModelμ—μ„œ μ•„λž˜μ™€ 같이 μ—λŸ¬ 처리λ₯Ό 진행할 수 μžˆμŠ΅λ‹ˆλ‹€.
// Retrofit API Interface
interface ApiService {
    @GET("example/data")
    suspend fun getData(): Response<Data>
}

// ViewModel
class MyViewModel(private val apiService: ApiService) : ViewModel() {

    fun fetchData() {
        viewModelScope.launch {
            try {
                val response = apiService.getData()
                if (response.isSuccessful) {
                    // μ„±κ³΅μ μœΌλ‘œ 데이터λ₯Ό 받은 경우
                    val data = response.body()
                    // 처리 둜직
                } else {
                    // HTTP μ—λŸ¬ 처리
                    handleHttpError(response.code())
                }
            } catch (e: Exception) {
                // λ„€νŠΈμ›Œν¬ 였λ₯˜ λ˜λŠ” 기타 μ˜ˆμ™Έ 처리
                handleNetworkError(e)
            }
        }
    }

    private fun handleHttpError(code: Int) {
        when (code) {
            400 -> Log.e("Error", "Bad Request")
            401 -> Log.e("Error", "Unauthorized")
            404 -> Log.e("Error", "Not Found")
            500 -> Log.e("Error", "Internal Server Error")
            else -> Log.e("Error", "Unknown HTTP Error: $code")
        }
    }

    private fun handleNetworkError(e: Exception) {
        if (e is SocketTimeoutException) {
            Log.e("Error", "Request Timeout")
        } else if (e is UnknownHostException) {
            Log.e("Error", "No Internet Connection")
        } else {
            Log.e("Error", "Unknown Network Error: ${e.message}")
        }
    }
}
  • μ΄λŠ” μƒνƒœ μ½”λ“œμ™€ λ„€νŠΈμ›Œν¬, νƒ€μž„μ•„μ›ƒμ— λŒ€ν•œ μ—λŸ¬λ₯Ό λΆ„κΈ°μ²˜λ¦¬ν•œ λͺ¨μŠ΅μ΄μ§€λ§Œ, μ•„λž˜μ™€ 같은 λ¬Έμ œκ°€ λ°œμƒν•©λ‹ˆλ‹€.

μ„œλ²„ 응닡 μ½”λ“œλ₯Ό ViewModel에 μ „νŒŒ

μœ„ μ½”λ“œλŠ” μ„œλ²„ 응닡 μ½”λ“œλ₯Ό ViewModelμ—μ„œ μ•Œκ³  μžˆμŠ΅λ‹ˆλ‹€. ViewModel은 μ•„ν‚€ν…μ²˜μ—μ„œ UI와 데이터 μ‚¬μ΄μ˜ μ€‘μž¬μž 역할을 μˆ˜ν–‰ν•©λ‹ˆλ‹€. λ”°λΌμ„œ ViewModel이 μ„œλ²„ 응닡 μ½”λ“œλ‚˜ 데이터 μΆœμ²˜μ— λŒ€ν•΄ μ•„λŠ” 것은 λ°”λžŒμ§ν•˜μ§€ μ•ŠμœΌλ©°, ViewModel은 데이터가 μ–΄λ””μ„œ μ˜€λŠ”μ§€ 상관없이 UIλ₯Ό μ—…λ°μ΄νŠΈν•˜λŠ” 역할에 집쀑해야 ν•©λ‹ˆλ‹€.

관심사 뢄리

  • ViewModel은 UI의 μƒνƒœλ₯Ό κ΄€λ¦¬ν•˜κ³ , 데이터가 UI에 μ–΄λ–»κ²Œ ν‘œμ‹œλ μ§€λ₯Ό κ²°μ •ν•˜λŠ” 역할을 ν•΄μ•Ό ν•©λ‹ˆλ‹€.
  • μ„œλ²„, 둜컬 λ°μ΄ν„°λ² μ΄μŠ€ λ“±μ˜ 데이터 μΆœμ²˜μ™€ μ‘λ‹΅μ½”λ“œλŠ” Repositoryλ‚˜ DataSource κ³„μΈ΅μ—μ„œ 관리해야 ν•˜λ©°, ViewModel은 λ°μ΄ν„°μ˜ μΆœμ²˜μ™€ λ¬΄κ΄€ν•˜κ²Œ ν•„μš”ν•œ 데이터λ₯Ό μ œκ³΅λ°›μ•„μ•Ό ν•©λ‹ˆλ‹€.

λΉ„μ¦ˆλ‹ˆμŠ€ 둜직이 ViewModel둜 μ „νŒŒ

λ§Œμ•½ ViewModel이 μ„œλ²„ 응닡 μ½”λ“œλ₯Ό μ•Œκ²Œ λœλ‹€λ©΄, μ΄λŠ” λΉ„μ¦ˆλ‹ˆμŠ€ 둜직이 ViewModel에 ν¬ν•¨λ˜μ–΄ κ΄€μ‹¬μ‚¬μ˜ 뢄리가 λ¬΄λ„ˆμ§ˆ 수 μžˆμŠ΅λ‹ˆλ‹€. νŠΉμ • 데이터 μΆœμ²˜μ— 쒅속적이 λ˜μ–΄, ν…ŒμŠ€νŠΈν•˜κΈ° μ–΄λ ΅κ³  μ½”λ“œκ°€ λ³΅μž‘ν•΄μ§ˆ 수 μžˆμŠ΅λ‹ˆλ‹€.

Result 객체 ν™œμš©

Result 객체?

  • Result κ°μ²΄λŠ” λ°μ΄ν„°μ˜ 처리 κ²°κ³Όλ₯Ό 성곡과 μ‹€νŒ¨λ‘œ λ‚˜λˆ„μ–΄ ν‘œν˜„ν™”λŠ” ν‘œμ€€ν™”λœ 데이터 λž˜νΌμž…λ‹ˆλ‹€.
  • 이λ₯Ό ν™œμš©ν•΄μ„œ ν•¨μˆ˜λ‚˜ λ©”μ„œλ“œκ°€ 성곡적인 결과와 μ‹€νŒ¨ 상황을 λͺ…ν™•ν•˜κ²Œ κ΅¬λΆ„ν•˜κ³ , 이λ₯Ό ν•˜λ‚˜μ˜ λ°˜ν™˜ κ°’μœΌλ‘œ μ²˜λ¦¬ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
sealed class Result<out T> {
    data class Success<out T>(val data: T) : Result<T>()
    data class Error(val exception: Throwable) : Result<Nothing>()
}
  • Kotlinμ—μ„œλŠ” 기본적으둜 Kotlin.Resultλ₯Ό μ œκ³΅ν•˜μ§€λ§Œ, 이λ₯Ό μ»€μŠ€ν…€ν•˜μ—¬ λ”μš± μœ μ—°ν•˜κ²Œ ν™œμš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

μ„ΈλΆ„ν™”λœ Result 객체

  • Result 객체λ₯Ό μ—λŸ¬ μ²˜λ¦¬μ— μ ν•©ν•˜κ²Œ ν™œμš©ν•˜κΈ° μœ„ν•΄μ„œ μ„ΈλΆ„ν™”ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
sealed class Result<out T> {
    data class Success<out T>(val data: T) : Result<T>()
    data class HttpError(val statusCode: Int) : Result<Nothing>() // HTTP μƒνƒœ μ½”λ“œ μ—λŸ¬
    data class NetworkError(val exception: Throwable) : Result<Nothing>() // λ„€νŠΈμ›Œν¬ μ—λŸ¬ (νƒ€μž„μ•„μ›ƒ, μ—°κ²° μ‹€νŒ¨ λ“±)
}
  • 이λ₯Ό ν™œμš©ν•˜λ©΄ HTTP μƒνƒœ μ½”λ“œ, λ„€νŠΈμ›Œν¬ 였λ₯˜, νƒ€μž„μ•„μ›ƒμ— 맞게 μ„ΈλΆ„ν™”ν•  수 있으며, ꡬ체적인 μ—λŸ¬ μ²˜λ¦¬κ°€ κ°€λŠ₯ν•©λ‹ˆλ‹€.

Repositoryμ—μ„œ μ—λŸ¬ 처리

  • Repositoryμ—μ„œ μ—λŸ¬μ²˜λ¦¬λ₯Ό μ§„ν–‰ν•˜μ—¬ λΉ„μ¦ˆλ‹ˆμŠ€ 둜직이 ViewModel둜 μ „νŒŒλ˜λŠ” 것을 방지할 수 μžˆμŠ΅λ‹ˆλ‹€.
  • ViewModel둜 Result둜 감싸진 객체λ₯Ό μ „λ‹¬ν•˜μ—¬ ViewModelμ—μ„œλŠ” μ—λŸ¬ μ½”λ“œλ₯Ό μ•Œ 수 μ—†κ²Œ ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
class DataRepository(private val apiService: ApiService) {
    
    suspend fun getData(): Result<List<String>> {
        return try {
            val response = apiService.getData() // μ„œλ²„μ— 데이터 μš”μ²­
            if (response.isSuccessful) {
                Result.Success(response.body() ?: emptyList())
            } else {
                Result.HttpError(response.code()) // HTTP μƒνƒœ μ½”λ“œ μ—λŸ¬ 처리
            }
        } catch (e: IOException) {
            // λ„€νŠΈμ›Œν¬ μ˜ˆμ™Έ 처리 (λ„€νŠΈμ›Œν¬ μ—°κ²° μ‹€νŒ¨, νƒ€μž„μ•„μ›ƒ λ“±)
            Result.NetworkError(e)
        }
    }
}

UI둜 μ „νŒŒ

  • 감싸진 Result객체λ₯Ό λΆ„μ„ν•˜μ—¬ μ‚¬μš©μžμ—κ²Œ λ‹€λ₯Έ UIλ₯Ό 보여쀄 수 μžˆμŠ΅λ‹ˆλ‹€.
// ViewModelμ—μ„œ 데이터λ₯Ό κ°€μ Έμ˜€κ³  Result에 따라 λΆ„κΈ° 처리
class MyViewModel(private val repository: DataRepository) : ViewModel() {
    private val _uiState = MutableLiveData<Result<List<String>>>()
    val uiState: LiveData<Result<List<String>>> get() = _uiState

    fun loadData() {
        viewModelScope.launch {
            _uiState.value = Result.Loading
            val result = repository.getData()
            _uiState.value = result // λ„€νŠΈμ›Œν¬ μ—λŸ¬, μƒνƒœ μ½”λ“œ 등을 μ²˜λ¦¬ν•œ Result λ°˜ν™˜
        }
    }
}

// UIμ—μ„œ Result 객체의 μƒνƒœμ— 따라 UI κ°±μ‹ 
@Composable
fun MyScreen(viewModel: MyViewModel = hiltViewModel()) {
    val uiState by viewModel.uiState.observeAsState(Result.Loading)

    when (uiState) {
        is Result.Success -> ShowData((uiState as Result.Success).data) // 성곡 μ‹œ 데이터 λ Œλ”λ§
        is Result.HttpError -> ShowError("HTTP Error: ${(uiState as Result.HttpError).statusCode}") // HTTP μƒνƒœ μ½”λ“œ 처리
        is Result.NetworkError -> ShowError("Network Error: ${(uiState as Result.NetworkError).exception.message}") // λ„€νŠΈμ›Œν¬ μ—λŸ¬ 처리
    }
}

이런 과정이 ν•„μš”ν•œ μ΄μœ λŠ”?

λΆ„λ¦¬λœ μ—λŸ¬ 처리

  • ViewModel은 λ‹¨μˆœνžˆ UI μƒνƒœλ₯Ό κ΄€λ¦¬ν•˜κ³ , 세뢀적인 λ„€νŠΈμ›Œν¬ 및 HTTP μƒνƒœ μ½”λ“œμ— λŒ€ν•œ μ²˜λ¦¬λŠ” Repository둜 λΆ„λ¦¬λ©λ‹ˆλ‹€.
  • μ΄λŠ” μœ μ§€λ³΄μˆ˜λ₯Ό μš©μ΄ν•˜κ²Œ ν•˜κ³ , μ½”λ“œμ˜ 관심사 뢄리λ₯Ό μœ μ§€ν•˜λŠ” 데 도움을 μ€λ‹ˆλ‹€.

ν…ŒμŠ€νŠΈ μš©μ΄μ„±

  • 각 μƒνƒœλ₯Ό λΆ„λ¦¬ν•¨μœΌλ‘œμ¨ κ°œλ³„μ μΈ ν…ŒμŠ€νŠΈκ°€ κ°€λŠ₯ν•©λ‹ˆλ‹€.
  • 성곡 응닡, HTTP μƒνƒœ μ—λŸ¬, λ„€νŠΈμ›Œν¬ μ—λŸ¬λ₯Ό 각각 ν…ŒμŠ€νŠΈν•  수 μžˆμŠ΅λ‹ˆλ‹€.

μ‚¬μš©μž κ²½ν—˜ ν–₯상

  • λ‹€μ–‘ν•œ μ—λŸ¬ 상황에 따라 μ μ ˆν•œ λ©”μ‹œμ§€λ‚˜ UI ν”Όλ“œλ°±μ„ μ œκ³΅ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  • μ΄λŠ” μ‚¬μš©μžκ°€ μ˜ˆμƒμΉ˜ λͺ»ν•œ 상황을 λ°©μ§€ν•˜λ©°, μΉœμ ˆν•œ μ˜ˆμ™Έ μ „λ‹¬λ‘œ μ‚¬μš©μž κ²½ν—˜μ„ 크게 ν–₯μƒμ‹œν‚¬ 수 μžˆμŠ΅λ‹ˆλ‹€.

Sandwich?

  • λ³΅μž‘ν•œ μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ•„ν‚€ν…μ²˜μ—μ„œ API 처리λ₯Ό 진행할 λ•Œ, 맀번 μ˜ˆμ™Έμ²˜λ¦¬ μ½”λ“œλ₯Ό μž‘μ„±ν•˜λŠ” 것은 λ§Žμ€ μ‹œκ°„μ„ μ†Œλͺ¨ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  • SandwichλŠ” Multi-Layred μ•„ν‚€ν…μ²˜μ—μ„œ 데이터 흐름과 Retrofit 응닡 및 μ—λŸ¬λ₯Ό κ°„κ²°ν•˜κ³  λͺ…μ‹œμ μœΌλ‘œ λ‹€λ£¨λŠ” 라이브러리λ₯Ό μ œκ³΅ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.

https://github.com/skydoves/sandwich