Android 네트워크 통신 안정화 — Retrofit2, OkHttp, Coroutine, Timeout과 재시도 전략

Android 앱에서 서버 API를 호출하는 기능은 거의 필수적입니다.

로그인, 게시글 조회, 검색, 이미지 정보 다운로드, 데이터베이스 업데이트 등 다양한 기능이 네트워크 통신에 의존합니다.

하지만 개발 환경에서 정상적으로 동작하던 API도 실제 사용자 환경에서는 예상하지 못한 오류가 발생할 수 있습니다.

예를 들어 다음과 같은 상황입니다.

  • Wi-Fi에서 모바일 데이터로 전환하는 순간 요청이 실패함

  • 서버 응답이 늦어져 화면에 로딩 표시만 계속 나타남

  • 인터넷에 연결되어 있지만 서버에 접근하지 못함

  • HTTP 200 응답을 받았지만 JSON 파싱에 실패함

  • 사용자가 검색 버튼을 연속으로 눌러 같은 요청이 여러 번 발생함

  • API 호출이 실패하자 즉시 재시도하면서 서버에 부하가 발생함

  • 화면을 닫았는데 이전 요청의 결과가 UI에 반영됨

  • 서버 오류와 네트워크 오류를 구분하지 못함

이러한 문제를 줄이려면 단순히 Retrofit으로 API를 호출하는 것에서 끝나서는 안 됩니다.

타임아웃, 오류 분류, 요청 취소, 재시도, 생명주기, 로깅 정책까지 함께 설계해야 합니다.

이번 글에서는 Kotlin + MVVM 기반 Android 프로젝트에서 Retrofit2와 OkHttp를 안정적으로 사용하는 방법을 정리하겠습니다.

작성 기준: 2026년 10월. 라이브러리 버전과 일부 API는 변경될 수 있으므로 실제 적용 시 공식 문서를 확인해야 합니다.


1. Retrofit2와 OkHttp의 역할

Retrofit2와 OkHttp는 함께 사용되는 경우가 많지만 역할은 다릅니다.

Retrofit은 HTTP API를 Kotlin 인터페이스 형태로 정의하고, 요청 및 응답을 객체로 변환하는 작업을 도와줍니다.

OkHttp는 실제 HTTP 연결과 요청 실행을 담당합니다.

Activity / Fragment
        │
        ▼
     ViewModel
        │
        ▼
     Repository
        │
        ▼
      Retrofit
        │
        ├── API 인터페이스
        └── JSON Converter
        │
        ▼
       OkHttp
        │
        ├── Timeout
        ├── Interceptor
        ├── Connection Pool
        └── HTTP Connection
        │
        ▼
      API Server

일반적인 MVVM 프로젝트에서는 Activity나 Fragment가 Retrofit을 직접 호출하기보다 Repository를 통해 데이터를 가져오도록 구성하는 것이 좋습니다.

2. Gradle 의존성 추가

app/build.gradle.kts에 필요한 라이브러리를 추가합니다.

dependencies {
    implementation("com.squareup.retrofit2:retrofit:<version>")
    implementation("com.squareup.retrofit2:converter-gson:<version>")
    implementation("com.squareup.okhttp3:logging-interceptor:<version>")

    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:<version>")
    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:<version>")
}

<version>에는 프로젝트에서 검증한 호환 버전을 지정합니다.

Retrofit, OkHttp, Coroutine의 버전은 서로 독립적으로 관리되므로 무조건 같은 숫자를 사용해서는 안 됩니다.

인터넷 권한도 필요합니다.

<uses-permission
    android:name="android.permission.INTERNET" />

HTTPS 서버를 사용하는 경우 일반적으로 별도의 평문 통신 허용 설정은 필요하지 않습니다.

3. API 응답 데이터 클래스 작성

예를 들어 서버에서 조류 정보를 제공한다고 가정하겠습니다.

응답 JSON은 다음과 같습니다.

