Offline Queue: 원칙, 전략 및 작동 메커니즘

저자: IT Sectr 게시일: 2026-06-13 읽는 시간: 10 분

Offline Queue는 장치가 오프라인일 때 사용자 작업을 로컬에 저장하고 연결이 복원된 후 서버로 전송하는 메커니즘입니다. 오프라인 큐가 없으면 사용자는 인터넷 없이 수행한 모든 작업을 잃게 되며, 이는 모바일 애플리케이션에서 허용되지 않습니다. Google Developers(2025)에 따르면, 오프라인 우선 아키텍처를 도입하면 인터넷이 불안정한 지역에서 사용자 유지율이 30% 향상됩니다.

핵심 요점

  • Offline Queue — 사용자가 인터넷 없이 수행하는 작업의 FIFO 큐로, 이후 동기화를 위해 사용됩니다.
  • Persistent storage — 큐는 로컬 데이터베이스(SQLite, Room)에 저장되어 앱 재시작 시에도 보존됩니다.
  • Exponential backoff — 전송 실패 시 간격을 늘려가며 재시도하는 전략입니다.
  • Conflict resolution — 오프라인 변경 사항이 서버 데이터와 충돌할 때 충돌을 해결하는 메커니즘입니다.
  • Idempotency keys — 재전송 시 서버에서 중복을 방지하기 위한 고유한 작업 키입니다.

오프라인 큐란?

Offline Queue는 장치가 네트워크에 접근할 수 없을 때 애플리케이션이 로컬에 저장하는 작업(생성, 업데이트, 삭제)의 정렬된 컬렉션입니다. 연결이 복원되면 큐는 사용자가 수행한 순서대로 서버에 작업을 전송합니다.

시나리오를 상상해보세요: 메신저 사용자가 인터넷 없이 지하철에서 메시지를 입력하고 있습니다. «보내기»를 탭할 때마다 Offline Queue에 추가됩니다. 기차가 터널을 나와 네트워크를 사용할 수 있게 되면 모든 메시지가 자동으로 전송됩니다. 사용자 경험 — 원활함: 전송 시 약간의 지연을 제외하고는 오프라인이었다는 것을 알아차리지 못합니다.

Uber Engineering(2024)에 따르면, 이들의 오프라인 큐는 연결 품질이 낮은 지역에서 하루에 200만 개 이상의 작업을 처리합니다. 큐는 FIFO 순서와 exactly-once 보장 전달 메커니즘을 갖춘 Room 로컬 스토리지를 사용합니다.

kotlin
data class QueuedOperation(
    val id: String,
    val type: OperationType,
    val endpoint: String,
    val payload: String,
    val timestamp: Long,
    val retryCount: Int = 0,
    val idempotencyKey: String
)

각 작업에는 재전송에 필요한 모든 데이터(엔드포인트, 요청 본문, 타임스탬프 및 idempotencyKey)가 포함됩니다. Room 데이터베이스는 앱 재시작 및 OS 충돌 시 큐의 지속성을 보장합니다.

모바일 앱에 작업 큐가 필요한 이유

전달 보장 — 큐의 주요 목적입니다. 사용자는 네트워크를 사용할 수 없더라도 자신의 작업(메시지 보내기, 좋아요, 주문)이 완료될 것이라고 확신해야 합니다. 재시도 메커니즘이 있는 Offline Queue는 최종 전달을 보장합니다.

열악한 연결 환경에서의 UX 향상GSMA Mobile Economy Report(2025)에 따르면, 전 세계 모바일 사용자의 약 40%가 불안정한 인터넷 연결을 사용합니다. Offline Queue는 지하철, 엘리베이터, 원격 지역 등 연결이 불안정한 모든 곳에서 앱을 사용할 수 있게 합니다.

데이터 손실 감소 — 큐가 없으면 오프라인에서 수행된 모든 작업이 손실됩니다. 사용자가 긴 양식을 작성하고 «제출»을 탭한 후 네트워크 오류를 볼 수 있습니다 — 모든 입력이 손실됩니다. Offline Queue는 데이터를 저장하고 첫 번째 기회에 전송합니다. Google Docs의 자동 저장은 문서를 위한 오프라인 큐의 전형적인 예입니다.

비동기 동기화 — 큐를 사용하면 앱이 전송 중에 UI를 차단하지 않습니다. 사용자는 계속 작업하고 동기화 관리자는 백그라운드에서 큐를 처리합니다. 이는 반응형 아키텍처 원칙을 따르며 인터페이스 응답성을 향상시킵니다.

오프라인 큐 아키텍처: 저장 및 처리

큐의 세 가지 계층: 스토리지(지속성), 스케줄러, 실행기. 스토리지 — QueuedOperation 테이블이 있는 Room. 스케줄러 — 네트워크를 사용할 수 있을 때 동기화를 시작하는 WorkManager(Android) 또는 BGTaskScheduler(iOS). 실행기 — 작업을 하나씩 전송하는 순차적 FIFO 반복기.

