대규모 실시간 쿼리 이해

이 문서를 읽고 초당 작업 수천 개 또는 동시 사용자 수십만 명 이상으로 서버리스 앱을 확장하는 방법을 알아보세요. 이 문서에는 시스템을 심도 있게 이해하는 데 도움이 되는 심화 주제가 포함되어 있습니다. Cloud Firestore를 처음 사용하는 경우에는 빠른 시작 가이드를 참조하세요.

Cloud Firestore와 Firebase 모바일/웹 SDK는 클라이언트 측 코드가 데이터베이스에 직접 액세스하는 서버리스 앱을 개발하기 위한 강력한 모델을 제공합니다. SDK를 사용하면 클라이언트가 실시간으로 데이터 업데이트를 리슨할 수 있습니다. 실시간 업데이트를 사용하여 서버 인프라가 필요 없는 반응형 앱을 빌드할 수 있습니다. 시작하고 실행하는 것은 매우 쉽지만 Cloud Firestore를 구성하는 시스템의 제약조건을 이해하여 트래픽이 증가할 때 서버리스 앱을 확장하고 성능을 향상시키는 데 도움이 됩니다.

앱 확장에 관한 도움말은 다음 섹션을 참조하세요.

사용자와 가까운 데이터베이스 위치 선택

다음 다이어그램은 실시간 앱의 아키텍처를 보여줍니다.

실시간 앱 아키텍처 예시

사용자 기기(모바일 또는 웹)에서 실행 중인 앱이 Cloud Firestore에 연결을 설정하면 데이터베이스가 위치한 동일한 리전의 Cloud Firestore 프런트엔드 서버로 연결이 라우팅됩니다. 예를 들어 데이터베이스가 us-east1에 있으면 연결도 us-east1에 있는 Cloud Firestore 프런트엔드로 라우팅됩니다. 이러한 연결은 오래 지속되고 앱에서 명시적으로 닫을 때까지 계속 열려 있습니다. 프런트엔드는 기본 Cloud Firestore 스토리지 시스템에서 데이터를 읽습니다.

사용자의 물리적 위치와 Cloud Firestore 데이터베이스 위치 간의 거리는 사용자가 경험하는 지연 시간에 영향을 미칩니다. 예를 들어 북미에 있는 Google Cloud 리전의 데이터베이스와 통신하는 인도의 사용자는 데이터베이스가 더 가까이 위치하는 경우(예: 인도 또는 아시아의 다른 지역)에 비해 앱이 더 느리게 작동한다는 것을 경험할 수 있습니다.

안정성을 고려한 설계

다음 주제는 앱의 안정성에 영향을 주거나 이를 개선합니다.

오프라인 모드 사용 설정

Firebase SDK는 오프라인 데이터 지속성을 제공합니다. 사용자 기기의 앱을 Cloud Firestore에 연결할 수 없는 경우 로컬에서 캐시된 데이터로 작업하면 앱을 계속 사용할 수 있습니다. 이렇게 하면 인터넷 연결이 불안정하거나 몇 시간 또는 며칠 동안 전혀 액세스할 수 없는 경우에도 데이터 액세스가 보장됩니다. 오프라인 모드에 대한 자세한 내용은 오프라인 데이터 사용 설정을 참조하세요.

자동 재시도 이해

Firebase SDK는 작업을 재시도하고 끊어진 연결을 다시 설정합니다. 이렇게 하면 서버 재시작 또는 클라이언트 및 데이터베이스 사이의 네트워크 문제로 인해 발생하는 일시적인 오류를 해결할 수 있습니다.

리전 및 멀티 리전 위치 중에서 선택

리전 및 멀티 리전 위치 중에서 선택할 때 여러 가지 장단점이 있습니다. 주요 차이점은 데이터 복제 방식입니다. 데이터 복제 방식에 따라 앱의 가용성 보장이 달라집니다. 멀티 리전 인스턴스는 보다 강력한 제공 안정성을 공급하고 데이터의 내구성을 높이지만 비용이 많이 듭니다.

실시간 쿼리 시스템 이해

스냅샷 리스너라고도 하는 실시간 쿼리를 사용하면 앱에서 데이터베이스 변경사항을 리슨하고 데이터가 변경되는 즉시 지연 시간이 짧은 알림을 받을 수 있습니다. 앱은 주기적으로 업데이트를 위해 데이터베이스를 폴링하여 동일한 결과를 얻을 수 있지만 종종 속도가 느리고 비용이 많이 들고 코드가 더 많이 필요합니다. 실시간 쿼리를 설정하고 사용하는 방법의 예시는 실시간 업데이트 가져오기를 참조하세요. 다음 섹션에서는 스냅샷 리스너의 작동 방식을 자세히 살펴보고 성능을 유지하면서 실시간 쿼리를 확장하기 위한 몇 가지 권장사항에 대해 설명하겠습니다.

