느린 연결을 위한 Kotlin 네트워킹: 타임아웃과 안전한 재시도
느린 연결을 위한 실용적인 Kotlin 네트워킹: 타임아웃 설정, 안전한 캐시, 중복 없는 재시도, 불안정한 모바일 네트워크에서 중요한 동작 보호 방법.

느리고 불안정한 연결에서 무엇이 깨지나
모바일에서 “느림”은 보통 ‘인터넷 없음’이 아닙니다. 짧게만 연결되는 경우가 많습니다. 요청이 8~20초 걸리다가 중간에 멈춘 뒤 완료되기도 하고, 한 순간 성공했다가 다음 순간 실패하기도 합니다. 와이파이에서 LTE로 전환되거나, 신호가 약한 지역에 들어가거나, OS가 앱을 백그라운드로 보냈을 때 이런 일이 발생합니다.
“불안정함”은 더 심합니다. 패킷이 유실되고, DNS 조회가 타임아웃되고, TLS 핸드셰이크가 실패하고, 연결이 임의로 리셋됩니다. 코드상으로 모든 것을 ‘올바르게’ 해도 현장에서는 네트워크가 계속 변하기 때문에 실패를 보게 됩니다.
기본 설정이 여기서 흔히 깨집니다. 많은 앱이 타임아웃, 재시도, 캐싱을 라이브러리 기본값에 맡기고 “충분히 좋은” 기준을 정하지 않습니다. 기본값은 안정적인 와이파이와 빠른 API에 맞춰져 있는 경우가 많지, 통근열차, 엘리베이터, 붐비는 카페 상황에는 맞지 않습니다.
사용자는 “소켓 타임아웃”이나 “HTTP 503”을 말하지 않습니다. 그들은 끝없어 보이는 로더, 긴 대기 후 갑작스러운 오류(다음 시도에서 정상 동작), 중복 동작(두 번 예약, 두 번 주문, 이중 청구), 업데이트 손실, UI에는 ‘실패’로 보이지만 서버에는 성공한 상태 같은 증상들을 봅니다.
느린 네트워크는 작은 설계 격차를 돈과 신뢰 문제로 키웁니다. 앱이 “전송 중”, “실패”, “완료”를 명확히 구분하지 않으면 사용자는 다시 탭합니다. 클라이언트가 맹목적으로 재시도하면 중복을 만들 수 있습니다. 서버가 멱등성을 지원하지 않으면 한 번의 불안정한 연결이 여러 번의 ‘성공한’ 쓰기를 만들 수 있습니다.
“중요한 작업”은 한 번만 일어나야 하고 정확해야 하는 모든 것입니다: 결제, 체크아웃 제출, 예약, 포인트 이체, 비밀번호 변경, 배송지 저장, 클레임 제출, 승인 전송 등.
현실적인 예: 약한 LTE에서 누군가 체크아웃을 제출합니다. 앱이 요청을 보냈는데 응답이 도착하기 전에 연결이 끊깁니다. 사용자는 오류를 보고 다시 ‘결제’ 버튼을 탭하고 이제 서버에는 두 개의 요청이 도달합니다. 명확한 규칙이 없으면 앱은 재시도할지, 기다릴지, 중단할지 판단할 수 없습니다. 사용자는 재시도해야 할지 알 수 없습니다.
코드를 조정하기 전에 규칙을 정하세요
연결이 느리거나 불안정할 때 대부분의 버그는 HTTP 클라이언트가 아니라 규칙이 불명확해서 생깁니다. 타임아웃, 캐시, 재시도를 건드리기 전에 “정상”이 무엇인지 적어두세요.
먼저 절대 두 번 실행되면 안 되는 작업을 정하세요. 보통 금전과 계정 관련 동작입니다: 주문하기, 카드 결제, 출금 요청, 비밀번호 변경, 계정 삭제 등입니다. 사용자가 두 번 탭하거나 앱이 재시도해도 서버는 한 번의 요청으로 처리해야 합니다. 지금 당장 그 보장을 할 수 없다면 해당 엔드포인트는 “자동 재시도 금지”로 처리하세요.
다음으로 네트워크가 불안할 때 각 화면이 무엇을 허용할지 결정하세요. 어떤 화면은 오프라인에서도 유용할 수 있습니다(마지막으로 본 프로필, 이전 주문). 다른 화면은 읽기 전용이 되거나 명확한 “다시 시도” 상태를 보여야 합니다(재고 수, 실시간 가격). 이러한 기대를 섞으면 혼란스러운 UI와 위험한 캐싱이 생깁니다.
액션별 허용 대기 시간은 코드에서 편해 보이는 값이 아니라 사용자가 느끼는 기준으로 정하세요. 로그인은 짧은 대기를 견딜 수 있고, 파일 업로드는 더 오래 필요합니다. 체크아웃은 빠르게 느껴지면서도 안전해야 합니다. 30초 타임아웃은 문서상 ‘신뢰할 수 있음’일지 몰라도 체감상 망가진 것으로 느껴질 수 있습니다.
마지막으로 기기에 무엇을 얼마나 오래 저장할지 결정하세요. 캐시된 데이터는 도움이 되지만 오래된 데이터는 잘못된 선택(옛 가격, 만료된 자격)을 유발할 수 있습니다.
규칙을 모두가 볼 수 있는 곳(README면 충분)에 적어두세요. 간단하게 유지하세요:
- 어떤 엔드포인트가 “중복 금지”이며 멱등성 처리가 필요한가?
- 어떤 화면이 오프라인에서 작동해야 하고, 어떤 화면은 오프라인에서 읽기 전용인가?
- 액션별 최대 허용 대기 시간은 얼마인가(로그인, 피드 새로고침, 업로드, 체크아웃)?
- 기기에 무엇을 캐시할 수 있으며 만료 시간은 얼마인가?
- 실패 후 오류를 보여줄 것인가, 나중에 큐에 넣을 것인가, 수동 재시도를 요구할 것인가?
이 규칙이 명확하면 타임아웃 값, 캐싱 헤더, 재시도 정책, UI 상태를 구현하고 테스트하기가 훨씬 쉬워집니다.
사용자 기대에 맞는 타임아웃
느린 네트워크는 여러 방식으로 실패합니다. 좋은 타임아웃 설정은 단순히 숫자를 골라 넣는 것이 아니라 사용자가 무엇을 하려는지에 맞춰져야 하며, 앱이 회복할 수 있도록 충분히 빨리 실패하도록 해야 합니다.
세 가지 타임아웃을 쉽게 정리하면:
- Connect timeout: 서버 연결을 설정하는 데 기다릴 시간(DNS 조회, TCP, TLS). 실패하면 요청은 사실상 시작되지 않았습니다.
- Write timeout: 요청 본문을 전송하는 데 걸리는 시간(업로드, 큰 JSON, 느린 업로드 링크).
- Read timeout: 요청을 보낸 뒤 서버가 데이터를 보내기를 기다리는 시간. 불안정한 모바일 네트워크에서 자주 문제가 됩니다.
타임아웃은 화면과 위험도에 맞춰야 합니다. 피드는 느려도 큰 문제가 아닐 수 있고, 중요한 작업은 끝나거나 명확히 실패해서 사용자가 다음 행동을 결정할 수 있게 해야 합니다.
실무에서 시작점(측정 후 조정):
- 목록 로딩(저위험): connect 5–10s, read 20–30s, write 10–15s.
- 입력 중 검색: connect 3–5s, read 5–10s, write 5–10s.
- 중요 작업(결제, 주문 제출 등): connect 5–10s, read 30–60s, write 15–30s.
일관성이 완벽함보다 중요합니다. 사용자가 “제출” 후 2분 동안 스피너를 보면 다시 탭할 것입니다.
UI에서도 무한 로딩이 생기지 않게 상한을 두세요. 즉시 진행 표시를 보여주고 취소를 허용하며, 예를 들어 20–30초 후에는 “아직 시도 중입니다…”와 함께 재시도나 연결 확인 옵션을 제공하세요. 네트워크 라이브러리가 여전히 대기 중이더라도 경험을 정직하게 유지할 수 있습니다.
타임아웃이 발생하면 디버그를 위해 충분한 로그를 남기되 비밀정보는 기록하지 마세요. 유용한 항목: URL 경로(전체 쿼리 제외), HTTP 메서드, 상태(있는 경우), 타이밍 분해(connect·write·read), 네트워크 타입(Wi‑Fi, 셀룰러, 비행기 모드), 대략적인 요청/응답 크기, 그리고 요청 ID(클라이언트 로그와 서버 로그를 매칭할 수 있게).
간단하고 일관된 Kotlin 네트워킹 설정
연결이 느리면 클라이언트 설정의 작은 불일치가 큰 문제로 커집니다. 깔끔한 기준을 정하면 디버깅이 빨라지고 모든 요청에 동일한 규칙이 적용됩니다.
하나의 클라이언트, 하나의 정책
보통 OkHttpClient 하나를 Retrofit과 함께 빌드하는 한 곳을 만들고 그곳에 기본값을 둡니다. 기본 헤더(앱 버전, 로케일, 인증 토큰)와 명확한 User-Agent, 타임아웃을 한곳에서 설정하고(호출마다 흩어지지 않도록), 디버깅 가능하도록 로깅을 켜며 재시도 정책을 한 곳에서 결정하세요(설령 “자동 재시도 없음”이라 해도).
설정을 한 파일에 모아두는 작은 예시:
val okHttp = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.callTimeout(30, TimeUnit.SECONDS)
.addInterceptor { chain ->
val request = chain.request().newBuilder()
.header("User-Agent", "MyApp/${BuildConfig.VERSION_NAME}")
.header("Accept", "application/json")
.build()
chain.proceed(request)
}
.build()
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttp)
.addConverterFactory(MoshiConverterFactory.create())
.build()
사용자 메시지로 매핑되는 중앙 에러 처리
네트워크 오류는 단순히 “예외”가 아닙니다. 각 화면이 다르게 처리하면 사용자에게 무작위 메시지가 보입니다.
하나의 매퍼를 만들어 실패를 소수의 사용자 친화적 결과로 변환하세요: 연결 없음/비행기 모드, 타임아웃, 서버 오류(5xx), 검증 또는 인증 오류(4xx), 그리고 알 수 없는 경우.
이렇게 하면 UI 문구(“연결이 없습니다” vs “다시 시도하세요”)가 일관되고 기술적 세부사항을 노출하지 않습니다.
화면이 닫힐 때 요청 태그하고 취소하기
불안정한 네트워크에서는 호출이 늦게 끝나 화면이 이미 사라진 뒤에 UI를 업데이트할 수 있습니다. 화면이 닫히면 그에 해당하는 작업을 취소하는 규칙을 표준으로 만드세요.
Retrofit과 Kotlin 코루틴을 사용하면 ViewModel의 코루틴 스코프를 취소하면 해당 HTTP 호출도 취소됩니다. 코루틴이 아닌 경우 Call 레퍼런스를 유지하고 cancel()을 호출하세요. 기능을 종료할 때 관련된 호출 그룹을 태그하고 한 번에 취소할 수도 있습니다.
백그라운드 작업은 UI에 의존하지 않게
완료되어야 하는 중요한 작업(리포트 전송, 큐 동기화, 제출 완료 등)은 UI와 분리된 스케줄러에서 실행하세요. Android에서는 WorkManager가 일반적인 선택입니다. 재시도와 앱 재시작 후에도 작업을 이어갈 수 있기 때문입니다. UI 액션은 가볍게 유지하고 더 긴 작업은 필요하면 백그라운드로 넘기세요.
모바일에서 안전한 캐싱 규칙
캐시는 느린 연결에서 반복 다운로드를 줄이고 화면을 즉각적으로 보이게 하는 큰 이점이 있습니다. 그러나 오래된 데이터가 잘못된 선택을 초래할 수 있습니다(구형 가격, 만료된 자격 등).
안전한 접근은 사용자가 조금 오래된 것을 허용할 수 있는 항목만 캐시하고, 금전·보안·최종 결정에 영향을 주는 항목은 반드시 최신 체크를 하게 하는 것입니다.
신뢰할 수 있는 Cache-Control 기초
대부분 규칙은 몇 가지 헤더로 정리됩니다:
max-age=60: 60초 동안 캐시된 응답을 서버에 묻지 않고 재사용해도 됨.no-store: 이 응답은 저장하지 마라(토큰이나 민감한 화면에 적합).must-revalidate: 만료된 경우 서버에 확인해야 함.
모바일에서는 must-revalidate가 일시적인 오프라인 이후에 ‘조용히 잘못된’ 데이터를 방지합니다. 지하철에서 앱을 열 때 빠른 화면을 원하지만 동시에 데이터가 여전히 유효한지 확인하고 싶을 것입니다.
ETag 갱신: 빠르고 비용이 적으며 신뢰 가능함
읽기 엔드포인트에는 ETag 기반 검증이 긴 max-age보다 낫습니다. 서버가 응답과 함께 ETag를 보내면 다음 요청 시 애플리케이션은 If-None-Match를 보내고, 변경이 없으면 서버는 304 Not Modified로 돌아옵니다. 이는 약한 네트워크에서 작고 빠릅니다.
제품 목록, 프로필 세부, 설정 화면 등에 잘 맞습니다.
간단한 경험칙:
- 읽기 엔드포인트는 짧은
max-age와must-revalidate를 쓰고 가능하면ETag를 지원하세요. - 쓰기 엔드포인트(POST/PUT/PATCH/DELETE)는 캐시하지 마세요. 항상 네트워크 기반으로 처리하세요.
- 민감한 응답(인증 응답, 결제 단계 등)은
no-store사용. - 정적 자산(아이콘, 공개 설정)은 더 길게 캐시해도 괜찮습니다.
앱 전반에 걸쳐 캐싱 결정은 일관되게 하세요. 사용자들은 작은 지연보다 화면별 불일치를 더 잘 느낍니다.
상황을 악화시키지 않는 안전한 재시도
재시도는 쉬운 해결책처럼 보이지만 오히려 역효과를 낼 수 있습니다. 잘못된 요청을 재시도하면 불필요한 부하가 생기고 배터리가 소모되며 앱이 멈춘 것처럼 느껴질 수 있습니다.
일시적인 실패만 재시도하세요. 연결 끊김, 읽기 타임아웃, 짧은 서버 중단은 다음 시도에 성공할 수 있습니다. 잘못된 비밀번호, 누락 필드, 404는 그렇지 않습니다.
실무 규칙:
- 타임아웃과 연결 실패는 재시도하세요.
- 502, 503, 때때로 504는 재시도 대상입니다.
- 4xx(단, 408이나 429는 별도의 대기 규칙이 있으면 예외)를 재시도하지 마세요.
- 이미 서버에 도달해 처리 중일 수 있는 요청은 재시도하지 마세요.
- 재시도는 적게(보통 1~3회).
백오프 + 지터: 재시도 폭주를 줄이기
많은 사용자가 같은 문제를 만나면 즉시 재시도는 복구를 늦추는 트래픽 파동을 만듭니다. 지수적 백오프(시도할수록 대기 시간 증가)와 소량의 지터(랜덤 딜레이)를 적용해 장치들이 동시에 재시도하지 않게 하세요.
예시: 약 0.5초 대기, 다음 1초, 다음 2초에 ±20% 랜덤을 추가합니다.
전체 재시도 시간에 상한을 두세요
제한이 없으면 재시도가 몇 분간 사용자를 스피너에 가둘 수 있습니다. 전체 작업(모든 대기 포함)에 대한 최대 시간을 정하세요. 많은 앱은 10~20초 안에 멈추고 명확한 재시도 옵션을 보여줍니다.
또한 컨텍스트에 맞춰 조절하세요. 사용자가 폼을 제출하면 빠른 응답을 원하고, 백그라운드 동기화 실패는 나중에 재시도해도 됩니다.
멱등성 키나 서버 측 중복 검사 같은 보호가 없다면 비멱등성 작업(주문 생성, 결제 전송 등)을 자동으로 재시도하지 마세요. 안전을 보장할 수 없다면 명확히 실패시키고 사용자가 다음 행동을 결정하게 하세요.
중요한 작업의 중복 방지
느린 연결에서는 사용자가 두 번 탭합니다. OS가 백그라운드에서 재시도할 수도 있고, 앱이 타임아웃 후 재전송할 수도 있습니다. “무언가 생성”하는 작업(주문, 송금, 비가역 삭제 등)은 중복이 큰 피해를 줍니다.
멱등성은 같은 요청이 반복되어도 동일한 결과를 내도록 하는 것입니다. 요청이 반복되면 서버는 첫 번째 결과를 다시 반환하거나 “이미 처리됨”이라고 알려야 합니다.
중요한 시도마다 멱등성 키 사용
중요 작업에는 사용자가 시도를 시작할 때 고유 멱등성 키를 생성하고 요청과 함께 전송하세요(보통 Idempotency-Key 헤더나 본문 필드로).
실무 흐름:
- 사용자가 “결제”를 누르면 UUID 멱등성 키를 생성합니다.
- 이를 로컬에 작은 레코드로 저장: status = pending, createdAt, 요청 페이로드 해시.
- 키와 함께 요청을 전송합니다.
- 성공 응답을 받으면 status = done으로 표시하고 서버의 결과 ID를 저장합니다.
- 재시도해야 하면 같은 키를 재사용하세요(새 키를 만들지 마세요).
같은 키를 재사용하는 규칙이 우발적 이중 청구를 막습니다.
앱 재시작과 오프라인 간극 처리
앱이 요청 도중 강제 종료되면 다음 실행에서도 안전해야 합니다. 멱등성 키와 요청 상태를 로컬 저장소(작은 DB 행 등)에 보관하세요. 재시작 시 같은 키로 재시도하거나 저장된 키/서버 결과 ID를 사용해 “상태 확인” 엔드포인트를 호출하세요.
서버는 중복 키를 받으면 두 번째 시도를 거부하거나 원래 응답(같은 주문 ID, 같은 영수증)을 반환해야 합니다. 서버가 이를 지원하지 못하면 클라이언트 측 중복 방지는 결코 완전하지 않습니다. 클라이언트는 요청을 보낸 뒤 서버에서 무슨 일이 일어났는지 볼 수 없기 때문입니다.
사용자에게 친절한 터치: 시도가 대기 중이면 “결제 진행 중”을 표시하고 버튼을 비활성화하여 최종 결과를 받을 때까지 다시 누를 수 없게 하세요.
중복 재시도를 줄이는 UI 패턴
느린 연결은 단순히 요청을 깨뜨리지 않습니다. 사람들의 탭 행동을 바꿉니다. 화면이 2초간 멈추면 많은 사용자가 아무 일도 일어나지 않았다고 생각하고 버튼을 다시 누릅니다. 네트워크가 불안한 상황에서도 “한 번의 탭”이 신뢰할 수 있게 느껴지도록 UI를 설계해야 합니다.
낮은 위험이고 되돌릴 수 있는 작업(아이템 찜, 드래프트 저장, 읽음 표시 등)은 낙관적 UI가 안전합니다. 금전 관련, 재고, 비가역 삭제 같은 경우는 확정된 UI가 낫습니다.
중요 작업의 기본값으로는 명확한 대기 상태가 좋습니다. 첫 탭 후 주요 버튼을 즉시 “제출 중…” 상태로 바꾸고 비활성화하며 무슨 일이 일어나고 있는지 간단히 설명하세요.
불안정한 네트워크에서 잘 작동하는 패턴:
- 첫 탭 후 주요 액션을 비활성화하고 최종 결과가 나올 때까지 비활성화 상태를 유지.
- 금액, 수신자, 수량 같은 세부를 보여주는 눈에 띄는 “대기 중” 상태 표시.
- 사용자가 이미 보낸 것을 확인할 수 있도록 “최근 활동” 뷰 제공.
- 앱이 백그라운드로 갔다 와도 대기 상태 유지.
- 같은 화면에 여러 탭 대상이 있는 것보다 명확한 하나의 주요 버튼을 선호.
때로 요청은 성공했지만 응답이 손실될 수 있습니다. 이 경우를 반복 탭을 유도하는 오류로 보지 말고 “아직 확실하지 않습니다”라는 불확실성 상태로 처리하고 “상태 확인” 같은 안전한 다음 단계를 제시하세요. 상태 확인이 불가능하면 로컬에 대기 레코드를 유지하고 연결이 복구되면 업데이트하겠다고 알려주세요.
“다시 시도”는 명시적이고 안전하게 만드세요. 같은 클라이언트 측 요청 ID나 멱등성 키로 요청을 반복할 수 있을 때만 보여 주세요.
현실적인 예: 불안정한 체크아웃 제출
고객이 신호가 약한 기차에 있습니다. 장바구니에 담고 결제 버튼을 누릅니다. 앱은 인내심을 가져야 하지만 동시에 두 개의 주문이 생기지 않도록 해야 합니다.
안전한 순서는 다음과 같습니다:
- 앱이 클라이언트 측 시도 ID를 만들고 멱등성 키와 함께 체크아웃 요청을 보냅니다(예: 장바구니에 저장된 UUID).
- 요청은 명확한 연결 타임아웃을 기다리고, 그다음 더 긴 읽기 타임아웃을 기다립니다. 기차가 터널로 들어가며 호출이 타임아웃됩니다.
- 앱은 서버 응답을 전혀 받지 않은 경우에만 짧은 지연 후 한 번 재시도합니다.
- 서버는 두 번째 요청을 받았을 때 같은 멱등성 키를 보고 원래 결과를 반환하거나 중복 생성을 막습니다.
- 앱은 재시도에서 온 응답이든 원래 응답이든 성공 응답을 받으면 최종 확인 화면을 보여줍니다.
캐싱은 엄격한 규칙을 따릅니다. 제품 목록, 배송 옵션, 세금 테이블은 짧게 캐시할 수 있습니다(GET). 체크아웃 제출(POST)은 절대 캐시하지 않습니다. HTTP 캐시를 사용하더라도 브라우징을 돕는 읽기 전용으로만 생각하세요.
중복 방지는 네트워크와 UI 선택의 혼합입니다. 사용자가 결제를 누르면 버튼을 비활성화하고 “주문 제출 중...”을 보여주며 취소는 하나만 허용하세요. 네트워크가 끊기면 “계속 시도 중”으로 바꾸고 동일한 시도 ID를 유지합니다. 사용자가 강제 종료 후 다시 열면 앱은 그 ID로 주문 상태를 확인해 재결제 요구를 하지 않게 합니다.
빠른 체크리스트와 다음 단계
사무실 와이파이에서는 “대체로 괜찮은” 앱이 기차, 엘리베이터, 농촌 지역에서는 무너진다면 배포 전 게이트로 삼으세요. 이 작업은 영리한 코드보다는 반복 가능한 명확한 규칙을 만드는 것입니다.
출시 전 체크리스트:
- 엔드포인트 유형별로 타임아웃을 설정하고 지연·고지연 네트워크에서 테스트하세요(로그인, 피드, 업로드, 체크아웃).
- 안전한 곳에서만 재시도하고 백오프를 적용해 제한하세요(읽기에는 몇 번, 쓰기에는 보통 없음).
- 모든 중요한 쓰기 요청(결제, 주문, 폼 제출)에 멱등성 키를 추가해 재시도나 이중 탭이 중복을 만들지 않게 하세요.
- 캐싱 규칙을 명확히 하세요: 무엇을 오래된 상태로 제공해도 되는지, 무엇은 항상 최신이어야 하는지, 무엇은 절대 캐시하지 않아야 하는지.
- 상태를 가시화하세요: 대기, 실패, 완료는 다르게 보여야 하고 앱 재시작 후에도 완료된 작업을 기억해야 합니다.
이 항목들 중 하나라도 “나중에 결정하겠다”라면 화면마다 무작위 동작이 발생할 것입니다.
지속시키기 위한 다음 단계
한 페이지 분량의 네트워킹 정책을 작성하세요: 엔드포인트 분류, 타임아웃 목표, 재시도 규칙, 캐싱 기대치. 이를 한 곳에 적용(interceptors, 공유 클라이언트 팩토리, 작은 래퍼)해서 모든 팀원이 기본 동작을 동일하게 받게 하세요.
그다음 간단한 중복 드릴을 해보세요. 체크아웃 같은 중요한 동작 하나를 골라 스피너를 멈춘 것처럼 시뮬레이션하고 앱을 강제 종료, 비행기 모드 토글, 버튼 다시 누르기를 실행해 보세요. 안전하다고 증명할 수 없다면 결국 사용자가 문제를 찾아낼 것입니다.
앱과 백엔드에 같은 규칙을 손으로 일일이 연결하지 않고 구현하고 싶다면 AppMaster (appmaster.io)가 생산 준비된 백엔드와 네이티브 모바일 소스 코드를 생성하는 데 도움을 줄 수 있습니다. 그럴 때도 핵심은 정책입니다: 멱등성, 재시도, 캐싱, UI 상태를 한 번 정의하고 전체 플로우에 일관되게 적용하세요.
자주 묻는 질문
먼저 각 화면과 작업에 대해 “정상”이 무엇인지 정의하세요. 특히 결제나 주문처럼 한 번만 실행되어야 하는 작업을 명확히 정해야 합니다. 규칙이 정해지면 타임아웃, 재시도, 캐시, UI 상태를 라이브러리 기본값에 의존하지 말고 그 규칙에 맞춰 설정하세요.
사용자는 보통 끝없이 도는 로더, 긴 대기 후 발생하는 오류, 두 번째 시도에서만 동작하는 작업, 또는 두 번 생성된 주문·중복 청구 같은 문제를 경험합니다. 이런 문제들은 대개 네트워크 자체보다 재시도와 “대기 vs 실패” 규칙이 불명확해서 발생합니다.
Connect는 서버에 연결을 설정하는 시간(예: DNS, TCP, TLS), write는 요청 본문을 업로드하는 시간, read는 요청을 보낸 뒤 응답을 기다리는 시간입니다. 위험도가 낮은 읽기는 짧게, 중요한 제출은 읽기/쓰기 타임아웃을 더 길게 두되 UI 차원에서 명확한 상한을 두세요.
OkHttp에서 하나만 설정할 수 있다면 전체 작업을 끝까지 제한하는 callTimeout을 쓰는 것이 좋습니다. 그러면 무한 대기 상황을 피할 수 있습니다. 필요하다면 업로드나 느린 응답에 대해 connect/read/write를 별도로 설정하세요.
일시적인 실패(연결 끊김, DNS 문제, 타임아웃)와 때때로 502/503/504은 재시도해볼 가치가 있습니다. 4xx 오류(잘못된 비밀번호, 누락 필드 등)는 재시도하지 마세요. 쓰기 요청은 서버에 도달해 처리 중일 수 있으므로 멱등성 보호 없이는 자동 재시도가 위험합니다.
재시도는 1–3회로 작게 유지하고 지수적 백오프와 약간의 랜덤 지터를 추가하세요. 또한 전체 재시도에 허용할 최대 시간을 정해 두면 사용자가 스피너에 오래 갇히지 않습니다. 이렇게 하면 앱이 느려지거나 배터리를 소모하는 것을 줄일 수 있습니다.
멱등성은 같은 요청을 여러 번 보내도 두 번째 이상은 동일한 결과를 만들지 않는 성질입니다. 결제와 주문에서는 중복 청구나 중복 생성 방지를 위해 멱등성이 필수적입니다. 각 시도에 멱등성 키를 보내고 재시도 시 동일 키를 재사용하세요.
사용자가 동작을 시작할 때 고유 키를 생성해 로컬에 ‘대기 중’ 상태로 저장하고 요청과 함께 전송하세요. 재시도나 앱 재시작이 있을 때 같은 키를 재사용해 재시도를 하거나 상태 확인을 하도록 하세요. 이렇게 하면 하나의 의도(intent)가 서버에 여러 번 쓰이는 일을 막을 수 있습니다.
모바일에서는 약간 오래된 데이터가 허용되는 항목만 캐시하고, 금전·보안·최종 결정에 영향을 주는 항목은 항상 최신 확인을 하세요. 읽기에는 짧은 캐시 기간과 재검증(ETag)을 권장하고, 쓰기 요청은 캐시하지 않으며 민감한 응답에는 no-store를 사용하세요.
첫 탭 이후 기본 버튼을 비활성화하고 즉시 “제출 중…” 상태를 보여 주세요. 백그라운드로 갔다 와도 대기 상태를 유지하고, 응답을 잃었을 가능성은 ‘우리가 아직 확실하지 않습니다’ 같은 문구로 안내해 반복 탭을 유도하지 마세요. 상태 확인을 제공해 안전하게 재시도를 허용하세요.