처리 순서 — 데이터 일관성에 매우 중요합니다. 사용자가 레코드를 만든 다음 편집한 경우 두 작업 모두 동일한 순서로 전송되어야 합니다. 그렇지 않으면 서버가 먼저 존재하지 않는 레코드의 업데이트를 받게 되어 오류가 발생합니다. 순차적 FIFO — 작업 간 종속성 제어를 통한 엄격한 순서.

병합 전략 — 큐에 CREATE가 있고 바로 뒤에 같은 객체의 DELETE가 있는 경우, 두 작업을 전송하지 않고 제거할 수 있습니다. 최종 상태는 객체가 생성되지 않은 것입니다. 마찬가지로 CREATE + UPDATE를 최신 데이터로 하나의 CREATE에 병합할 수 있습니다. 큐 최적화는 HTTP 요청 수를 줄이고 동기화 속도를 높입니다.

Android Developers(2025)에 따르면, Android에서 Offline Queue를 처리하는 선호 방법은 WorkManager입니다. 장치 재시작 후에도 실행을 보장하고, 네트워크 제약 조건을 지원하며, NetworkType.CONNECTED를 통해 재시도 정책을 구성할 수 있습니다.

kotlin
class SyncWorker(
    private val context: Context,
    private val params: WorkerParameters
) : CoroutineWorker(context, params) {

    override suspend fun doWork(): Result = runCatching {
        queueRepository.processNextBatch(batchSize = 10)
        Result.success()
    }.getOrDefault(Result.retry())
}

CoroutineWorker는 작업 배치를 처리하고 실패 시 Result.retry()를 반환합니다 — WorkManager는 지수 백오프로 자동 재시도합니다. Android에서 안정적인 Offline Queue를 얻는 가장 간단한 방법입니다.

재시도 전략: exponential backoff 및 재시도 정책

Exponential Backoff — 간격을 늘려가며 재시도하는 표준 전략: 2초, 4초, 8초, 16초... 최대 임계값까지. 이는 서버가 일시적으로 사용 불가능할 때 반복적인 과부하를 방지합니다. Java 라이브러리 Resilience4j(2024)는 구성 가능한 백오프를 갖춘 기성 Retry 구현을 제공합니다.

최대 시도 횟수 — 중요한 매개변수입니다. 5~10회 시도 후에도 작업이 실패하면 추가 재시도는 낭비이고 무용지물입니다. dead letter queue가 권장됩니다: 시도를 소진한 후 작업은 수동 분석을 위해 별도 테이블로 이동됩니다. Microsoft Patterns & Practices(2024)에 따르면, dead letter queue는 동기화 문제 디버깅을 간소화하고 잘못된 작업이 큐를 차단하는 것을 방지합니다.

Jitter — 무작위 변동 — 백오프 간격에 무작위 숫자를 추가합니다. 중단 후 1,000개의 장치가 동시에 네트워크를 복구하면 모두 동시에 동기화를 시작합니다. Jitter는 시간적으로 분산시켜 서버의 Cache Stampede를 방지합니다. 완전한 jitter: delay = random(0, backoff) — AWS(2024)가 API 클라이언트에 권장합니다.

충돌 해결: 데이터 충돌을 해결하는 방법

Last Write Wins(LWW) — 가장 간단한 전략: 충돌 시 더 늦은 타임스탬프의 작업이 승리합니다. LWW는 시간 동기화가 필요합니다 — 타임스탬프는 서버에서 생성되거나 논리적 시계(램포트 시계)를 사용해야 합니다. 단점: 한 사용자의 데이터가 경고 없이 다른 사용자의 데이터로 덮어쓰기될 수 있습니다.

OT(운영 변환) — Google Docs와 Figma가 오프라인 모드를 포함한 실시간 공동 편집에 사용하는 알고리즘입니다. OT는 작업을 변환하여 문서의 모든 상태에 적용할 수 있도록 하여 잠금 없이 일관성을 보장합니다. CRDT(충돌 없는 복제 데이터 유형) — 모바일 앱에서 인기를 얻고 있는 OT의 대안: 중앙 서버 없이 수학적으로 충돌을 해결할 수 있도록 데이터가 구조화됩니다.

사용자 정의 병합 — 간단한 데이터 모델(메모, 연락처)을 가진 앱의 경우 사용자 정의 병합 규칙을 구현할 수 있습니다. 예를 들어 메모의 경우: 텍스트가 두 버전에서 수정된 경우 구분자로 연결하여 병합합니다. 사용자 해결 충돌 — 자동 병합이 불가능한 경우 사용자에게 두 버전을 모두 표시하고 선택하게 합니다. Dropbox(2024)는 오프라인 파일 충돌에 대해 이 접근 방식을 사용하며 «Conflicted Copy» 접두사가 있는 복사본을 만듭니다.

