Realm 데이터 모델

Realm 플랫폼의 핵심인 Realm 모바일 데이터베이스는 오픈 소스로 모바일 사용에 최적화된 내장 데이터베이스 라이브러리입니다. SQLite나 Core Data 같은 데이터 저장소를 사용하는 경우 Realm 모바일 데이터베이스를 경량 관계형 데이터베이스로 뒷받침되는 객체 관계 매핑, 즉 ORM이라고 생각할 수 있습니다. 하지만 Realm은 ORM이 아닙니다. 대신 Realm은 “데이터 컨테이너” 모델을 사용합니다. 데이터 객체는 Realm에 객체로서 저장됩니다. 이 덕분에 Realm은 몇 가지 중요한 장점을 갖습니다.

  • Realm은 네이티브 객체 를 저장합니다: Realm 모바일 데이터베이스에는 Swift, Java, Objective-C, C#, React Native를 사용하는 JavaScript 등 모바일 앱 개발에 주로 사용되는 대부분의 언어 바인딩이 있습니다. Realm에 저장하는 객체를 그대로 나머지 코드에서 사용할 수 있습니다.
  • Realm은 zero-copy 입니다: 데이터에 접근하기 위해 데이터를 데이터베이스 안팎으로 복사할 필요가 없습니다. 해당 객체 그대로 직접 작업할 수 있습니다.
  • Realm은 라이브 오브젝트 패턴 을 구현합니다: Realm에 저장된 객체의 인스턴스가 있고 애플리케이션의 다른 곳에서 이 객체를 업데이트하면 기존 인스턴스에 변화가 반영됩니다.
  • Realm은 크로스 플랫폼 입니다: Realm에 특정 플랫폼에 국한된 객체를 저장하지 않는 이상 데이터는 OS를 가리지 않고 동기화될 수 있습니다. (사실 실제 Realm 데이터 파일을 다른 플랫폼 간에 복사해서 사용할 수도 있습니다.)
  • Realm은 오프라인-우선 입니다: 애플리케이션은 Realm이 Realm 오브젝트 서버와 동기화되는지 여부와 관계없이 같은 방법으로 동작됩니다. 연결이 가능해지면 변경 사항이 매끄럽게 동기화됩니다.
  • 마지막으로 Realm은 ACID를 준수합니다.

크로스 플랫폼 모두에 적용되는 개념을 설명하면서 간단한 예제 코드는 Swift로 제공하겠습니다. 다른 언어로 된 예제는 해당 언어 문서의 섹션에서 볼 수 있습니다.

Realm이란?

Realm이란 Realm 모바일 데이터베이스 컨테이너의 인스턴스입니다. Realm은 로컬, 동기화, 혹은 인 메모리 방식으로 사용할 수 있습니다. 이 중 어느 종류의 Realm이라도 애플리케이션에서 같은 방식으로 동작할 수 있습니다. 인 메모리 Realm은 저장 메커니즘이 없는 임시 저장소를 뜻합니다. 동기 Realm은 Realm 오브젝트 서버를 사용해서 다른 기기 사이에 컨텐츠를 동기화합니다. 애플리케이션이 동기 Realm을 로컬 파일처럼 사용하는 동안 쓰기 접근이 가능한 다른 디바이스에서 해당 Realm의 데이터를 업데이트할 수 있습니다. 따라서 Realm은 방에 참여한 어느 사용자라도 업데이트할 수 있는 채팅 애플리케이션의 채팅방을 나타낼 수 있습니다. 혹은 소유한 모든 기기에서 접근할 수 있는 쇼핑 앱의 장바구니가 될 수도 있습니다.

다른 종류의 데이터베이스 사용에 익숙한 분을 위해 Realm에 적용되는 않는 개념을 소개합니다.

  • Realm은 단일 애플리케이션 차원의 데이터베이스가 아닙니다. 일반적으로 애플리케이션은 하나의 SQL 데이터베이스만을 가질 수 있지만, Realm의 경우 하나의 애플리케이션에서 여러 Realm을 사용해서 데이터를 효율적으로 구성하거나 접근 제어 목적으로 데이터를 저장할 수도 있습니다.
  • Realm은 테이블이 아닙니다. 전형적으로 테이블은 사용자 기록, 이메일 내용 등 한 종류의 정보만 저장합니다. 하지만 Realm은 여러 종류의 객체를 저장할 수 있습니다.
  • Realm은 스키마가 없는 문서 저장소가 아닙니다. 객체 속성은 키/값 쌍과 유사하므로 Realm을 문서 저장소라고 생각하기 쉽지만, Realm 내부의 객체는 기본 값을 가지거나 값이 있거나 옵셔널 이도록 정의하는 스키마를 가지고 있습니다.