모바일 SDK 중 하나로 빌드한 메시지 앱을 통해 Cloud Firestore에 연결하는 두 명의 사용자가 있다고 가정해 보겠습니다.

클라이언트 A는 데이터베이스에 써서 chatroom이라는 컬렉션에 문서를 추가하고 업데이트합니다.

collection chatroom:
    document message1:
      from: 'Sparky'
      message: 'Welcome to Cloud Firestore!'

    document message2:
      from: 'Santa'
      message: 'Presents are coming'

클라이언트 B는 스냅샷 리스너를 사용하여 동일한 컬렉션에서 업데이트를 리슨합니다. 클라이언트 B는 새 메시지가 생성될 때마다 즉시 알림을 받습니다. 다음 다이어그램은 스냅샷 리스너의 아키텍처를 보여줍니다.

스냅샷 리스너 연결 아키텍처

클라이언트 B가 스냅샷 리스너를 데이터베이스에 연결하면 다음과 같은 이벤트 시퀀스가 발생합니다.

  1. 클라이언트 B가 Cloud Firestore에 대한 연결을 열고 Firebase SDK를 통해 onSnapshot(collection("chatroom"))을 호출하여 리스너를 등록합니다. 이 리스너는 몇 시간 동안 활성 상태를 유지할 수 있습니다.
  2. Cloud Firestore 프런트엔드는 기본 스토리지 시스템에 쿼리하여 데이터 세트를 부트스트랩합니다. 일치하는 문서의 전체 결과 집합을 로드합니다. 이를 폴링 쿼리라고 합니다. 그러면 시스템이 데이터베이스의 Firebase 보안 규칙을 평가하여 사용자가 이 데이터에 액세스할 수 있는지 확인합니다. 사용자가 승인되면 데이터베이스는 사용자에게 데이터를 반환합니다.
  3. 그러면 클라이언트 B의 쿼리가 리슨 모드로 전환됩니다. 리스너는 구독 핸들러에 등록되고 데이터가 업데이트될 때까지 기다립니다.
  4. 이제 클라이언트 A가 쓰기 작업을 전송하여 문서를 수정합니다.
  5. 데이터베이스가 문서 변경사항을 스토리지 시스템에 커밋합니다.
  6. 시스템은 동일한 업데이트를 트랜잭션 방식으로 내부 변경 로그에 커밋합니다. 변경 로그는 변경이 발생할 때 엄격한 순서를 지정합니다.
  7. 그런 다음 변경 로그는 업데이트된 데이터를 구독 핸들러 풀에 팬아웃합니다.
  8. 역순 쿼리 일치자를 실행하여 업데이트된 문서가 현재 등록된 스냅샷 리스너와 일치하는지 확인합니다. 이 예시에서는 문서가 클라이언트 B의 스냅샷 리스너와 일치합니다. 이름에서 알 수 있듯이 역순 쿼리 일치자는 역순으로 실행하는 일반 데이터베이스 쿼리라고 생각하면 됩니다. 문서를 검색하여 쿼리와 일치하는 항목을 찾는 대신 수신 문서와 일치하는 항목을 찾기 위해 쿼리를 효율적으로 검색합니다. 일치하는 항목을 찾으면 시스템이 해당 문서를 스냅샷 리스너에 전달합니다. 그런 다음 시스템은 데이터베이스의 Firebase 보안 규칙을 평가하여 승인된 사용자만 데이터를 수신하도록 합니다.
  9. 시스템이 문서 업데이트를 클라이언트 B의 기기에 있는 SDK로 전달하면 onSnapshot 콜백이 실행됩니다. 로컬 지속성이 사용 설정되면 SDK는 업데이트를 로컬 캐시에도 적용합니다.

Cloud Firestore 확장성의 핵심은 변경 로그에서 구독 핸들러와 프런트엔드 서버로의 팬아웃에 따라 달라집니다. 팬아웃을 통해 단일 데이터 변경사항을 효율적으로 전파하여 수백만 개의 실시간 쿼리와 연결된 사용자에게 제공할 수 있습니다. 여러 영역(또는 멀티 리전 배포의 경우 여러 리전)에 걸쳐 이러한 모든 구성요소의 많은 복제본을 실행함으로써 Cloud Firestore는 고가용성과 확장성을 달성합니다.

