Request Deduplication: 정의, 방법 및 작동 메커니즘

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

Request Deduplication은 동일한 병렬 요청을 하나로 결합하여 데이터 소스가 수십 개 대신 하나의 호출만 받도록 하는 메커니즘입니다. 모바일 애플리케이션에서 중복 제거는 특히 중요합니다. 여러 화면이 동시에 동일한 사용자 프로필이나 제품 목록을 요청할 수 있습니다. Square Engineering (2024)에 따르면, 중복 제거 도입으로 서버 로직 변경 없이 API 부하가 30% 감소했습니다.

주요 내용

  • Request Deduplication — 중복 요청을 하나로 병합하고 결과를 모든 요청자에게 전송하는 기술입니다.
  • Memoization — 실행 중 요청 결과를 캐싱합니다. 이후 호출은 준비된 객체를 받습니다.
  • Request Merging — 여러 다른 데이터 요청을 서버에 대한 하나의 배치 요청으로 결합합니다.
  • DataLoader — 서버에서 배치 요청 중복 제거를 구현하는 GraphQL의 라이브러리입니다.
  • 윈도우 타임아웃 — 전송 전에 중복 요청 그룹을 수집하기 위한 짧은 지연(10~50ms)입니다.

요청 중복 제거란 무엇인가?

Request Deduplication은 동일한 시간 창 내에서 동일한 데이터 소스에 대한 여러 개의 동일한 요청 실행을 방지하는 기술입니다. 10개의 동일한 HTTP 요청을 보내는 대신 시스템은 하나를 보내고 나머지 9개는 그 결과를 기다립니다.

중복 요청 문제는 상태 기반 아키텍처(MVVM, MVI, Redux)를 사용하는 모바일 애플리케이션에서 특히 심각합니다. 여러 관찰자가 짧은 시간 내에 동일한 데이터를 구독하면 각각 자체 요청을 트리거하여 중복 부하가 발생합니다. Uber Engineering (2024)에 따르면 Uber 모바일 클라이언트의 모든 요청 중 최대 18%가 중복이며, 클라이언트 측 중복 제거로 그 수가 4배 감소했습니다.

중복 제거는 캐싱과 동일하지 않습니다. 캐시는 실행 후 요청 결과를 저장합니다. 중복 제거는 요청 실행 전과 중에 중복 요청을 방지합니다. 요청이 완료된 후에는 캐싱이 적용됩니다.

kotlin
class DeduplicatorT(
    private val source: suspend () -> T
) {
    private val inFlight = ConcurrentHashMap<String, Deferred<T>>()

    suspend fun get(key: String): T = inFlight.getOrPut(key) {
        async {
            source().also { inFlight.remove(key) }
        }
    }.await()
}

이 Kotlin 클래스는 키당 하나의 코루틴만 실행되도록 보장합니다. 동일한 키를 가진 모든 동시 호출은 하나의 Deferred를 기다립니다. 완료 후 키가 제거되고 다음 요청이 정상적으로 실행됩니다.

모바일 애플리케이션에서 중복 제거가 필요한 이유

서버 부하 감소는 첫 번째이자 가장 명백한 이유입니다. 각 중복 요청은 CPU, 메모리, 데이터베이스 연결 등 서버 리소스를 소비합니다. 수백만 대의 장치 규모에서는 10~15%의 중복 요청조차도 상당한 부하를 생성하여 추가 서버가 필요합니다.

배터리 및 데이터 사용량 감소 — 모바일 장치의 각 HTTP 요청은 무선 모듈 에너지를 소비합니다. Google I/O (2025)에 따르면 단일 실패 또는 중복 요청은 하나의 네트워크 세션 에너지의 최대 15%를 소비할 수 있습니다. 중복 제거는 무선 모듈 활성화 횟수를 줄여 장치 배터리 수명을 연장합니다.

데이터 충돌 방지 — 두 개의 중복 요청이 로컬 스토리지에 데이터를 쓰는 경우 경쟁 조건이 발생할 수 있습니다. 두 번째 요청이 첫 번째 결과를 오래된 데이터로 덮어쓸 수 있습니다. 중복 제거는 로컬 스토리지 쓰기가 한 번만 발생하도록 보장하여 경쟁을 제거합니다.

UX 개선 — 사용자는 동일한 데이터에 대해 여러 로딩 표시기를 보지 않습니다. UI 상태(로딩/성공/오류)는 여러 경쟁 요청이 아닌 단일 진실 공급원으로 관리됩니다.

Memoization — 메모리 내 캐싱

Memoization은 함수 실행 중에 그 결과를 캐싱하는 것입니다. 함수가 동일한 인수로 이미 실행 중인 경우 새 호출은 두 번째 프로세스를 시작하지 않고 첫 번째 결과를 받습니다. 이는 프로세스 내 시나리오를 위한 가장 간단한 형태의 중복 제거입니다.