앞서 말한 가상 채팅 애플리케이션은 오픈 채팅을 위해 동기 Realm 하나를 사용할 수 있으며, 그 밖에도 사용자 데이터를 저장하는 다른 동기 Realm이나, 관리자가 아닌 사용자에게는 읽기 권한만 있는 마스터 채널 리스트를 위한 다른 동기 Realm, 또한 해당 디바이스에 저장될 설정값을 위한 로컬 Realm을 추가적으로 사용할 수 있습니다. 혹은 같은 기기에서 여러 명의 사용자가 쓸 수 있도록 각 사용자의 개인 데이터를 사용자별 Realm에 저장할 수도 있습니다. Realm은 애플리케이션이 한 번에 여러 개를 사용할 수 있을 만큼 가볍습니다. (모바일 플랫폼은 자원 제약이 일부 존재하지만 최소한 12개까지는 한 번에 열어도 문제가 없습니다.)

Realm 열기

JavaScript에서는 모델의 스키마를 설정 객체 안에서 생성자에 전달해야 합니다. 자세한 내용은 JavaScript 문서의 모델을 참고하세요.

Realm을 열기 위해서는 접근 방법을 정의하는 설정 객체 를 생성자에 전달해야 합니다. 설정 객체는 Realm 데이터베이스의 위치를 지정합니다.

  • 기기의 로컬 파일 시스템 상의 경로
  • Realm 오브젝트 서버의 URL과 필요한 자격 증명 (사용자/비밀번호, 인증 토큰)
  • 인 메모리 Realm의 식별자

(설정 객체는 사용 언어에 따라 다른 값을 지정할 수 있으며 필요시 마이그레이션에 사용됩니다. 앞서 설명한 것처럼 JavaScript의 경우 모델 스키마도 설정 객체에 추가됩니다.) 설정 객체를 제공하지 않으면 해당 애플리케이션의 로컬 Realm인 기본 Realm 이 열립니다.

동기 Realm을 여는 것은 다음 예제와 같습니다. 이름이 "settings"인 Realm을 열겠습니다.

// 설정 객체 생성
let realmUrl = URL(string: "realms://example.com:9000/~/settings")!
let realmUser = SyncCredentials.usernamePassword(username: username, password: password)
let config = Realm.Configuration(user: realmUser, realmURL: realmUrl)

// 설정 객체로 Realm 열기
let settingsRealm = try! Realm(configuration: config)

로컬 혹은 인 메모리 Realm은 훨씬 간단하게 열 수 있습니다. URL 또는 사용자 인수가 필요하지 않고 다음처럼 한 줄만으로 기본 Realm을 열 수 있습니다.

let defaultRealm = try! Realm()

Realm URL

동기 Realm은 공용(public), 개인(private), 혹은 공유(shared) 형식 중 하나입니다. 모두 같은 방식으로 접근할 수 있고 내부적인 차이는 없습니다. 다만 사용자가 이를 읽고 쓸 수 있도록 접근을 제어하는 방식에서 차이가 있습니다. URL 형식도 조금 다를 수 있습니다.

  • 공용 Realm은 모든 사용자가 접근할 수 있습니다. Realm 오브젝트 서버에서 어드민 사용자가 소유하며, 어드민이 아닌 사용자에게는 읽기 전용입니다. URL은 realms://server/realm-name 형식입니다.
  • 개인 Realm은 특정 사용자가 만들고 소유하며, 기본적으로는 해당 사용자에게만 읽기와 쓰기 권한이 있습니다. URL 형식은 realms://server/user-id/realm-name입니다.
  • 공유 Realm은 개인 Realm으로, 소유한 사용자가 다른 사용자에게 읽기 권한이나 읽기와 쓰기 권한을 부여한 Realm입니다. 예를 들어 여러 명의 가족 구성원이 공유하는 쇼핑 리스트와 같은 경우입니다. URL 형식은 개인 Realm과 같은 realms://server/user-id/realm-name입니다. (경로 중 user-id는 소유한 사용자의 ID입니다.) 공유받은 사용자는 해당 Realm의 로컬 사본을 각각 갖지만, “마스터” 복사본 하나만이 오브젝트 서버에 동기화됩니다.

