다중 Realm 치트 시트

소개

최상의 환경을 제공하기 위한 데이터베이스를 설계하기는 쉽지 않습니다. 특히 모바일 데이터베이스를 설계하는 경우 최종 사용자의 장치에서 가능한 많은 정보를 사용할 수 있어야 하는 동시에 최종 사용자의 기기 디스크 용량과 사용하는 무선 대역폭의 양도 제한해야 하므로 이들 사이의 균형을 맞추는 것이 어렵습니다.

Realm의 설계

다중 Realm을 사용하는 경우 가장 단순한 설계 원칙은 모든 사용자가 접근할 대상과 특정 그룹만 접근할 대상을 결정하는 것입니다. 간단하게 보이지만 실제로는 애플리케이션의 모델 간 상호 작용을 이해해야 합니다. 또한, 어떤 모델을 Realm에서는 <List> 타입이나 백링크로 사용하는 하드 링크로 연결해야 할지, 아니면 레코드 id를 사용해서 액세스할 수 있는 외래 키로 연결해야 할지 결정해야 합니다. Realm에서는 이런 외부 키로 다른 Realm의 모델에 연결하게 됩니다.

Realm 플랫폼의 기본 목표는 대규모 모바일 클라이언트들이 공유하는 데이터를 빠르고 효율적으로 동기화하는 것입니다. 데이터가 다양한 기기들을 이동할 수 있어야 하고 사용자의 저장 공간과 데이터 요금제를 고려하여 오직 필요한 데이터만 실사용자의 기기에서 이동할 수 있다는 원칙으로 DB를 설계했습니다.

Realm은 적어도 두 가지 기본적인 유형으로 Realm을 만들어서 데이터를 분리하기를 권장합니다.

  • 공용 Realm 은 모든 사용자가 로그인할 때 필요로 하는 데이터를 위해 사용합니다.

    • 모든 사용자에 대한 최소한의 프로필 데이터 (사용자가 앱 안에서 다른 사용자를 탐색할 수 있어야 하는 경우)

    • 접근을 확인하거나 설정하는 데 필요한 역할/권한

    • 팀 모델이나 사용자/프로세스 등을 그룹화할 메커니즘

    • 모든 사용자가 접근해야 하는 공용 데이터 모델

  • 온디멘드 Realms 은 사용자/팀/프로젝트 혹은 기타 개별 작업 단위 수준에서 제한되며 모든 사용자의 장치와 동기화되지 않아야 하는 데이터를 위해 사용합니다.

Realm에 접근하기

다양한 Realm을 명료하게 유지하려면 특정 Realm의 모델을 포함하는 Realm 설정에 명시적으로 접근하는 접근자를 사용합니다. 이렇게 하면 올바른 설정으로 Realm이 초기화할 수 있습니다.

Realm Demos GitHub 저장소의 “Realm Bingo” 예제 앱에서는 모델을 참조하는 방식을 확인할 수 있습니다.

여기서는 Realm 접근자의 생성을 주요 구성 요소로 분해해서 필요에 따라 쉽게 구성하고 있습니다.

첫 번째 상수 세트는 연결할 기본 호스트와 공용 Realm을 위한 루트 경로 이름과 개인 Realm을 위한 경로 이름을 지정합니다. 인증 서비스와 동기화 서버인 Realm 오브젝트 서버와 통신하기 위해 defaultSyncHost를 재사용합니다. 대규모 프로덕션 환경이라면 인증 서비스는 다른 호스트가 될 수도 있고, 동기화 서버 역시 다중으로 사용할 수도 있습니다. 이 모든 경우에 완벽하게 동작할 수 있지만, 예제에서는 간단하게 인증과 동기화를 위해 같은 호스트를 사용합니다.

static let defaultSyncHost = "1.2.3.4"
static let syncRealmPath   = "RealmBingo"
static let personalCards   = "myCards"
static let ApplicationName = "RealmBingo"
    
 
// RMP 인증 시스템과 통신하기 위한 URL
static let syncAuthURL	= URL(string: "http://\(defaultSyncHost):9080")!
    
// 애플리케이션 대신 관리하는 동기화 서비스 및 Realm과 통신하기 위한 URL
static let syncServerURL	= URL(string: "realm://\(defaultSyncHost):9080/\(ApplicationName)-\(syncRealmPath)")
    
// 주의: Realm 파일이란 Realm 안의 전체 모델 / 스키마 컬렉션을 의미합니다.
// 따라서 Realm이 나타내는 모델을 명시해야 합니다.
// 다음 예를 확인하세요.
    
static let commonRealmURL = URL(string: "realm://\(defaultSyncHost):9080/\(ApplicationName)-CommonRealm")!
    
