[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