Idempotency keys — 중복 방지

Idempotency Key — 서버가 중복 요청을 감지하는 데 사용하는 고유한 작업 식별자입니다. 클라이언트가 동일한 키로 동일한 요청을 보내면 서버는 다시 실행하지 않고 이미 완료된 작업의 결과를 반환합니다. 이는 네트워크 오류로 인한 재전송이 가능한 Offline Queue에 매우 중요합니다.

Idempotency key의 형식은 UUID 또는 요청 매개변수의 해시입니다. 서버는 중복을 감지하기 위해 일정 시간(보통 24시간) 동안 결과와 함께 완료된 키를 저장해야 합니다. Stripe API(2024)가 참조 예시입니다: 키는 Idempotency-Key 헤더로 전달되며, 동일한 키로 반복된 요청은 캐시된 응답을 반환합니다.

클라이언트 측 생성 — 키는 작업을 보내기 전에 클라이언트에서 생성되어 QueuedOperation 테이블에 저장됩니다. 재시도 시 키는 변경되지 않습니다. Exactly-once 아키텍처 — 클라이언트의 idempotency key와 서버의 중복 제거 조합이 작업이 두 번 실행되지 않음을 보장하는 유일한 방법입니다.

kotlin
fun createOperation(type: OperationType, payload: String): QueuedOperation =
    QueuedOperation(
        id = UUID.randomUUID().toString(),
        type = type,
        endpoint = type.endpoint,
        payload = payload,
        timestamp = currentTimeMillis(),
        idempotencyKey = UUID.randomUUID().toString()
    )

각 작업은 두 개의 UUID를 받습니다: 하나는 큐의 레코드 식별자, 다른 하나는 서버용 idempotency key입니다. idempotencyKey에 의한 서버 측 중복 제거는 재전송 시에도 주문이 중복되지 않음을 보장합니다.

자주 묻는 질문

Offline Queue와 캐시의 차이점은 무엇인가요?

캐시는 오프라인에서 빠르게 읽기 위한 데이터 복사본을 저장합니다. Offline Queue는 서버에 나중에 쓰기 위해 사용자 작업을 저장합니다. 캐시는 읽기용, 큐는 쓰기용입니다. 두 구성 요소는 오프라인 우선 아키텍처에서 공존할 수 있습니다.

모바일 장치에 안전한 큐 크기는 얼마인가요?

권장 제한 — 100~500개 작업. 그 이상은 메모리 오버플로우와 네트워크 복구 시 긴 동기화 위험이 있습니다. 제한을 초과하면 앱이 사용자에게 경고하고 작업 우선순위를 제안해야 합니다. 합리적인 제한 — 업데이트 작업 50개 + 생성 작업 10개.

큐에서 오래된 작업은 어떻게 처리하나요?

7일 이상 된 작업으로 성공 횟수가 0인 경우 dead letter queue로 이동됩니다. 수동으로 분석하세요: API가 변경되어 엔드포인트가 더 이상 존재하지 않을 수 있습니다. 자동 정리 — HealthCheck 작업이 매일 실행되어 만료된 작업을 삭제하거나 보관합니다.

아직 전송되지 않은 이전 작업에 의존하는 작업은 어떻게 하나요?

종속성 그래프(DAG)를 사용하세요: 각 작업에는 전송 전에 완료되어야 하는 parentOperationId 목록이 포함됩니다. ORDER BY parent를 사용한 Room 쿼리는 올바른 순서로 작업을 반환합니다. 계단식 전송 — 각 작업이 완료된 후 하위 작업이 차단 해제되었는지 확인합니다.

Offline Queue를 테스트하려면 어떻게 하나요?

네트워크 손실을 시뮬레이션하려면 Android Emulator의 Network Less Tool 또는 iOS Simulator의 Network Link Conditioner를 사용하세요. 오프라인 모드에서 큐에 작업을 추가하고, 연결을 복원하고, 모든 작업이 전송되어 서버에서 처리되었는지 확인하는 테스트를 작성하세요.

요약

  • Offline Queue — 연결 복원 후 전송하기 위해 로컬에 저장된 작업의 FIFO 큐.
  • Persistent storage(Room / SQLite) — 앱 재시작 시 큐를 보존하는 데 필요.
  • Jitter가 있는 Exponential backoff — 서버 과부하를 방지하는 표준 재시도 전략.
  • Conflict resolution — 오프라인 데이터 충돌 해결을 위한 LWW, OT, CRDT 또는 사용자 정의 규칙.
  • Idempotency key — 서버에서 exactly-once 전달을 보장하는 각 작업의 UUID.
  • Dead letter queue — 수동 분석을 위해 시도를 소진한 문제 작업의 격리.
  • Android 모범 사례 — WorkManager + Room + ExponentialBackoff — Google의 검증된 조합.

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기