{
  "id": 1234,
  "name": "참새",
  "scientificName": "Passer montanus"
}

Kotlin 데이터 클래스를 작성합니다.

data class BirdDto(
    val id: Long,
    val name: String,
    val scientificName: String
)

Gson을 사용하면 JSON을 Kotlin 객체로 변환할 수 있습니다.

다만 서버에서 필드가 누락되거나 null이 전달되는 경우에는 Kotlin의 null 안정성이 자동으로 보장되지 않습니다.

특히 Gson은 Kotlin의 non-null 선언과 기본 생성자 동작을 완전히 검증해 주는 도구가 아닙니다.

따라서 외부 API 응답은 신뢰할 수 없는 입력으로 취급하고 필요한 필드의 유효성을 검사하는 것이 좋습니다.

4. Retrofit API 인터페이스 작성

Retrofit에서는 인터페이스로 API를 정의합니다.

interface BirdApi {

    @GET("api/birds")
    suspend fun getBirds(): List<BirdDto>

    @GET("api/birds/{id}")
    suspend fun getBird(
        @Path("id") id: Long
    ): BirdDto

    @GET("api/birds/search")
    suspend fun searchBirds(
        @Query("keyword") keyword: String
    ): List<BirdDto>
}

suspend 함수를 사용하면 Coroutine에서 자연스럽게 호출할 수 있습니다.

중요한 점은 Retrofit의 suspend HTTP 요청이 일반적인 동기 네트워크 호출처럼 메인 스레드를 차단하지 않도록 구현되어 있다는 것입니다.

따라서 모든 Retrofit suspend 호출을 반드시 Dispatchers.IO로 감싸야 하는 것은 아닙니다.

반면 Repository 내부에서 동기 SQLite 접근이나 파일 작업을 수행한다면 해당 부분은 별도로 Dispatchers.IO에서 실행해야 합니다.

5. OkHttpClient 구성하기

OkHttpClient는 타임아웃과 인터셉터 등을 설정할 수 있습니다.

val okHttpClient = OkHttpClient.Builder()
    .connectTimeout(10, TimeUnit.SECONDS)
    .readTimeout(20, TimeUnit.SECONDS)
    .writeTimeout(20, TimeUnit.SECONDS)
    .callTimeout(30, TimeUnit.SECONDS)
    .build()

각 타임아웃의 의미는 다음과 같습니다.

설정의미
connectTimeout서버 연결을 수립하는 과정의 제한 시간
readTimeout개별 읽기 I/O 작업의 제한 시간
writeTimeout개별 쓰기 I/O 작업의 제한 시간
callTimeout전체 HTTP 호출의 제한 시간

여기서 중요한 차이가 있습니다.

readTimeout은 전체 다운로드 시간의 제한이 아닙니다.

서버가 데이터를 조금씩 계속 전송하는 경우에는 전체 요청이 오래 지속될 수 있습니다.

반면 callTimeout은 연결, 요청 전송, 서버 처리, 응답 수신 등을 포함한 전체 호출에 적용됩니다.

위의 10초, 20초, 30초는 설명을 위한 예시 값입니다.

실제 서비스에서는 API 응답 시간, 파일 크기, 네트워크 환경을 고려해 설정해야 합니다.

6. Retrofit 인스턴스 생성

Retrofit 인스턴스는 일반적으로 재사용하도록 구성합니다.

object ApiClient {

    private val okHttpClient = OkHttpClient.Builder()
        .connectTimeout(10, TimeUnit.SECONDS)
        .readTimeout(20, TimeUnit.SECONDS)
        .callTimeout(30, TimeUnit.SECONDS)
        .build()

    private val retrofit = Retrofit.Builder()
        .baseUrl("https://api.example.com/")
        .client(okHttpClient)
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    val birdApi: BirdApi =
        retrofit.create(BirdApi::class.java)
}

baseUrl()은 일반적으로 마지막에 /가 포함된 URL을 사용해야 합니다.

