데이터베이스 구조화

이 가이드에서는 Firebase Realtime Database의 JSON 데이터 구조화와 관련된 주요 데이터 아키텍처 개념 및 몇 가지 권장사항을 설명합니다.

데이터베이스를 적절히 구조화하려면 철저한 사전 준비가 필요합니다. 무엇보다도 최대한 쉽게 데이터를 저장하고 이후에 검색할 수 있도록 만들 방법을 계획하는 것이 중요합니다.

데이터를 구조화하는 방법: JSON 트리

모든 Firebase Realtime Database 데이터는 JSON 객체로 저장됩니다. 데이터베이스를 클라우드 호스팅 JSON 트리라고 생각하면 됩니다. SQL 데이터베이스와 달리 테이블이나 레코드가 없으며, JSON 트리에 추가된 데이터는 연결된 키를 갖는 기존 JSON 구조의 노드가 됩니다. 사용자 ID 또는 의미 있는 이름과 같은 고유 키를 직접 지정할 수도 있고, push()를 사용하여 자동으로 지정할 수도 있습니다.

키를 직접 만드는 경우 키는 UTF-8로 인코딩되어야 하고 768바이트 이하여야 하며 ., $, #, [, ], /, ASCII 제어문자 0~31 또는 127을 포함할 수 없습니다. 값 자체에도 ASCII 제어 문자를 사용할 수 없습니다.

예를 들어 사용자가 기본적인 프로필과 연락처 목록을 저장할 수 있는 채팅 애플리케이션을 가정해 보겠습니다. 사용자 프로필은 일반적으로 /users/$uid와 같은 경로에 위치합니다. 사용자 alovelace의 데이터베이스 항목은 다음과 같이 표시됩니다.

{
  "users": {
    "alovelace": {
      "name": "Ada Lovelace",
      "contacts": { "ghopper": true },
    },
    "ghopper": { "..." },
    "eclarke": { "..." }
  }
}

데이터베이스는 JSON 트리를 사용하지만 데이터베이스에 저장되는 데이터는 사용 가능한 JSON 유형에 대응하는 특정한 기본 유형으로 표현하여 코드를 보다 쉽게 관리할 수 있습니다.

데이터 구조 권장사항

데이터 중첩 피하기

Firebase Realtime Database는 최대 32단계의 데이터 중첩을 허용하므로 중첩을 기본 구조로 도입해도 괜찮다고 생각할 수도 있습니다. 그러나 데이터베이스의 특정 위치에서 데이터를 가져오면 모든 하위 노드가 함께 검색됩니다. 또한 사용자에게 데이터베이스의 특정 노드에 대한 읽기 또는 쓰기 권한을 부여하면 해당 노드에 속한 모든 데이터에 대한 권한이 함께 부여됩니다. 따라서 실제 구현에서는 데이터 구조를 최대한 평면화하는 것이 좋습니다.

다음은 데이터 중첩이 바람직하지 않은 이유를 보여 주는 다중첩 구조의 예입니다.

{
  // This is a poorly nested data architecture, because iterating the children
  // of the "chats" node to get a list of conversation titles requires
  // potentially downloading hundreds of megabytes of messages
  "chats": {
    "one": {
      "title": "Historical Tech Pioneers",
      "messages": {
        "m1": { "sender": "ghopper", "message": "Relay malfunction found. Cause: moth." },
        "m2": { ... },
        // a very long list of messages
      }
    },
    "two": { "..." }
  }
}

이 중첩 설계에서는 전체 데이터를 반복하는 것이 어렵습니다. 예를 들어 채팅 대화 제목을 나열하려면 모든 멤버와 메시지를 포함한 전체 chats 트리를 클라이언트에 다운로드해야 합니다.

데이터 구조 평면화

비정규화를 통해 데이터를 서로 다른 경로로 분할하면 필요에 따라 별도의 호출을 통해 효율적으로 다운로드할 수 있습니다. 아래의 평면화된 구조를 살펴보세요.