보통 개인 Realm의 URL에 사용자 ID 대신 물결표 (~)가 표시됩니다. 이는 “현재 사용자 ID를 대입하라”는 뜻입니다. 애플리케이션 개발자가 코드에서 개인 Realm을 쉽게 참조할 수 있도록 하기 위한 축약으로, 예를 들어 개인 설정 Realm을 참조하라는 표현을 realms://server/~/settings라고 할 수 있습니다.

Realm URL은 파일 시스템과 일치하는 구조라고 이해할 수 있습니다. 공용 Realm은 최상위 “루트” 디렉터리에 있으며, 사용자가 소유한 Realm은 하위 디렉터리에 있습니다. (물결표는 사용자의 홈 디렉터리를 ~로 참조하는 Unix 스타일입니다.)

참고: realms:// 접두어는 https://와 비슷합니다. 예를 들어 “s”는 SSL 암호화 사용자를 나타냅니다. realm://으로 시작하는 Realm URL은 암호화되지 않습니다.

권한

Realm 오브젝트 서버가 관리하는 Realm에는 공용, 개인, 공유 형식을 제어하는 접근 권한 이 있습니다. 권한은 각 Realm에서 다음 세 불린 플래그를 사용해서 설정합니다.

  • mayRead 사용자가 Realm을 읽을 수 있는지를 표기합니다.
  • mayWrite 사용자가 Realm에 쓸 수 있는지를 표기합니다.
  • mayManage 사용자가 Realm에 대한 다른 사용자 권한을 변경할 수 있는지 여부를 표기합니다.

권한 플래그는 기본 혹은 사용자별 로 설정할 수 있습니다. 사용자가 Realm 접근 권한을 요청하면, 먼저 오브젝트 서버가 해당 Realm에 해당 사용자를 위한 사용자별 권한 설정이 있는지 확인합니다. 해당 사용자에 대한 사용자별 권한 설정이 없는 경우 해당 Realm의 기본 권한을 사용합니다. 예를 들어 mayRead 기본 설정이 true이고 개인 사용자별로 mayWrite 권한을 가지도록 특정 Realm에 설정할 수 있습니다.

기본적으로 Realm은 개인 Realm 형식입니다. 소유한 사용자가 모든 권한을 갖고, 다른 사용자는 아무 권한도 갖지 않습니다. 다른 사용자는 명시적으로 접근 권한을 부여받아야 합니다. (단, 어드민 사용자는 항상 해당 오브젝트 서버의 모든 Realm에 대한 모든 권한을 갖습니다.)

권한에 대한 자세한 설명은 다음 문서를 확인하세요.

모델과 스키마

객체를 기존 관계형 데이터베이스에 저장하려면 테이블(예를 들어 users)에 해당하는 객체 클래스(예를 들어 User)의 각 인스턴스를 테이블 행에 매핑하고, 객체의 속성은 테이블 열에 매핑해야 합니다. 하지만 Realm에서는 매핑없이 코드에서 실제 객체를 사용할 수 있습니다.

class Dog: Object {
    dynamic var name = ""
    dynamic var age = 0
    dynamic var breed: String? = nil
    dynamic var owner: Person?
}

Dog 객체에는 네 개의 속성이 있습니다. 이 중 name은 빈 문자열을, age0을 기본값으로 하므로 기본값이 있는 필수 속성입니다. 한편 breed 속성은 옵셔널 문자열 속성이고 owner 속성은 옵셔널 Person 객체 속성입니다. 옵셔널 속성은 nullable 속성 이라고도 부르는데 값이 언어에 따라 nil이나 null로 설정될 수 있다는 뜻으로 Realm에 저장하기 위해 특정 값을 설정하지 않아도 됩니다. name이나 age와 같은 필수 속성은 ` nil`로 설정할 수 없습니다.

필수나 옵셔널 속성 문법에 대해서는 사용하는 언어의 문서를 확인하세요! 예를 들어 Java의 경우 모든 속성이 기본적으로 nullable이며, 필수 속성에는 @Required 어노테이션이 있어야 합니다. JavaScript는 속성 유형과 기본값을 다른 방식으로 표시합니다.

관계

관계형 데이터베이스에서는 테이블 간의 관계를 기본 키와 외래 키로 정의합니다. 한 사람(Person)이 여러 개(Dog)를 소유하고 있다면 Dog 모델은 소유자인 Person의 기본 키인 외래 키 필드를 가질 겁니다. 이런 관계를 “다 대 다” 관계라고 부릅니다. 역관계, 즉 개가 소유주에 의해 소유된 관계는 명시적으로 데이터베이스에 정의되지 않지만 일부 ORM에는 구현돼 있습니다.