또한 요청마다 새로운 OkHttpClient를 생성하기보다 하나의 클라이언트를 재사용하는 것이 연결 풀과 스레드 자원 관리 측면에서 유리합니다.

단, 인증 설정이나 서버별 네트워크 정책이 크게 다르다면 별도 클라이언트 구성이 필요할 수 있습니다.

7. HTTP 오류와 네트워크 오류 구분

Retrofit 요청 실패는 크게 다음과 같이 구분할 수 있습니다.

오류 유형예시
HTTP 오류400, 401, 403, 404, 429, 500
네트워크 오류연결 실패, DNS 오류, 타임아웃
데이터 오류JSON 파싱 실패, 필수 필드 누락
취소화면 종료, 검색어 변경, 사용자의 요청 취소
기타 오류예상하지 못한 내부 오류

이 오류들을 모두 같은 메시지로 처리하면 사용자가 문제를 이해하기 어렵습니다.

예를 들어 인터넷 연결 실패와 인증 만료는 해결 방법이 다릅니다.

8. API 결과를 sealed class로 관리

Repository에서 API 호출 결과를 일관되게 전달하려면 sealed class를 사용할 수 있습니다.

sealed interface ApiResult<out T> {

    data class Success<T>(
        val data: T
    ) : ApiResult<T>

    data class HttpError(
        val code: Int,
        val message: String
    ) : ApiResult<Nothing>

    data class NetworkError(
        val cause: IOException
    ) : ApiResult<Nothing>

    data class UnexpectedError(
        val cause: Throwable
    ) : ApiResult<Nothing>
}

이렇게 하면 ViewModel에서 성공, HTTP 오류, 네트워크 오류를 구분할 수 있습니다.

취소는 일반적인 실패 결과로 변환하지 않고 Coroutine의 취소 흐름을 유지하도록 처리하는 것이 좋습니다.

9. Repository에서 안전하게 예외 처리하기

다음은 Retrofit의 suspend API를 호출하고 결과를 분류하는 예시입니다.

class BirdRepository(
    private val api: BirdApi
) {

    suspend fun getBirds(): ApiResult<List<BirdDto>> {

        return try {

            ApiResult.Success(api.getBirds())

        } catch (e: CancellationException) {
            throw e

        } catch (e: HttpException) {
            ApiResult.HttpError(
                code = e.code(),
                message = e.message()
            )

        } catch (e: IOException) {
            ApiResult.NetworkError(e)

        } catch (e: Exception) {
            ApiResult.UnexpectedError(e)
        }
    }
}

여기서 중요한 부분은 다음 코드입니다.

catch (e: CancellationException) {
    throw e
}

Coroutine의 취소를 일반적인 오류로 처리하면 이미 종료된 화면의 작업이 계속 진행되거나 취소 의미가 사라질 수 있습니다.

따라서 CancellationException은 다시 전달하는 것이 좋습니다.

10. ViewModel에서 UI 상태 관리

네트워크 통신 중에는 로딩, 성공, 실패 상태를 구분해야 합니다.

sealed interface BirdUiState {

    data object Loading : BirdUiState

    data class Success(
        val birds: List<BirdDto>
    ) : BirdUiState

    data class Error(
        val message: String
    ) : BirdUiState
}

ViewModel에서는 StateFlow로 상태를 관리할 수 있습니다.

class BirdViewModel(
    private val repository: BirdRepository
) : ViewModel() {

    private val _uiState =
        MutableStateFlow<BirdUiState>(
            BirdUiState.Loading
        )

    val uiState: StateFlow<BirdUiState> =
        _uiState.asStateFlow()

    fun loadBirds() {
        viewModelScope.launch {

            _uiState.value = BirdUiState.Loading

            _uiState.value =
                when (val result = repository.getBirds()) {

                    is ApiResult.Success -> {
                        BirdUiState.Success(result.data)
                    }

                    is ApiResult.HttpError -> {
                        BirdUiState.Error(
                            "서버 응답 오류: ${result.code}"
                        )
                    }

                    is ApiResult.NetworkError -> {
                        BirdUiState.Error(
                            "네트워크 연결을 확인해 주세요."
                        )
                    }

                    is ApiResult.UnexpectedError -> {
                        BirdUiState.Error(
                            "데이터 처리 중 오류가 발생했습니다."
                        )
                    }
                }
        }
    }
}