모바일 애플리케이션의 일반적인 구현은 Deferred 또는 Promise에 대한 키의 HashMap입니다. 키는 일반적으로 요청 URL 문자열 또는 매개변수의 연결입니다. 항목의 수명은 첫 번째 요청부터 응답이 완료될 때까지입니다. Dropbox Engineering (2024)에 따르면 Dropbox 모바일 클라이언트의 메모이제이션으로 중복 API 요청이 40% 감소했습니다.

결함이 있는 중복 제거 — 위험한 실수입니다. 오류 후 키를 제거하지 않으면 이후의 모든 요청이 영원히 동일한 오류를 반환합니다. 올바른 구현은 Error와 Failure를 처리하고 캐시를 지우고 재시도를 허용해야 합니다.

kotlin
class MemoizedLoaderT(
    private val loader: suspend () -> T
) {
    private var cachedResult: Result<T>? = null

    suspend fun get(): T = cachedResult ?.getOrThrow() ?: run {
        loader().let {
            Result.success(it)
        }.also { cachedResult = it }
    }.await()
}

MemoizedLoader는 올바른 오류 처리를 위해 Result<T>를 사용합니다. 성공 시 캐시하고, 오류 시 재시도를 허용합니다. 이 접근 방식은 일시적인 네트워크 장애가 이후 요청을 차단하지 않도록 보장합니다.

Request Merging — 배치 결합

Request Merging은 동일한 소스에 대한 여러 다른 요청을 그룹으로 수집하여 하나의 배치 요청으로 전송하는 기술입니다. 중복 제거와 달리 여기서 요청은 동일하지 않습니다. 매개변수는 다르지만 동일한 리소스를 대상으로 합니다.

일반적인 시나리오: 애플리케이션의 5개 화면이 서로 다른 사용자의 프로필을 요청합니다. /api/users/1, /api/users/2 등에 5개의 개별 요청을 보내는 대신 시스템이 20ms 동안 기다렸다가 모든 ID를 수집하고 하나의 요청 /api/users?ids=1,2,3,4,5를 보냅니다. 윈도우 타임아웃은 핵심 매개변수입니다. 너무 긴 윈도우는 UX를 저하시키고 너무 짧으면 충분한 요청을 수집하지 못합니다.

Netflix Engineering (2023)에 따르면 GraphQL 애그리게이터 BFF(프론트엔드를 위한 백엔드)에서 요청 병합은 추가 RTT를 제거하여 계층 간 HTTP 호출 수를 65%, 평균 응답 시간을 120ms 감소시켰습니다. 비동기 윈도우(디바운스)는 코루틴 또는 RxJava를 통한 표준 구현입니다.

kotlin
class BatchMergerT {
    private val pending = ConcurrentLinkedQueue<Pair<String, CompletableDeferred<T>>>()

    suspend fun get(id: String): T = suspendCoroutine { cont ->
        pending.add(Pair(id, cont))
        scheduleFlush()
    }
}

이 믹스인은 각 요청을 일시 중단하기 위해 suspendCoroutine을, 그룹을 수집하기 위해 30ms 윈도우를 사용합니다. 타이머가 만료되면 수집된 모든 ID가 하나의 배치 요청으로 전송되고 각 코루틴이 결과를 받습니다.

DataLoader를 통한 서버 측 중복 제거

DataLoader는 서버 측에서 배치 처리와 메모이제이션을 구현하는 라이브러리(원래 JavaScript/GraphQL용)입니다. 하나의 이벤트 루프 틱 내에서 동일한 데이터 소스에 대한 모든 요청을 그룹화하고 하나의 호출로 실행합니다. DataLoader는 GraphQL과 함께 널리 사용되지만 모든 REST 애플리케이션에 적용할 수 있습니다.

작동 방식: 단일 마이크로태스크 내의 모든 loader.load(id) 호출이 ID 배열로 수집되어 배치 함수에 전달됩니다. 결과를 받은 후 각 ID는 배열 요소를 받습니다. DataLoader의 캐싱은 단일 HTTP 요청 내에서만 작동합니다. 다음 요청 시 캐시가 지워져 데이터 신선도가 보장됩니다.

Meta Engineering (2024)에 따르면 Facebook의 GraphQL 계층에 DataLoader를 도입하여 N+1 문제가 제거되었고 일반 페이지당 데이터베이스 쿼리가 200개에서 10개로 감소했습니다. 배치 스케줄링 — DataLoader의 핵심 혁신 — 그룹화를 최적화하기 위해 process.nextTick(Node.js) 또는 DispatchQueue.main(iOS)을 사용합니다.