func commonRealmConfig(user: SyncUser) -> Realm.Configuration  {
    let config = Realm.Configuration(syncConfiguration: SyncConfiguration(user: SyncUser.current!, realmURL: TeamWorkConstants.commonRealmURL), objectTypes: [CommonModel_1.self, CommonModel_2.self, ..., CommonModel_N.self])
    return config
}
 
 
// 모든 사용자의 홈 경로에 있는 개인 Realm입니다.
// 여기에서 사용자 프로필 정보와 사용자의 응답을 유지합니다.
static let privateRealmURL   = URL(string: "realm://\(defaultSyncHost):9080/~/RealmBingo")!
 
 
func privateRealmConfig(user: SyncUser) -> Realm.Configuration  {
    let config = Realm.Configuration(syncConfiguration: SyncConfiguration(user: SyncUser.current!, realmURL: TeamWorkConstants.privateRealmURL), objectTypes: [PrivateModel_1.self, PrivateModel_2.self, ..., PrivateModel_N.self])
    return config
}

예를 들어 팀 기반의 애플리케이션을 단순하게 나타내는 Realm의 모델 컬렉션은 다음과 같습니다.

이 다이어그램은 다중 Realm 시스템과 특수한 역할 기반의 권한 시스템에 의해 관리될 수 있는 시스템을 모두 나타냅니다. 공용 Realm에서는 모든 참여자에게 유용한 정보를 볼 수 있습니다. 프로필 정보, 역할 등의 정보는 런타임 시 권한을 결정할 수 있는 애플리케이션 상의 로직으로 사용됩니다. 팀과 팀 구성원에 대한 설명은 사람들이 소속된 프로젝트를 열 수 있도록 앱에서 사용하는 정보입니다.

위에서 설명한 기본 정의로부터 RealmBingo 앱을 시작한다면 다음 두 클래스의 Realm을 적용할 수 있습니다.

  1. 공용, 공유 Realm
  2. 프로젝트 Realm

공용 Realm은 위와 같이 정의할 수 있습니다. Realm 구성을 정의하는 모델 모음, Realm 설명자의 구성 요소 정의, 호스트 이름이나 IP 주소, 공용 Realm의 경로 등입니다. 다음 예제를 확인하세요.

static let commonRealmURL = URL(string: "realm://\(defaultSyncHost):9080/\(ApplicationName)-CommonRealm")!

Once the common URLs and other constants are defined we can create a function that returns to us an appropriate Realm configuration

func commonRealmConfig(user: SyncUser) -> Realm.Configuration  {
    let config = Realm.Configuration(syncConfiguration: SyncConfiguration(user: SyncUser.current!, realmURL: TeamWorkConstants.commonRealmURL), objectTypes: [CommonModel_1.self, CommonModel_2.self, ..., CommonModel_N.self])
    return config
}

온디멘드 Realm은 좀 더 가변적입니다. 일반적인 정적 엔드포인트 이름을 각 Realm에 사용하거나 공용 Realm을 구성할 수도 있습니다.

다중 Realm에 걸쳐 레코드 접근

다중 Realm을 사용하는 경우, Realm이 Realm 사이를 상호 지원하는 쿼리를 제공하지 않는데 어떻게 여러 Realm에서 데이터에 접근할 수 있는지 의문이 생길 수 있습니다. 가장 쉬운 방법은 공용 Realm에서 값을 식별하기 위해 SyncUser ID와 비슷한 것을 만드는 것입니다. 예를 들어 특정 팀에 소속된 사용자의 ID를 알고 싶은 경우 아래와 같은 Realm 쿼리를 사용합니다.