이 예시는 상태 관리 구조를 설명하기 위한 것입니다.

실제 프로젝트에서는 HTTP 401, 403, 429, 500 등을 별도로 처리하는 것이 좋습니다.

또한 서버가 전달한 오류 메시지를 사용자에게 그대로 노출하지 않도록 주의해야 합니다.

11. HTTP 상태 코드별 처리 전략

HTTP 상태 코드는 오류 대응 방법을 결정하는 데 중요한 정보입니다.

상태 코드의미일반적인 대응
400잘못된 요청요청 파라미터 확인
401인증 필요 또는 실패인증 상태 확인
403접근 권한 없음권한 안내
404리소스 없음데이터 없음 처리
408요청 타임아웃재시도 가능성 검토
429요청 과다Retry-After 및 제한 정책 확인
500서버 내부 오류서버 상태 확인
502·503·504게이트웨이·서비스 오류제한적 재시도 검토

단, 상태 코드만으로 무조건 재시도 여부를 결정하면 안 됩니다.

예를 들어 결제나 데이터 생성 요청은 서버에서 이미 처리되었지만 응답만 유실되었을 수 있습니다.

이 경우 동일 요청을 재시도하면 중복 처리 문제가 발생할 수 있습니다.

12. 네트워크 요청 재시도는 언제 필요한가?

일시적인 네트워크 장애는 재시도로 해결될 수 있습니다.

하지만 모든 오류를 재시도하는 것은 위험합니다.

대표적으로 다음과 같이 구분할 수 있습니다.

재시도를 검토할 수 있는 경우

  • 일시적인 연결 실패

  • 일부 타임아웃

  • HTTP 429

  • 일시적인 HTTP 503

  • 서버가 명시적으로 재시도를 안내한 경우

일반적으로 재시도하지 않는 경우

  • 잘못된 요청 형식으로 인한 HTTP 400

  • 권한 부족으로 인한 HTTP 403

  • 유효하지 않은 데이터

  • 사용자가 취소한 요청

특히 POST 요청은 멱등성(Idempotency)을 고려해야 합니다.

서버에서 중복 요청 방지 키를 지원하지 않는다면 무조건 재시도해서는 안 됩니다.

13. Coroutine으로 재시도 구현

다음은 일시적인 IOException에 대해서만 재시도하는 예시입니다.

suspend fun <T> retryOnIOException(
    maxAttempts: Int = 3,
    initialDelayMillis: Long = 500L,
    block: suspend () -> T
): T {

    require(maxAttempts >= 1)

    var currentDelay = initialDelayMillis

    repeat(maxAttempts - 1) {

        try {
            return block()

        } catch (e: CancellationException) {
            throw e

        } catch (e: IOException) {
            delay(currentDelay)
            currentDelay *= 2
        }
    }

    return block()
}

사용 예시는 다음과 같습니다.

val birds = retryOnIOException {
    api.getBirds()
}

이 예제는 지수 백오프의 기본 구조를 설명합니다.

실제 서비스에서는 최대 대기 시간, 무작위 지연(Jitter), 서버의 Retry-After 응답, 전체 요청 제한 시간 등을 고려하는 것이 좋습니다.

또한 이 함수는 IOException만 재시도하므로 Retrofit의 HTTP 429나 503을 별도로 처리하려면 추가 정책이 필요합니다.

14. OkHttp Interceptor 활용하기

OkHttp의 Interceptor는 HTTP 요청과 응답을 공통으로 처리할 때 유용합니다.

