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. 네트워크 통신 테스트 시나리오
실제 프로젝트에서는 다음 상황을 테스트하는 것이 좋습니다.
정상적인 Wi-Fi 환경에서 API 호출
모바일 데이터 환경에서 API 호출
요청 중 네트워크 연결 해제
요청 중 Wi-Fi와 모바일 데이터 전환
서버 응답 지연
HTTP 401, 403, 404, 429, 500 응답
잘못된 JSON 응답
필수 필드가 누락된 JSON 응답
검색어 연속 입력
화면 종료 중 네트워크 요청 취소
동일한 POST 요청 중복 실행
앱 백그라운드 이동 후 복귀
특히 네트워크 오류는 실제 환경에서 재현하기 어려울 수 있으므로 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 호출에 성공하는 것만으로 완성되지 않습니다.
실제 사용자 환경에서는 네트워크가 끊기거나 서버가 느려지고, 응답 데이터가 예상과 다르게 전달되는 상황이 발생할 수 있습니다.
따라서 다음 원칙을 적용하는 것이 좋습니다.
Retrofit과 OkHttp의 역할을 분리해 이해합니다.
타임아웃을 설정하고 HTTP 오류와 네트워크 오류를 구분합니다.
Coroutine 취소를 정상적인 제어 흐름으로 처리합니다.
재시도는 오류 유형과 요청의 멱등성을 고려해 적용합니다.
검색 요청은 중복 실행을 줄이고 최신 결과만 반영합니다.
오프라인 상황에서도 가능한 기능을 유지하도록 설계합니다.
MockWebServer로 다양한 오류 상황을 재현하고 검증합니다.
특히 SQLite를 사용하는 Android 앱에서는 서버 데이터를 무조건 화면에 직접 표시하기보다 Repository를 중심으로 로컬 데이터와 원격 데이터를 관리하는 구조가 유용합니다.
안정적인 네트워크 통신의 핵심은 오류가 발생하지 않게 만드는 것이 아니라, 오류가 발생하더라도 앱이 예측 가능한 방식으로 동작하도록 설계하는 것입니다.
참고 자료
Retrofit 공식 저장소
https://github.com/square/retrofitOkHttp 공식 문서
https://square.github.io/okhttp/OkHttp Interceptors
https://square.github.io/okhttp/features/interceptors/Android Developers — Kotlin Coroutines
https://developer.android.com/kotlin/coroutinesAndroid Developers — StateFlow 및 SharedFlow
https://developer.android.com/kotlin/flow/stateflow-and-sharedflowAndroid Developers — 네트워크 연결 상태 모니터링
https://developer.android.com/develop/connectivity/network-ops/reading-network-stateAndroid Developers — WorkManager
https://developer.android.com/develop/background-work/background-tasks/persistentMockWebServer 공식 저장소
https://github.com/square/okhttp/tree/master/mockwebserver
태그: Android, Kotlin, Retrofit2, OkHttp, Coroutine, MVVM, API 통신, Timeout, Interceptor, HTTP 오류, 네트워크 재시도, StateFlow, SQLite, WorkManager, MockWebServer