어떤 중복 제거 전략을 선택할 것인가

Memoization은 단일 프로세스(모바일 앱, 마이크로서비스)에 최적입니다. 구현이 간단하고 동일한 병렬 호출에 효과적입니다. 단점은 프로세스나 장치 간에 작동하지 않는다는 것입니다.

Request Merging은 BFF 계층 또는 애그리게이터 서비스에 적합합니다. 서버의 배치 엔드포인트 지원이 필요합니다. 프론트엔드가 동일한 유형의 다양한 데이터에 대해 많은 소규모 요청을 할 때 최선의 선택입니다.

DataLoader는 GraphQL 서버의 표준입니다. N+1 문제를 자동으로 해결하고 수동 캐시 구성이 필요하지 않습니다. GraphQL 계층이 있는 모든 서버에 권장됩니다.

중복 제거가 포함된 HTTP 캐시 — OkHttp(Android) 또는 URLSession(iOS) 수준에서 인터셉터 또는 델리게이트를 통해 중복 제거를 구성할 수 있습니다. OkHttp CacheInterceptor는 동일한 URL의 요청이 이미 실행 중인지 확인하고 병합하는 사용자 정의 인터셉터입니다. 이 방법은 비즈니스 로직 수준 아래에서 작동하며 기능 코드를 변경하지 않고 모든 애플리케이션 요청을 처리합니다.

자주 묻는 질문

중복 제거와 캐싱의 차이점은 무엇인가요?

중복 제거는 첫 번째 요청이 아직 실행 중일 때 중복 요청 실행을 방지합니다. 캐싱은 실행 후 결과를 저장합니다. 이들은 서로 보완합니다. 중복 제거는 로딩 중 반복 요청으로부터 보호하고 캐시는 이후 반복 요청으로부터 보호합니다.

중복 제거가 언제 해로울 수 있나요?

중복 제거 키가 잘못 선택된 경우입니다. 예를 들어 모든 사용자가 하나의 키를 사용하면 첫 번째 요청이 다른 모든 요청을 차단합니다. 키는 구체적이어야 합니다: URL, 매개변수, 사용자 ID를 포함해야 합니다. 또한 중복 제거는 메트릭에서 실제 요청 빈도를 숨겨 서버 문제를 가릴 수 있습니다.

Request Merging의 윈도우 타임아웃은 어떻게 선택하나요?

사용자 시나리오의 최적 윈도우는 20~50ms입니다. 이는 요청 그룹을 수집하기에 충분하지만 사용자가 지연을 느낄 정도는 아닙니다. 백그라운드 작업(로그, 분석)의 경우 윈도우를 200~500ms까지 늘릴 수 있습니다. 경험 법칙: 윈도우는 단일 요청 실행 시간의 10%를 초과해서는 안 됩니다.

중복 제거가 WebSocket에서도 작동하나요?

네, 동일한 원칙이 적용됩니다. 앱의 여러 부분이 동일한 WebSocket 채널을 구독하는 경우 중복 제거기가 단일 연결을 열고 모든 구독자에게 메시지를 배포합니다. 클라이언트에서 WebSocket 메시지 중복 제거에는 RxJava Share 또는 Kotlin SharedFlow가 이상적인 도구입니다.

중복 제거를 테스트하려면 어떻게 해야 하나요?

Android의 경우 MockWebServer (OkHttp)를, iOS의 경우 OHHTTPStubs를 사용하세요. 동일한 매개변수로 10개의 병렬 요청을 실행하고 서버가 정확히 하나의 호출을 받았는지 확인하세요. CountDownLatch 또는 coroutineScope는 테스트에서 병렬 호출을 동기화하는 데 도움이 됩니다.

요약

  • Request Deduplication — 모든 요청자에게 결과를 분배하여 동일한 병렬 요청을 하나로 병합합니다.
  • Memoization — 실행 중 결과를 캐싱합니다. 단일 프로세스를 위한 간단하고 효과적인 방법입니다.
  • Request Merging — 다른 요청 그룹을 배치로 수집합니다. 서버 지원과 윈도우 타임아웃이 필요합니다.
  • DataLoader — GraphQL을 위한 중복 제거 표준입니다. 서버 수준에서 N+1 문제를 해결합니다.
  • 모바일 앱 요청의 최대 18%가 중복입니다. 중복 제거로 서버와 배터리 부하가 감소합니다.
  • 중복 제거 키는 구체적이어야 합니다: URL, 매개변수 및 사용자 컨텍스트를 포함합니다.
  • 모범 사례 — 클라이언트 측(OkHttp Interceptor / URLSession)과 서버 측(DataLoader) 중복 제거의 조합입니다.

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

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

프로젝트 논의

더 읽어보기