모바일 및 웹 SDK에서 발행되는 모든 읽기 작업은 위의 모델을 따릅니다. 일관성을 계속 보장하기 위해 폴링 쿼리를 실행한 후 리슨 모드를 수행합니다. 이는 실시간 리스너, 문서 검색 호출, 원샷 쿼리에도 적용됩니다. 단일 문서 검색 및 원샷 쿼리는 성능과 관련하여 유사한 제약조건이 있는 단기 스냅샷 리스너라고 생각할 수 있습니다.

실시간 쿼리 확장 권장사항 적용

확장 가능한 실시간 쿼리를 설계하려면 다음 권장사항을 적용합니다.

시스템의 높은 쓰기 트래픽 이해

이 섹션에서는 증가하는 쓰기 요청에 시스템이 어떻게 반응하는지를 이해할 수 있습니다.

실시간 쿼리를 구동하는 Cloud Firestore 변경 로그는 쓰기 트래픽이 증가함에 따라 자동으로 수평 확장됩니다. 데이터베이스의 쓰기 속도가 단일 서버에서 처리할 수 있는 수준 이상으로 증가하면 변경 로그가 여러 서버로 분할되고 쿼리 처리에서 한 개가 아닌 여러 구독 핸들러의 데이터를 사용하기 시작합니다. 클라이언트와 SDK의 관점에서 이는 모두 투명하며 분할이 발생할 때 앱에서 취해야 할 조치는 없습니다. 다음 다이어그램은 실시간 쿼리의 확장 방식을 보여줍니다.

변경 로그 팬아웃 아키텍처

자동 확장을 사용하면 쓰기 트래픽을 제한 없이 늘릴 수 있지만 트래픽이 증가하면 시스템에서 응답하는 데 다소 시간이 걸릴 수 있습니다. 쓰기 핫스팟이 생성되지 않도록 5-5-5 규칙의 권장사항을 따르세요. Key Visualizer는 쓰기 핫스팟을 분석하는 데 유용한 도구입니다.

많은 앱이 Cloud Firestore에서 사전 조치 없이 수용할 수 있는 예측 가능한 유기적 성장을 하고 있습니다. 그러나 대규모 데이터 세트 가져오기와 같은 일괄 워크로드는 쓰기 작업을 너무 빠르게 늘릴 수 있습니다. 앱을 설계할 때는 쓰기 트래픽의 출처를 염두에 두어야 합니다.

쓰기와 읽기의 상호작용 방식 이해

실시간 쿼리 시스템을 쓰기 작업을 리더와 연결하는 파이프라인이라고 볼 수 있습니다. 문서가 생성, 업데이트 또는 삭제될 때마다 변경사항이 스토리지 시스템에서 현재 등록된 리스너로 전파됩니다. Cloud Firestore의 변경 로그 구조는 strong consistency를 보장합니다. 즉, 앱이 데이터베이스가 데이터 변경사항을 커밋했을 때와 비교해 순서에 맞지 않는 업데이트 알림은 받지 않습니다. 이를 통해 데이터 일관성과 관련된 특이 사례를 제거하여 앱 개발을 간소화합니다.

이 연결된 파이프라인은 핫스팟이나 잠금 경합을 유발하는 쓰기 작업이 읽기 작업에 부정적인 영향을 줄 수 있다는 것을 의미합니다. 쓰기 작업이 실패하거나 제한이 있는 경우 변경 로그의 일관된 데이터를 기다리는 읽기 작업이 중단될 수 있습니다. 이 문제가 앱에서 발생하면 쓰기 작업과 쿼리에 대한 응답 속도가 모두 느려질 수 있습니다. 핫스팟을 피하는 것이 바로 이 문제를 해결하는 열쇠입니다.

문서 및 쓰기 작업을 작게 유지

스냅샷 리스너로 앱을 빌드할 때는 일반적으로 사용자가 데이터 변경사항을 빠르게 파악하도록 하는 것이 좋습니다. 이렇게 하려면 작게 유지해보세요. 시스템은 수십 개의 필드가 있는 작은 문서를 시스템을 통해 신속하게 푸시할 수 있습니다. 수백 개의 필드와 대규모 데이터가 포함된 대용량 문서는 처리 시간이 더 오래 걸립니다.

마찬가지로 지연 시간을 짧게 유지하기 위해 짧고 빠른 커밋과 쓰기 작업을 권장합니다. 대규모 배치는 작성자의 관점에서 더 높은 처리량을 확보할 수 있지만 실제로는 스냅샷 리스너의 알림 시간이 늘어날 수 있습니다. 이는 일괄 처리를 통해 성능을 향상할 수 있는 다른 데이터베이스 시스템을 사용하는 것에 비해 직관적이지 않은 경우가 많습니다.