Realm은 모델 스키마의 속성으로 선언해서 이와 비슷한 관계 개념을 만듭니다.

다 대 일 관계

앞서 말한 Dog 모델로 설명하겠습니다.

class Dog: Object {
    dynamic var name = ""
    dynamic var age = 0
    dynamic var breed: String? = nil
    dynamic var owner: Person?
}

owner 속성은 관계를 설정하려는 Object의 서브클래스입니다. 위 코드만으로 “일 대 일” 혹은 다 대 일” 관계를 설정할 수 있습니다. 이제 DogPerson 사이의 관계를 정의합니다.

let bob = Person()
let fido = Dog()
fido.owner = bob

다른 언어에서도 비슷한 방식입니다. Java의 Dog 구현은 다음과 같습니다.

public class Dog extends Realm Object {
	@Required
	private String name = "";
	
	@Required
	private Integer age = 0;
	
	private String breed;
	private Person owner;
}

다 대 다 관계

Dog에서 사용한 Person 클래스를 보여드리겠습니다.

class Person: Object {
	let name = ""
	let dogs = List<Dog>()
}

Realm의 list 는 하나 이상의 Realm 객체를 포함할 수 있습니다. Bob의 개 리스트에 Fido를 추가해 보겠습니다.

bob.dogs.append(fido)

다른 언어에서도 역시 비슷합니다. Java의 경우 기본 list와 구분하기 위해 RealmList를 속성 타입으로 설정합니다.

public class Person extends RealmObject {
	@Required
	private String name;
	
	private RealmList<Dog> dogs;

(주의: Java의 경우 RealmList 속성이 항상 필수로 간주되므로 @Required 어노테이션을 붙이지 않아도 됩니다. JavaScript의 경우 다른 방식으로 list 속성을 정의합니다. 구체적인 언어 바인딩에 대한 내용은 해당 언어의 문서와 API를 참조하세요.)

역관계

한 사람이 여러 개를 소유한 관계는 개가 사람에게 소유되는 관계, 즉 역관계를 자동으로 생성하지 않습니다. 양측의 관계는 명시적으로 설정해야 합니다. 어떤 DogPersondogs 리스트에 추가하더라도 해당 Dogowner 속성이 자동으로 설정되지는 않습니다. 그 이유는 코드에서 관계를 쉽게 탐색하고 Realm의 알림 시스템이 올바르게 동작할 수 있도록 하기 위해서입니다.

일부 Realm 언어 바인딩은 특정 객체에 연결된 모든 객체를 반환하는 “객체 연결” 속성을 지원합니다. Dog를 이 방식으로 정의하면 모델은 다음과 같습니다.

class Dog: Object {
    dynamic var name = ""
    dynamic var age = 0
    dynamic var breed: String? = nil
    let owners = LinkingObjects(fromType: Person.self, property: "dogs")
}

bob.dogs.append(fido)를 실행하면 fido.ownerbob을 가리키게 됩니다.

현재 Objective-C, Swift, Xamarin에서 객체 연결 기능을 지원합니다.

기본 키

Realm은 외래 키를 사용하지 않지만, Realm 객체는 기본 키 속성을 지원합니다. 모델 클래스의 속성 중 하나를 기본 키로 지정하면 고유성이 적용됩니다. Realm에 저장하려면 해당 클래스 객체들의 기본 키가 모두 고유해야 합니다. 또한, 기본 키는 명시하지 않아도 인덱스이므로 기본 키를 기반으로 매우 효율적으로 객체를 쿼리 할 수 있습니다.

기본 키를 설정하는 방법은 사용하는 언어 바인딩의 문서를 참조하세요.

인덱스

속성에 인덱스 를 추가하면 여러 쿼리의 속도가 크게 향상됩니다. 속성이 같은지 자주 비교하는 경우, 즉 이메일 주소와 같이 완벽히 일치하는 객체를 찾는 경우라면 인덱스를 추가하는 편이 좋습니다. name IN {'Bob', 'Agatha', 'Fred'}와 같이 정확히 특정 단어를 “포함”하는 연산자로 쿼리 하는 경우에도 인덱스를 추가하면 속도가 빨라집니다.

인덱스 설정 방법에 대해서는 사용하는 언어 바인딩의 문서를 참조하세요.

마이그레이션

Realm의 데이터 모델은 표준 클래스로 정의되므로 모델은 쉽게 변경할 수 있습니다. 아래와 같은 속성을 지닌 Person 모델을 보겠습니다.

class Person: Object {
	dynamic var firstName = ""
	dynamic var lastName = ""
	dynamic var age = 0
}

firstNamelastName 속성을 단일 name 속성으로 통합하고 싶은 경우 다음처럼 간단하게 모델을 변경할 수 있습니다.

class Person: Object {
	dynamic var name = ""
	dynamic var age = 0
}

하지만 Realm 파일의 스키마가 모델과 일치하지 않으므로 기존 Realm을 사용하려고 하면 에러가 발생합니다. 이를 해결하려면 마이그레이션을 수행해야 합니다. 즉, 이전 버전의 Realm 스키마를 감지하고 디스크에서 업그레이드하는 간단한 코드를 애플리케이션에 추가해야 합니다.

로컬 Realm 마이그레이션

마이그레이션 수행 방법은 언어마다 다르지만 기본 개념은 비슷합니다.