예를 들어 모든 요청에 공통 헤더를 추가할 수 있습니다.

class HeaderInterceptor : Interceptor {

    override fun intercept(
        chain: Interceptor.Chain
    ): Response {

        val request = chain.request()
            .newBuilder()
            .header("Accept", "application/json")
            .build()

        return chain.proceed(request)
    }
}

OkHttpClient에 등록합니다.

val client = OkHttpClient.Builder()
    .addInterceptor(HeaderInterceptor())
    .build()

이렇게 하면 각 API 메서드에서 동일한 헤더를 반복해서 설정할 필요가 없습니다.

다만 인증 토큰은 단순한 고정 문자열과 다릅니다.

토큰 갱신과 동시 요청 처리까지 고려해야 하므로 별도의 인증 관리 구조가 필요합니다.

15. HTTP 로깅 설정하기

개발 중 네트워크 요청과 응답을 확인하려면 HttpLoggingInterceptor를 사용할 수 있습니다.

val loggingInterceptor =
    HttpLoggingInterceptor().apply {

        level = if (BuildConfig.DEBUG) {
            HttpLoggingInterceptor.Level.BASIC
        } else {
            HttpLoggingInterceptor.Level.NONE
        }
    }

클라이언트에 등록합니다.

val client = OkHttpClient.Builder()
    .addInterceptor(loggingInterceptor)
    .build()

BASIC 수준에서는 요청과 응답의 기본 정보를 확인할 수 있습니다.

더 상세한 BODY 로깅도 가능하지만 실제 서비스에서는 매우 주의해야 합니다.

로그에 다음과 같은 정보가 포함될 수 있기 때문입니다.

  • Authorization 헤더

  • 세션 쿠키

  • 개인정보

  • 사용자 입력 데이터

  • 서버에서 전달한 민감한 응답

디버그 빌드에서도 실제 사용자 계정과 개인정보를 다루는 경우에는 로깅 범위를 제한해야 합니다.

16. 중복 API 요청 방지하기

사용자가 검색 버튼을 연속으로 누르면 동일한 요청이 여러 번 실행될 수 있습니다.

예를 들어 다음과 같은 구조입니다.

fun search(keyword: String) {
    viewModelScope.launch {
        repository.searchBirds(keyword)
    }
}

이 코드는 호출할 때마다 새로운 Coroutine을 시작합니다.

검색 요청이 빠르게 반복되면 이전 요청과 새로운 요청이 동시에 실행될 수 있습니다.

Job을 이용한 이전 요청 취소

private var searchJob: Job? = null

fun search(keyword: String) {

    searchJob?.cancel()

    searchJob = viewModelScope.launch {

        val result = repository.searchBirds(keyword)

        _searchResult.value = result
    }
}

이렇게 하면 새로운 검색이 시작될 때 이전 검색 작업을 취소할 수 있습니다.

다만 요청 취소가 서버에서 이미 수행한 작업까지 되돌리는 것은 아닙니다.

따라서 데이터 변경 API는 서버 측에서도 중복 요청 방지 정책을 고려해야 합니다.

17. 검색어 입력에는 Debounce 적용하기

사용자가 검색창에 문자를 입력할 때마다 API를 호출하면 요청 수가 급격히 증가할 수 있습니다.

예를 들어 사용자가 다음과 같이 입력한다고 가정하겠습니다.

참
참새
참새목

각 입력마다 요청을 보내는 대신 일정 시간 동안 추가 입력이 없을 때 검색하도록 구현할 수 있습니다.

StateFlow와 debounce()를 사용하는 방법입니다.

private val keywordFlow = MutableStateFlow("")

fun updateKeyword(keyword: String) {
    keywordFlow.value = keyword
}

ViewModel에서 검색 흐름을 수집합니다.