let teamMemberIDs = try! someRealm.objects(Person.self).filter(NSPredicate(form:teams.name = %@”,teamName).map({\$0.id})

이 쿼리는 필터 조건에 맞는 사용자의 ID 컬렉션을 반환합니다. 위에서 작성한 Person 모델의 링크를 사용해서 연결된 공용 Realm의 Team 모델과 비교할 수 있습니다. 하나의 Realm을 사용하는 경우라면 이 ID들을 사용해서 같은 Realm 안의 다른 모델을 쿼리하고 관심 객체를 모아서 조작할 수 있습니다.

따라서 다중 Realm 애플리케이션의 다른 기능이라면 사용자 ID를 외래 키로 사용해서 다른 Realm의 값에 접근한다는 것입니다.

이런 방식으로 관리자가 작업을 할당할 수 있는 여러 팀이 있고 팀원이 팀에 관련된 Realm만 열 수 있는 중앙 집중형 클라이언트 서비스 애플리케이션과 같이 더욱 큰 시나리오를 계획할 수 있습니다.

3종류의 독립형 Realm이 있습니다.

  1. 모든 사용자가 접근할 수 있는 “공용 Realm”은 작업자, 어드민과 같은 역할에 대한 정보뿐만 아니라 사용자 프로필 정보, 작업장과 사람들을 지도에 표시할 위치, 모든 가용 팀의 리스트 데이터를 가집니다. 팀 모델에서 팀의 작업 할당 현황과 팀 작업 데이터를 가진 독립 Realm들 중 어떤 Realm이 작업 리스트를 가지고 있는지 추적합니다.

  2. 어드민만 사용하는 “task” Realm에서 어드민과 매니저가 팀에 할당할 작업을 생성합니다. 작업을 생성하는 과정에서 비즈니스 로직을 통해 작업 레코드를 개인별 팀 작업 Realm에도 복사합니다.

  3. 개인별 팀 작업 Realm은 각 팀의 작업 사본을 가집니다. 각 작업의 기본 키인 레코드 ID는 어드민과 매니저가 작업을 생성할 때 만든 소스/마스터 작업 목록에서 보유한 ID와 일치합니다.

여기서 사용하는 “외래 키”는 관리자 전용 작업 Realm의 ID 필드입니다. 이 ID 필드로 작업팀 Realm 내부의 작업에 대한 ID 값을 매핑합니다.

마찬가지로 팀 작업 Realm 내부의 작업 레코드에 있는 데이터만을 바탕으로 작업이 할당될 팀원을 결정하려면 작업자 필드 값을 사용해서 공용 Realm의 작업자 ID를 Person 모델 객체의 기본 키인 ID와 같은 값으로 찾는 쿼리를 사용합니다.

권한 관리

Realm의 권한 구조는 단순합니다. 읽기, 쓰기, 관리 권한을 모든 Realm에 적용할 수 있으며 각각의 상위 권한 수준에는 하위 권한 수준이 포함됩니다. 예를 들어 읽기 전용 Realm은 읽기 권한을 부여하는 것으로 생성될 수 있으며, 만약 쓰기 권한을 부여받은 사용자는 해당 Realm에 대한 읽기 권한도 가집니다. (따라서 익명 쓰기 전용 시스템은 생성할 수 없습니다.) 관리 권한으로는 모든 Realm에 접근할 수 있습니다. 이런 권한을 와일드카드로 모든 사용자에게 적용하거나 사용자 기반으로 개인별로 부여할 수도 있습니다. 단, 높은 수준의 사용 권한이 Realm에 와일드카드로 부여하는 경우 더 높은 권한 부여가 우선으로 적용됩니다.

Realm에서는 일반적으로 사용자별로 권한을 관리할 수 있습니다. 즉, 권한이 부여된 사용자가 다른 사용자에게 명시적으로 권한을 부여하는 방식입니다. 혹은 어떤 팀이나 그룹에 사용자가 추가되거나 삭제되는 경우 등 특정 이벤트에서 트리거하는 방식으로 코드상에서 권한을 부여할 수도 있습니다. 이런 경우 Realm 플랫폼의 백엔드 이벤트 서비스상의 이벤트 핸들러가 트리거됩니다. 두 방식은 동등하게 동작하며, 애플리케이션의 요구 사항이나 대규모 사용자나 복잡한 권한 관리를 위한 앱 아키텍처 상 자동화 시스템의 필요 여부에 따라 선택적으로 사용할 수 있습니다.

수동 권한 설정

다음 코드로 앱 내에서 권한을 변경할 수 있습니다.

let permission = SyncPermissionValue(realmPath: team1Path,
              userID: theId, // The SyncUser.identity value for the grantee
              accessLevel: .write)   // .read, .write, or .manage
user.applyPermission(permission) { error in
  if let error = error {
    // 에러 처리
    return
  }
  // 성공적으로 권한 적용
}

권한 읽기

권한을 가진 사용자를 검색하는 것은 조금 다른 방식이며, Realm에 사용자별 접근 권한을 기반으로 사용자에게 요청합니다. 예를 들어 해당 서버의 모든 Realm에 대한 자신의 권한을 알고 싶으면 다음 코드를 사용합니다.

SyncUser.current?.retrievePermissions { permissions, error in
  if let error = error {
    // 에러 처리
    return
  }
  // 성공적으로 권한 접근
}

반환된 권한 객체는 각각 접근 권한이 있는 Realm을 나타냅니다. 하지만 모든 사용자가 사용할 수 있는 와일드카드 권한으로 사용할 수 있는 Realm은 포함하지 않습니다.