  • Realm을 열 때 설정 객체 안에서 스키마 버전과 마이그레이션 함수를 생성자에 전달합니다.
  • 기존 스키마 버전이 생성자가 받은 스키마 버전과 같은 경우 정상적으로 진행합니다. (마이그레이션을 수행하지 않습니다.)
  • 기존 스키마 버전이 생성자가 받은 스키마 버전보다 낮은 경우 마이그레이션 함수를 호출합니다.

Swift에서 설정 객체에 추가되는 마이그레이션 함수는 다음과 같습니다.

let config = Realm.Configuration(
	schemaVersion: 1,
	migrationBlock: { migration, oldSchemaVersion in
		if (oldSchemaVersion < 1) {
			migration.enumerateObjects(ofType: Person.className()) { oldObject, newObject in
				let firstName = oldObject!["firstName"] as! String
				let lastName = oldObject!["lastName"] as! String
				newObject!["name"] = "\(firstName) \(lastName)"
			}
		}
	})

스키마에 버전이 설정되지 않은 경우 기본값은 0입니다.

동기 Realm 마이그레이션

Realm을 동기화하는 경우에는 마이그레이션 수행 규칙이 조금 달라집니다.

  • 추가적인 변경 사항은 자동으로 적용됩니다. 예를 들어 클래스를 추가하거나 기존 클래스에 속성을 추가하는 경우입니다.
  • 스키마에서 속성을 제거하는 경우 데이터베이스의 필드를 삭제하지 않지만, Realm에 해당 속성을 무시하도록 지시합니다. 새로운 객체는 이 속성을 계속 만들지만 값이 null로 설정됩니다. nullable이 아닌 필드는 타입에 맞게 0이나 빈 값으로 설정됩니다. 숫자 필드의 경우 0, 문자열 속성의 경우 빈 문자열이 설정됩니다.
  • 사용자 정의 마이그레이션 함수는 동기 Realm 마이그레이션에서 호출할 수 없고, 하는 경우 예외가 발생합니다.
  • 파괴적인 변경은 직접 지원되지 않습니다. 파괴적인 변경이란 해당 Realm과 상호 작용하는 코드에 변경 사항을 적용해서 Realm 스키마를 변경하는 것입니다. 예를 들어 nullable 문자열을 nullable이 아닌 문자열로 변경하는 것과 같이 같은 이름의 속성 타입을 변경하거나, 기본 키를 변경하거나, 필드를 옵셔널에서 필수로 혹은 필수에서 옵셔널로 변경하는 경우입니다.

사용자 정의 함수를 사용할 수 없으므로 동기 Realm에서 앞서 말한 경우와 같은 스키마 변경은 다음 두 방식을 사용해야 합니다.

  • 클라이언트 측에서 알림 핸들러를 작성해서 변경합니다.
  • 서버 측에서 Node.js 함수를 작성해서 변경합니다.

위 방법으로 기존 Realm에 파괴적인 변경을 직접 할 수는 없습니다. 대신 새 스키마로 새로운 동기 Realm을 만들고 기존 Realm의 변경을 수신해서 새 Realm으로 값을 복사하는 함수를 작성하세요. 이 함수는 클라이언트나 서버에서 만들 수 있습니다.

마이그레이션 정보

사용하는 언어 바인딩에서 마이그레이션의 문서에서 예제를 포함한 구체적인 정보를 볼 수 있습니다.