init {
    viewModelScope.launch {

        keywordFlow
            .debounce(300)
            .map { it.trim() }
            .distinctUntilChanged()
            .collectLatest { keyword ->

                if (keyword.isBlank()) {
                    _searchResult.value = emptyList()
                    return@collectLatest
                }

                _searchResult.value =
                    repository.searchBirds(keyword)
            }
    }
}

이 구조에서는 입력이 변경되면 이전 collectLatest 작업이 취소됩니다.

다만 Repository가 예외를 적절히 처리하도록 설계해야 합니다.

또한 실제 앱에서는 검색 로딩 상태와 오류 상태도 함께 관리하는 것이 좋습니다.

18. 인터넷 연결 상태 확인

Android에서는 ConnectivityManager를 통해 네트워크 연결 상태를 확인할 수 있습니다.

다만 네트워크에 연결되어 있다는 사실이 특정 서버에 정상적으로 접근할 수 있음을 의미하지는 않습니다.

예를 들어 Wi-Fi에 연결되어 있어도 인터넷이 차단되어 있을 수 있습니다.

따라서 네트워크 상태 확인은 사용자에게 안내하기 위한 보조 정보로 활용하고, 실제 API 요청의 오류 처리를 반드시 구현해야 합니다.

또한 네트워크 상태를 확인하는 코드가 있다고 해서 요청 직전에 연결이 유지된다는 보장도 없습니다.

네트워크 상태는 언제든 변경될 수 있습니다.

19. 오프라인 상태에서는 어떻게 처리할까?

네트워크가 불안정한 앱에서는 오프라인 사용을 고려할 수 있습니다.

예를 들어 조류 도감 앱이라면 다음과 같은 구조를 사용할 수 있습니다.

Activity / Fragment
        │
        ▼
     ViewModel
        │
        ▼
     Repository
        │
        ├── SQLite
        │      └── 저장된 데이터 표시
        │
        └── Retrofit
               └── 최신 데이터 다운로드

이 구조에서는 네트워크 연결이 없어도 기존 SQLite 데이터를 표시할 수 있습니다.

서버에서 최신 데이터를 가져오는 작업은 별도로 수행합니다.

데이터 동기화에 실패하더라도 기존 데이터를 삭제하지 않도록 설계하는 것이 중요합니다.

특히 서버 응답이 비어 있다는 이유만으로 로컬 데이터 전체를 삭제하면 일시적인 서버 문제로 사용자 경험이 크게 나빠질 수 있습니다.

20. WorkManager와 네트워크 재시도 연동

앱 화면에서 즉시 필요한 API 요청은 Coroutine으로 처리하는 것이 적절합니다.

반면 앱이 종료된 뒤에도 완료해야 하는 데이터 동기화는 WorkManager를 고려할 수 있습니다.

예를 들어 사용자가 작성한 메모를 서버에 업로드하는 작업입니다.

사용자 메모 저장
       │
       ▼
SQLite에 먼저 저장
       │
       ▼
WorkManager 동기화 예약
       │
       ├── 네트워크 연결됨
       │       └── 서버 업로드
       │
       └── 네트워크 없음
               └── 조건 충족 시 실행

이 구조에서는 사용자가 앱을 닫더라도 나중에 동기화할 수 있도록 설계할 수 있습니다.

단, WorkManager 역시 정확한 실행 시점을 보장하지는 않습니다.

또한 사용자 메모와 같은 데이터는 중복 업로드에 안전하도록 서버 API를 설계해야 합니다.

21. 네트워크 통신 테스트 시나리오

실제 프로젝트에서는 다음 상황을 테스트하는 것이 좋습니다.

  1. 정상적인 Wi-Fi 환경에서 API 호출

  2. 모바일 데이터 환경에서 API 호출

  3. 요청 중 네트워크 연결 해제

  4. 요청 중 Wi-Fi와 모바일 데이터 전환

  5. 서버 응답 지연

  6. HTTP 401, 403, 404, 429, 500 응답

  7. 잘못된 JSON 응답

  8. 필수 필드가 누락된 JSON 응답

  9. 검색어 연속 입력

  10. 화면 종료 중 네트워크 요청 취소

  11. 동일한 POST 요청 중복 실행

  12. 앱 백그라운드 이동 후 복귀