효율적인 리스너 사용

데이터베이스의 쓰기 속도가 증가하면 Cloud Firestore는 데이터 처리를 여러 서버로 분할합니다. Cloud Firestore의 샤딩 알고리즘은 동일한 컬렉션 또는 컬렉션 그룹의 데이터를 동일한 변경 로그 서버에 병치하려고 합니다. 시스템은 쿼리 처리와 관련된 서버 수를 최대한 적게 유지하면서 가능한 쓰기 처리량을 최대화하려고 합니다.

그러나 특정 패턴으로 인해 스냅샷 리스너에 대해 최적화되지 않은 동작이 발생할 수 있습니다. 예를 들어 앱에서 대부분의 데이터를 하나의 대규모 컬렉션에 저장하는 경우 리스너는 필요한 모든 데이터를 수신하기 위해 여러 서버에 연결해야 할 수 있습니다. 쿼리 필터를 적용해도 마찬가지입니다. 다수의 서버에 연결하면 응답 속도가 느려질 위험이 커집니다.

응답 속도가 느려지는 것을 방지하려면 시스템이 여러 다른 서버로 이동하지 않고도 리스너에 데이터를 제공할 수 있도록 스키마와 앱을 설계합니다. 쓰기 속도가 더 낮은 소규모 컬렉션으로 데이터를 분할하는 것이 가장 효과적일 수 있습니다.

전체 테이블 스캔이 필요한 관계형 데이터베이스의 성능 쿼리를 고려하는 것과 유사합니다. 관계형 데이터베이스에서 전체 테이블 스캔이 필요한 쿼리는 앱 제거율이 높은 컬렉션을 감시하는 스냅샷 리스너와 같습니다. 데이터베이스가 보다 구체적인 색인을 사용하여 제공할 수 있는 쿼리에 비해 실행 속도가 느릴 수 있습니다. 보다 구체적인 색인이 있는 쿼리는 단일 문서 또는 자주 변경되지 않는 컬렉션을 감시하는 스냅샷 리스너와 같습니다. 사용 사례의 동작과 요구사항을 가장 잘 이해하려면 앱을 부하 테스트해야 합니다.

폴링 쿼리 빠르게 유지

반응형 실시간 쿼리의 또 다른 핵심은 데이터를 부트스트랩하기 위한 폴링 쿼리가 빠르고 효율적으로 이루어지도록 하는 것입니다. 새 스냅샷 리스너가 처음 연결되면 리스너는 전체 결과 집합을 로드하고 사용자 기기로 전송해야 합니다. 느린 쿼리를 사용하면 앱의 응답 속도가 느려집니다. 여기에는 예컨대 여러 문서를 읽으려고 시도하는 쿼리나 적절한 색인을 사용하지 않는 쿼리 등이 포함됩니다.

경우에 따라 리스너가 수신 대기 상태에서 폴링 상태로 다시 전환할 수도 있습니다. 이런 일은 자동으로 발생하며 SDK 및 앱에 투명합니다. 다음 조건은 폴링 상태를 트리거할 수 있습니다.

  • 부하 변경으로 인해 시스템이 변경 로그를 재조정합니다.
  • 핫스팟으로 인해 데이터베이스에 쓰기가 실패하거나 지연됩니다.
  • 일시적인 서버 재시작은 리스너에 일시적으로 영향을 미칩니다.

폴링 쿼리가 충분히 빠르면 앱 사용자에게 폴링 상태가 투명해집니다.

장기 리스너 선호

리스너를 최대한 오래 열고 유지하는 것이 Cloud Firestore를 사용하는 앱을 빌드하는 가장 비용 효율적인 방법인 경우가 많습니다. Cloud Firestore를 사용하면 연결을 열린 상태로 유지하는 것에 대해서가 아니라 앱에 반환된 문서에 대해서 요금이 청구됩니다. 장기 스냅샷 리스너는 전체 기간 내내 쿼리를 제공하는 데 필요한 데이터만 읽습니다. 여기에는 최초 폴링 작업과 데이터가 실제로 변경될 때의 알림이 포함됩니다. 반면 원샷 쿼리는 앱이 마지막으로 쿼리를 실행한 이후 변경되지 않았을 수 있는 데이터를 다시 읽습니다.

앱에서 많은 양의 데이터를 소비해야 하는 경우에는 스냅샷 리스너가 적절하지 않을 수 있습니다. 예를 들어 오랜 기간 연결을 통해 초당 많은 문서를 푸시하는 사용 사례의 경우 실행 빈도가 더 낮은 원샷 쿼리를 선택하는 것이 더 좋을 수 있습니다.

다음 단계