{
  // Chats contains only meta info about each conversation
  // stored under the chats's unique ID
  "chats": {
    "one": {
      "title": "Historical Tech Pioneers",
      "lastMessage": "ghopper: Relay malfunction found. Cause: moth.",
      "timestamp": 1459361875666
    },
    "two": { "..." },
    "three": { "..." }
  },

  // Conversation members are easily accessible
  // and stored by chat conversation ID
  "members": {
    // we'll talk about indices like this below
    "one": {
      "ghopper": true,
      "alovelace": true,
      "eclarke": true
    },
    "two": { "..." },
    "three": { "..." }
  },

  // Messages are separate from data we may want to iterate quickly
  // but still easily paginated and queried, and organized by chat
  // conversation ID
  "messages": {
    "one": {
      "m1": {
        "name": "eclarke",
        "message": "The relay seems to be malfunctioning.",
        "timestamp": 1459361875337
      },
      "m2": { "..." },
      "m3": { "..." }
    },
    "two": { "..." },
    "three": { "..." }
  }
}

이제 대화당 몇 바이트만 다운로드하여 채팅방 목록 전체를 반복하면서 메타데이터를 빠르게 가져와서 UI에 채팅방을 나열하거나 표시할 수 있습니다. 메시지가 도착하면 별도로 가져와서 표시할 수 있으므로 UI가 빠른 반응 속도를 유지합니다.

확장 가능한 데이터 만들기

앱을 빌드할 때는 목록의 일부만 다운로드하는 것이 나을 때가 많습니다. 목록에 수천 개의 레코드가 포함된 경우에 특히 그러합니다. 데이터의 관계가 정적이며 일방적인 경우에는 상위 객체 아래에 하위 객체를 중첩하면 간단히 해결됩니다.

경우에 따라서는 관계가 동적이거나 데이터를 비정규화해야 할 수 있습니다. 대체적으로 쿼리를 사용하여 데이터의 일부를 검색하면 데이터를 비정규화할 수 있습니다. 자세한 내용은 데이터 검색을 참조하세요.

이 방법도 충분하지 않을 수 있습니다. 예를 들어 사용자와 그룹 간의 양방향 관계가 그런 경우입니다. 사용자는 그룹에 속할 수 있으며, 그룹은 사용자 목록으로 구성됩니다. 이때 사용자가 어떤 그룹에 속해 있는지를 판단하려면 문제가 다소 복잡해집니다.

이러한 경우 특정 사용자가 속하는 그룹을 나열하고 해당 그룹의 데이터만 가져오는 깔끔한 방법이 필요합니다. 이때 그룹 색인이 상당한 도움이 될 수 있습니다.

// An index to track Ada's memberships
{
  "users": {
    "alovelace": {
      "name": "Ada Lovelace",
      // Index Ada's groups in her profile
      "groups": {
         // the value here doesn't matter, just that the key exists
         "techpioneers": true,
         "womentechmakers": true
      }
    },
    // ...
  },
  "groups": {
    "techpioneers": {
      "name": "Historical Tech Pioneers",
      "members": {
        "alovelace": true,
        "ghopper": true,
        "eclarke": true
      }
    },
    // ...
  }
}

위와 같이 Ada의 레코드와 그룹 모두에 관계를 저장하면 일부 데이터가 중복됨을 알 수 있습니다. 현재 alovelace는 그룹 아래에 색인이 생성되어 있고 techpioneers는 Ada의 프로필에 나열되어 있습니다. 따라서 Ada를 그룹에서 삭제하려면 두 위치에서 업데이트가 이루어져야 합니다.

이러한 중복성은 양방향 관계에서 불가피합니다. 사용자 또는 그룹 목록이 수백만 개로 늘어나거나 Realtime Database 보안 규칙으로 인해 일부 레코드에 액세스할 수 없더라도 이 중복성 덕분에 Ada의 소속 그룹을 빠르고 효율적으로 확인할 수 있습니다.

이와 같이 ID를 키로 나열하고 값을 true로 설정하여 데이터를 반전하는 방식을 사용하면 키를 확인하는 작업이 /users/$uid/groups/$group_id를 읽어서 null인지 확인하는 것만큼 간단합니다. 색인은 데이터 쿼리 또는 검색보다 속도가 빠르고 훨씬 효율적입니다.

다음 단계