특히 네트워크 오류는 실제 환경에서 재현하기 어려울 수 있으므로 MockWebServer와 같은 테스트 도구를 활용하면 좋습니다.

22. MockWebServer로 API 테스트하기

OkHttp의 MockWebServer를 사용하면 실제 서버 없이 HTTP 응답을 테스트할 수 있습니다.

테스트 의존성을 추가합니다.

testImplementation(
    "com.squareup.okhttp3:mockwebserver:<version>"
)

예를 들어 서버 오류를 반환하도록 설정할 수 있습니다.

val server = MockWebServer()

server.enqueue(
    MockResponse()
        .setResponseCode(500)
        .setBody("""{"message":"server error"}""")
)

server.start()

테스트용 Retrofit 인스턴스는 MockWebServer의 URL을 사용합니다.

val retrofit = Retrofit.Builder()
    .baseUrl(server.url("/"))
    .addConverterFactory(GsonConverterFactory.create())
    .build()

테스트가 끝나면 서버를 종료해야 합니다.

server.shutdown()

이렇게 하면 실제 운영 서버에 오류를 발생시키지 않고도 HTTP 오류 처리 로직을 검증할 수 있습니다.

23. 네트워크 통신 안정화 체크리스트

  • OkHttpClient 재사용

  • connectTimeout, readTimeout, callTimeout 구분

  • HTTP 오류와 네트워크 오류 분리

  • CancellationException 재전파

  • Retrofit suspend API 사용

  • 동기 I/O 작업은 Dispatchers.IO에서 처리

  • 재시도 가능한 오류만 재시도

  • POST 요청의 중복 처리 방지

  • 검색 요청에 Debounce 또는 이전 Job 취소 적용

  • 인증 토큰과 개인정보 로깅 방지

  • JSON 응답 유효성 검사

  • 오프라인 데이터 표시 정책 검토

  • 지속성 있는 동기화는 WorkManager 활용

  • MockWebServer로 오류 상황 테스트

  • 앱 종료와 화면 전환 중 요청 취소 확인

24. 마무리

Android 앱의 네트워크 통신은 단순히 API 호출에 성공하는 것만으로 완성되지 않습니다.

실제 사용자 환경에서는 네트워크가 끊기거나 서버가 느려지고, 응답 데이터가 예상과 다르게 전달되는 상황이 발생할 수 있습니다.

따라서 다음 원칙을 적용하는 것이 좋습니다.

  1. Retrofit과 OkHttp의 역할을 분리해 이해합니다.

  2. 타임아웃을 설정하고 HTTP 오류와 네트워크 오류를 구분합니다.

  3. Coroutine 취소를 정상적인 제어 흐름으로 처리합니다.

  4. 재시도는 오류 유형과 요청의 멱등성을 고려해 적용합니다.

  5. 검색 요청은 중복 실행을 줄이고 최신 결과만 반영합니다.

  6. 오프라인 상황에서도 가능한 기능을 유지하도록 설계합니다.

  7. MockWebServer로 다양한 오류 상황을 재현하고 검증합니다.

특히 SQLite를 사용하는 Android 앱에서는 서버 데이터를 무조건 화면에 직접 표시하기보다 Repository를 중심으로 로컬 데이터와 원격 데이터를 관리하는 구조가 유용합니다.

안정적인 네트워크 통신의 핵심은 오류가 발생하지 않게 만드는 것이 아니라, 오류가 발생하더라도 앱이 예측 가능한 방식으로 동작하도록 설계하는 것입니다.


참고 자료

태그: Android, Kotlin, Retrofit2, OkHttp, Coroutine, MVVM, API 통신, Timeout, Interceptor, HTTP 오류, 네트워크 재시도, StateFlow, SQLite, WorkManager, MockWebServer