최신 영문 문서를 보시려면 이 곳을 참고하세요.

시작하기

설치

  1. Realm 최신 버전을 다운로드하고 압축을 풉니다.
  2. ios/ 또는 osx/디렉터리(대상 플랫폼에 따라 선택)에서 Realm.framework을 선택하여 Xcode 프로젝트의 File Navigator에 넣습니다. 이때, 선택된 대상 그룹의 폴더에 파일이 복사 되는 것을 꼭 확인하시고, Finish 버튼을 누릅니다.
  3. Xcode의 File Navigator에서 프로젝트를 클릭합니다. 어플리케이션 대상을 선택하고 Build Phases 탭으로 이동합니다. Link Binary with Libraries의 +를 클릭하여 libc++.dylib를 추가합니다.
  4. Swift와 함께 사용한다면 Swift/RLMSupport.swift 파일을 Xcode의 File Navigator에 넣으신 후 Copy items if needed를 선택합니다.
  5. OSX 프로젝트에서 사용한다면 +를 클릭하여 New Copy Files Phase를 선택합니다. 해당 phase를 Copy Framework로 이름을 변경하고 DestinationFramework로 설정한 후 Realm.framework를 추가합니다.
  1. Podfile에 pod "Realm" 문장을 앱 타깃에 추가하고 pod "Realm.Headers" 문장을 테스트 타깃에 추가합니다. (“Realm 앱 테스트하기”)
  2. 커맨드라인 프롬프트에서, pod install를 실행합니다.
  3. 프로젝트에 CocoaPods을 통해 생성된 .xcworkspace파일을 사용합니다.
  4. Swift를 사용한다면 추가적인 작업을 진행합니다.
    • 최신 버전의 Realm을 다운로드 받고 압축을 풉니다.
    • Drag the file at Swift/RLMSupport.swift into the File Navigator of your
    • Swift/RLMSupport.swift 파일을 마우스로 끌어 Xcode 프로젝트의 File Navigator에 추가하고 Copy items if needed 옵션을 선택합니다.

자세한 내용은 CocoaPods 웹사이트에서 확인하세요.

  1. 최신 버전의 Realm을 다운로드 받고 압축을 풉니다.
  2. ios/ 폴더에서 Realm-dynamic.frameworkRealm-dynamic-simulator.framework 파일을 Xcode 프로젝트의 File Navigator에 추가합니다. Copy items if needed 옵션이 선택되어 있는지 확인한 후 Finish를 선택합니다.
  3. 앱 타깃의 Build Phases 탭에서 패널의 좌측 상단 + 버튼을 클릭하고 New Copy Files Phase를 선택합니다. Copy Frameworks로 이름을 변경하고 DestinationFrameworks로 설정한 후 Realm-dynamic.framework를 추가합니다.

주의: 동적 프레임워크는 iOS 8 프로젝트 전용입니다.

  1. github "realm/realm-cocoa"를 Cartfile에 추가합니다.
  2. carthage update를 실행합니다.
  3. Swfit를 사용한다면 추가적인 작업을 진행합니다.
    • 최신 버전의 Realm을 다운로드 받고 압축을 풉니다.
    • Drag the file at Swift/RLMSupport.swift into the File Navigator of your
    • Swift/RLMSupport.swift 파일을 마우스로 끌어 Xcode 프로젝트의 File Navigator에 추가하고 Copy items if needed 옵션을 선택합니다.

자세한 내용은 Carthage 프로젝트에서 확인하세요.

Xcode 플러그인

제공되는 Xcode 플러그인은 새로운 Realm 모델을 쉽게 만들 수 있도록 도와줍니다.

Realm의 Xcode 플러그인을 설치하는 가장 쉬운 방법은 Alcatraz를 이용하여 “RealmPlugin”을 설치하는 방법입니다. 물론 릴리즈를 통해 제공하는 plugin/RealmPlugin.xcodeproj을 열어서 직접 설치하여 빌드도 가능합니다. 설치된 플러그인을 확인하기 위해서는 Xcode 재실행이 필요합니다. Xcode 메뉴에서 새 파일을 생성 (File > New > File… — or ⌘N) 할 때, 새 Realm 모델을 생성하는 옵션을 볼 수 있습니다.

Realm 브라우저

또한 Realm 데이터베이스를 읽고 수정할 수 있는 앱을 제공하고 있습니다. 릴리즈browser/아래에서 확인 할 수 있습니다.

Tools > Generate demo database 메뉴를 통해서 테스트용 데이터베이스와 더미 데이터를 생성할 수 있습니다.

API 레퍼런스

전체 API 레퍼런스에서 모든 클래스와 메소드 등의 정보를 찾아보세요.

예제

릴리즈examples/에서 Objective‑C, Swift 그리고 RubyMotion 어플리케이션의 예제를 확인 할수 있습니다. 예제에는 마이그레이션, UITableViewController의 사용법, 암호화, 명렁어 툴 및 그 이외의 Realm의 여러 기능 사용법을 포함하고 있습니다.

도움을 얻으려면

  • 커뮤니티 뉴스레터에 가입하여 일반적인 팁을 얻거나, 다른 사례를 배우고 Realm과 관련된 블로그 포스팅이나 튜토리얼 등을 확인 할 수 있습니다.
  • StackOverflow: #realm 태그가 포함된 이전 질문 등을 찾거나 새로운 질문을 등록할 수 있습니다.
  • Twitter: 공식 계정 @realm에 연락 하거나 또는 #realm 태그를 이용할 수 있습니다.
  • Email: docs/cocoa/latest [email protected].

모델

Realm 데이터 모델은 @property로 일반적인 Objective‑C 인터페이스를 사용하여 정의할 수 있습니다. 간단하게 서브클래스 RLMObject나 존재하는 모델 클래스를 Realm 데이터 모델 객체로 만들 수 있습니다. Realm 모델 객체는 다른 Objective‑C의 객체와 기능이 대체로 같습니다. 자신만의 메소드와 프로토콜을 추가할 수 있고 다른 객체와 동일하게 사용할 수 있습니다. 주요 제한은 생성한 쓰레드 안에서만 사용할수 있다는 점과 인스턴스 변수에 직접적으로 다른 관련된 속성에 접근할 수 없다는 점입니다.

만약 이미 제공 중인 Xcode 플러그인을 설치하였다면 “New File…” 다이얼로그를 통해서 인터페이스와 동작하는 파일의 템플릿을 확인 할 수 있습니다.

관계와 자료구조는 타깃 타입의 속성이나 RLMArray의 객체 리스트를 포함하여 간단하게 정의 할 수 있습니다.

#import <Realm/Realm.h>

@class Person;

// Dog model
@interface Dog : RLMObject
@property NSString *name;
@property Person   *owner;
@end
RLM_ARRAY_TYPE(Dog) // define RLMArray<Dog>

// Person model
@interface Person : RLMObject
@property NSString      *name;
@property NSDate        *birthdate;
@property RLMArray<Dog> *dogs;
@end
RLM_ARRAY_TYPE(Person) // define RLMArray<Person>

// Implementations
@implementation Dog
@end // none needed

@implementation Person
@end // none needed
import Realm

// Dog model
class Dog: RLMObject {
    dynamic var name = ""
    dynamic var owner: Person? // Can be optional
}

// Person model
class Person: RLMObject {
    dynamic var name = ""
    dynamic var birthdate = NSDate(timeIntervalSince1970: 1)
    dynamic var dogs = RLMArray(objectClassName: Dog.className())
}

실행할 때 Realm이 코드에 명시된 모든 모델을 분석하므로 사용하지 않는 모델이더라도 문법적으로 유효해야 합니다.

더 자세한 내용은 RLMObject에서 확인하세요.

속성 타입

Realm이 지원하는 속성 타입은 다음과 같습니다: BOOL, bool, int, NSInteger, long, long long, float, double, CGFloat, NSString, NSDate, NSData.

모델 관계를 지정하기 위해 RLMArray<Object>RLMObject를 사용할 수 있습니다. RLMObject 클래스의 상속도 지원합니다.

속성 특성

주의, Realm은 Objective‑C의 속성 특성 중에 nonatomic, atomic, strong, copy, weak, 등을 무시합니다. Realm은 내부적으로 이미 최적화된 스토리지 방법론을 가지고 있기 때문입니다. 그래서 오해의 소지를 피하기 위해서 이러한 속성의 특성을 모델에 사용하지 않을 것을 권고합니다. 그러나 만약 속성 특성을 설정하였다면 한 영역에 RLMObject을 추가하기 전까지는 사용할 수 있습니다. 사용자 지정의 getter와 setter는 문제 없이 사용 할 수 있지만 한 영역의 RLMObject에서는 사용이 불가능합니다.

사용자 지정 모델

모델 정보를 추가적으로 지정하기 위해 다음과 같은 클래스 메소드가 있습니다:

  • +attributesForProperty: 속성 타입의 인덱싱 설정과 같이 속성 특성을 제공하기 위해 재정의할 수 있습니다.
  • +defaultPropertyValues 객체가 생성될 때마다 초기값을 제공하기 위해 재정의할 수 있습니다.
  • +primaryKey 해당 모델의 기본키 설정을 위해 재정의할 수 있습니다. 기본키를 지정하면 효율적으로 객체를 검색 혹은 수정할 수 있고 고유값으로 설정할 수 있습니다.
  • ignoredProperties persisting model properties로부터 Realm을 유지하기 위해 재정의할 수 있습니다.

쓰기

모든 객체의 변경(추가, 수정, 삭제)는 쓰기 트랜잭션 내에서 처리됩니다.

Realm 객체는 보통의 객체와 같이 단독적으로 값을 지정하고 사용이 가능합니다. 쓰레드 사이에 객체를 공유하거나 실행 중인 앱 간에 재사용을 하기 위해서는 쓰기 트랜잭션 내에서 수행되는 작업으로 Realm에 객체를 저장해야만 합니다. 다음과 같은 방법으로 Realm에 객체를 추가할 수 있습니다:

// Create object
Person *author = [[Person alloc] init];
author.name    = @"David Foster Wallace";

// Get the default Realm
RLMRealm *realm = [RLMRealm defaultRealm];
// You only need to do this once (per thread)

// Add to Realm with transaction
[realm beginWriteTransaction];
[realm addObject:author];
[realm commitWriteTransaction];
// Create a Person object
let author = Person()
author.name = "David Foster Wallace"

// Get the default Realm
let realm = RLMRealm.defaultRealm()
// You only need to do this once (per thread)

// Add to the Realm inside a transaction
realm.beginWriteTransaction()
realm.addObject(author)
realm.commitWriteTransaction()

Realm에서 객체를 생성한 후에는 그 객체는 지속적으로 사용할 수 있습니다. 그리고 그것을 구성하는 모든 변경사항도 지속적으로 사용가능합니다. (반드시 쓰기 트랜잭션 내에서 이루어져야 합니다.) 쓰기 트랜잭션이 커밋되는 시점에 같은 Realm 객체를 사용하는 서로 다른 쓰레드에 변경사항이 반영됩니다.

이 점은 주의하시기 바랍니다. 동시에 각각의 블럭에 여러 쓰기가 진행 중이라면 해당 쓰레드는 블럭이 됩니다. 이 부분은 여러 다른 범용 솔루션과 비슷합니다. 그래서 이런 경우에는 적절한 사례를 살펴보길 추천합니다. 즉, 쓰기에 대해서 쓰레드를 나누는 것을 말합니다.

Realm의 MVCC 설계 덕분에 쓰기 트랜잭션이 실행 중에는 읽기는 제한되지 않습니다. 한 번에 여러 쓰레드에서 동시 쓰기를 해야하지 않는 이상, 대량의 쓰기 트렌잭션으로 구성하기 보다는 다수의 세세한 쓰기 트랜잭션으로 구성하길 선호하실 겁니다.

자세한 내용은 RLMRealmRLMObject를 확인하세요.

쿼리

쿼리는 RLMObject 콜렉션을 포함하는 RLMResults 객체를 반환합니다. RLMResults는 NSArray와 매우 유사한 인터페이스를 제공하고 RLMResult에 포함된 객체는 인덱스 첨자로 접근할 수 있습니다. NSArray와 달리, RLMResults는 타입을 강제하고 하나의 RLMObject 서브클래스를 갖도록만 설계되었습니다. 자세한 내용은 RLMResults를 참조하세요.

모든 쿼리 (쿼리와 속성 접근을 포함하여)는 바로 처리되지 않습니다. 속성에 접근할 때에만 데이터를 읽습니다.

쿼리에 대한 결과는 데이터의 복사본이 아닙니다: 쿼리 결과의 수정(쓰기 트랜잭션과 함께)은 직접적으로 디스크에 반영합니다. 동일하게, RLMResult 내에 있는 RLMObject로부터 직접적으로 관계그래프를 탐색할 수 있습니다.

타입을 통한 객체 검색

Realm에서 가장 기본적인 객체 검색 메소드는 [RLMObject allObjects]이고, 이는 기본 Realm으로부터 같은 서브클래스 타입의 모든 RLMObject 인스턴스의 RLMResults를 반환합니다.

// Query the default Realm
RLMResults *dogs = [Dog allObjects]; // retrieves all Dogs from the default Realm

// Query a specific Realm
RLMRealm *petsRealm = [RLMRealm realmWithPath:@"pets.realm"]; // get a specific Realm
RLMResults *otherDogs = [Dog allObjectsInRealm:petsRealm]; // retrieve all Dogs from that Realm
// Query the default Realm
let dogs = Dog.allObjects()

// Query a specific Realm
let petsRealm = RLMRealm(path: "pets.realm")
let otherDogs = Dog.allObjectsInRealm(petsRealm)

조건 쿼리

만약에 이미 NSPredicate에 익숙하다면, Realm에서 조회하는 방법을 이미 알고 있습니다. RLMObject, RLMRealm, RLMArray 그리고 RLMResults 모두 NSArray를 조회했던 것과 같이 간단하게 NSPredicate 인스턴스, 조건 문자열이나 조건 형식의 문자열 전달로 특정 RLMObject 인스턴스를 조회할 수 있는 메소드를 제공합니다.

예를 들면, 아래는 이전의 예시로부터 확장하여 [RLMObject objectsWhere:]를 호출하여, 기본 Realm으로 부터 색이 ‘tan’이고 이름이 ‘B’로 시작하는 모든 dog를 검색할 수 있습니다:

// Query using a predicate string
RLMResults *tanDogs = [Dog objectsWhere:@"color = 'tan' AND name BEGINSWITH 'B'"];

// Query using an NSPredicate object
NSPredicate *pred = [NSPredicate predicateWithFormat:@"color = %@ AND name BEGINSWITH %@",
                                                     @"tan", @"B"];
tanDogs = [Dog objectsWithPredicate:pred];
// Query using a predicate string
var tanDogs = Dog.objectsWhere("color = 'tan' AND name BEGINSWITH 'B'")

// Query using an NSPredicate object
let predicate = NSPredicate(format: "color = %@ AND name BEGINSWITH %@", "tan", "B")
tanDogs = Dog.objectsWithPredicate(predicate)

애플의 Predicates Programming Guide에서 조건절에 대한 상세한 정보를 얻을 수 있습니다. Realm은 일반적인 조건절을 지원합니다:

  • 비교 연산자는 속성 이름 또는 피연산자가 될 수 있습니다. 피연산자 중 적어도 하나는 속성의 이름이어야 합니다.
  • 비교 연산자 ==, <=, <, >=, >, !=, BETWEEN는 int, long, long long, float, double, NSDate 속성타입을 지원합니다. 예. age == 45
  • 비교 연산자 ==, !=, 예. [Employee objectsWhere:@”company == %@”, company]
  • 비교 연산자 ==, !=는 bool 속성을 지원합니다.
  • NSString, NSData 속성을 위해서 ==, !=, BEGINSWITH, CONTAINS, ENDSWITH 연산자를 지원합니다. 예. name CONTAINS ‘Ja’
  • 대소문자를 구분하지 않는 string 비교. 예. name CONTAINS[c] ‘Ja’ 알림. “A-Z”, “a-z”만이 대소문자 구분에서 제외됩니다.
  • Realm은 다음의 연산자 또한 지원합니다: “AND”, “OR”, “NOT”. 예. name BEGINSWITH ‘J’ AND age >= 32
  • 포함 연산자 IN. 예. name IN {‘Lisa’, ‘Spike’, ‘Hachi’}
  • Nil 비교 ==, !=. 예. [Company objectsWhere:@”ceo == nil”]. 객체를 대상으로한 관계에서만 유효합니다. 예를 들어, ceo is a property on the Company model.
  • ANY 비교. 예. ANY student.age < 21
  • 알림, 집합 표현 타입을 지원하지는 않지만 객체 값을 이용한 BETWEEN 연산자를 지원합니다. 예. RLMResults *results = [Person objectsWhere:@"age BETWEEN %@", @[42, 43]];

좀 더 상세한 내용은 [RLMObject objectsWhere:]을 통해 확인하세요.

결과값 정렬

RLMResults는 하나 혹은 여러 개의 속성으로 정렬 기준이나 순서를 설정하도록 지원합니다. 예를 들어, 아래는 [RLMObject objectsWhere:where:]의 결과값을 이름 알파벳순으로 정렬하는 코드입니다.

// Sort tan dogs with names starting with "B" by name
RLMResults *sortedDogs = [[Dog objectsWhere:@"color = 'tan' AND name BEGINSWITH 'B'"]
                               sortedResultsUsingProperty:@"name" ascending:YES];
// Sort tan dogs with names starting with "B" by name
var sortedDogs = Dog.objectsWhere("color = 'tan' AND name BEGINSWITH 'B'").sortedResultsUsingProperty("name", ascending: true)

[RLMObject objectsWhere:][RLMResults sortedResultsUsingProperty:ascending:]을 통해 상세 내용을 확인하세요.

연속 조회

Realm이 내세우는 조회 엔진의 장점은 각각의 연속적인 쿼리에 대한 데이터베이스 서버에 별도의 연결을 요구하는 기존의 데이터베이스와 비교하여 매우 적은 오버헤드로 연속 조회하는 기능입니다.

예를 들어, 만약에 ‘tan’색의 dog를 조회한후, ‘tan’색의 dog 중에서 이름이 ‘B’로 시작하는 dog를 찾기 원할 때 아래와 같이 연결된 쿼리로 조회가 가능합니다:

RLMResults *tanDogs = [Dog objectsWhere:@"color = 'tan'"];
RLMResults *tanDogsWithBNames = [tanDogs objectsWhere:@"name BEGINSWITH 'B'"];
let tanDogs = Dog.objectsWhere("color = 'tan'")
let tanDogsWithBNames = tanDogs.objectsWhere("name BEGINSWITH 'B'")

Realm

기본 Realm

항상 [RLMRealm defaultRealm]을 호출하여 realm 변수를 초기화하여 접근했습니다. 해당 메소드는 앱 내부 Documents 폴더 안에 “default.realm”에 대응되는 RLMRealm 객체를 반환합니다.

기본 Realm을 대상으로 사용자 지정 경로를 지정하고 싶다면 +[RLMRealm setDefaultRealmPath:]으로 설정할 수 있습니다. 앱을 테스트하거나 iOS 8 Shared Container으로 앱 간에 Realm을 공유할 때에 특히 유용합니다.

많은 Realm API 메소드는 RLMRealm 객체를 받는 버전과 기본 Realm을 사용하는 간편한 버전이 있습니다. 예를 들어, [RLMObject allObjects][RLMObject allObjectsInRealm:[RLMRealm defaultRealm]]는 동일합니다.

다른 Realm

간혹 여러 개의 Realm을 두는게 유용할 수도 있습니다. 예를 들어, 어플리케이션과 함께 파일을 모아놓을 필요가 있다면 읽기 전용 Realm을 하나 더 두어 분리할 수 있습니다. [RLMRealm realmWithPath:][RLMRealm realmWithPath:readOnly:error:]에서 좀 더 자세한 정보를 확인하세요.

[RLMRealm realmWithPath:]에 전달하는 파일 경로는 읽기 권한이 부여되어야 하니 주의하세요. Realm 파일을 저장하기에 가장 일반적인 경로는 iOS의 경우 “Documents”이며 OSX의 경우 “Application Support” 입니다. 다시 생성될 수 있는 파일은 <Application_Home>/Library/Caches 경로를 추천합니다. Apple iOS Data Storage 가이드라인을 따르세요.

In-Memory Realm

Realm은 기본적으로 디스크에 유지됩니다. 그러나 [RLMRealm inMemoryRealmWithIdentifier:] 팩토리 메소드를 사용해 순수하게 메모리에서 사용 할 수 있습니다.

RLMRealm *realm = [RLMRealm inMemoryRealmWithIdentifier:@"MyInMemoryRealm"];
let realm = RLMRealm.inMemoryRealmWithIdentifier("MyInMemoryRealm")

In-memory Realm은 영구적으로 데이터를 저장하지 않습니다. 하지만 조회, 관계, 쓰레드 보호를 포함한 Realm의 모든 기능이 동일하게 동작합니다. 디스크 영구보존을 위한 오버헤드를 피하려 한다면 유용한 옵션입니다.

주의: In-memory Realm의 대한 참조가 범위를 벗어나게 되면 모든 데이터는 메모리 해제됩니다. 앱이 구동되는 동안 In-memory Realm에 메모리 참조를 하도록 추천합니다.

하나의 Realm을 쓰레드 간에 공유하여 사용

여러 쓰레드에서 동일한 Realm 파일에 접근하기 위해서는 쓰레드마다 다른 Realm 객체를 두어야 하고 [RLMRealm defaultRealm], [RLMRealm realmWithPath:][RLMRealm realmWithPath:readOnly:error:]를 호출하셔야 합니다. 같은 경로를 지정할 경우에는, 모든 RLMRealm 객체는 디스크상의 같은 파일에 대응됩니다.

쓰레드 간에 Realm 객체 공유를 지원하지 않습니다. 같은 Realm 파일에 접근하는 RLMRealm 객체는 모두 같은 readOnly 값이어야 합니다. (혹은 모두 같은 readwrite이거나 모두 readonly).

하나의 트랜잭션 내에서 동시에 여러 쓰기를 일괄 처리하여 많은 양의 데이터를 기록 할 때 Realm은 상당히 효율적입니다. 트랜잭션은 또한 메인 쓰레드를 블록킹하지 않도록 Grand Central Dispatch를 사용하여 백그라운드로 수행할 수 있습니다. RLMRealm 객체는 쓰레드로부터 안전하지 않은 쓰레드를 통해 공유 할 수 없습니다. 그래서 읽거나 쓰고자 하는 각 쓰레드/dispatch_queue에 RLMRealm 인스턴스를 받아야 합니다. 아래는 백그라운드 큐에 백만 개의 개체를 삽입하는 예입니다:

dispatch_async(queue, ^{    
    
  // Get realm and table instances for this thread
  RLMRealm *realm = [RLMRealm defaultRealm];
  
  // Break up the writing blocks into smaller portions
  // by starting a new transaction
  for (NSInteger idx1 = 0; idx1 < 1000; idx1++) {
    [realm beginWriteTransaction];
    
    // Add row via dictionary. Property order is ignored.
    for (NSInteger idx2 = 0; idx2 < 1000; idx2++) {
      [Person createInRealm:realm
                 withObject:@{@"name"      : [self randomString],
                              @"birthdate" : [self randomDate]}];
    }

    // Commit the write transaction
    // to make this data available to other threads
    [realm commitWriteTransaction];
  }
});
dispatch_async(queue) {
  // Get realm and table instances for this thread
  let realm = RLMRealm.defaultRealm()

  // Break up the writing blocks into smaller portions
  // by starting a new transaction
  for idx1 in 0..<1000 {
    realm.beginWriteTransaction()

    // Add row via dictionary. Property order is ignored.
    for idx2 in 0..<1000 {
      Person.createInDefaultRealmWithObject([
        "name": "\(idx1)",
        "birthdate": NSDate(timeIntervalSince1970: idx2)
      ])
    }

    // Commit the write transaction
    // to make this data available to other threads
    realm.commitWriteTransaction()
  }
}

Realm 간에 객체 복사하기

원본 객체를 +[RLMObject createInRealm:withObject:]에 넘겨주는 것만으로 다른 Realm에 Realm 객체를 손쉽게 복사할 수 있습니다. 예를 들어, [MyRLMObjectSubclass createInRealm:otherRealm withObject:originalObjectInstance].

Realm과 앱을 같이 배포하기

초기 데이터와 함께 앱을 배포하는 건 흔한 일이고 초기 실행 때 가능하게 하는 방법이 여기 있습니다:

  1. 우선, Realm을 옮깁니다. 같은 데이터 모델을 사용해야 하고 Realm을 생성하고 해당 앱에 초기화하길 원하는 데이터를 담은 후 배포합니다. Realm 파일은 플랫폼 간에 호환이 가능하기 때문에 OS X 앱(JSONImport 예제)(https://github.com/realm/realm-cocoa/tree/master/examples/osx/objc/JSONImport)이나 시뮬레이터에 띄워진 앱을 사용할 수 있습니다.
  2. Realm 파일을 생성한 코드에서 해당 파일의 압축된 복사본을 만들어야 합니다. (-[RLMRealm writeCopyToPath:error:]). 해당 메소드는 Realm 파일 크기를 줄이고 사용자에게 전달될 최종 버전을 경량화 시킵니다.
  3. 새로 생성된 해당 복사본을 최종 앱의 Xcode Project Navigator에 마우스로 끌어 추가합니다.
  4. Xcode에서 앱 타깃의 Build Phases 탭으로 간 후 Realm 파일을 “Copy Bundle Resources”에 추가합니다.
  5. 이때, 같이 배포된 Realm 파일은 접근 가능해집니다. [[NSBundle mainBundle] pathForResource:ofType:] 메소드 사용으로 파일 경로를 확인할 수 있습니다.
  6. [RLMRealm realmWithPath:readOnly:error:] 메소드 호출로 읽기 전용 Realm을 만들 수도 있습니다. 혹은, 초기 데이터를 바탕으로 쓰기가 가능한 Realm 파일을 만들고 싶다면, [[NSFileManager defaultManager] copyItemAtPath:toPath:error:] 메소드 호출을 통해 Documents 폴더에 배포된 파일을 복사할 수 있습니다. 그리고 새 Realm을 생성하세요. [RLMRealm realmWithPath:]

배포된 Realm 파일을 어떻게 다루는지 예제를 보고 싶다면 마이그레이션 예제 앱에서 확인하세요.

관계

RLMObject는 RLMObject과 RLMArray<Object> 속성 사용으로 서로 연결할 수 있습니다. RLMArray는 NSArray와 매우 유사한 인터페이스를 제공하고 RLMArray에 포함된 객체는 인덱스 첨자로 접근할 수 있습니다. NSArray와 달리, RLMArray는 타입을 강제하고 하나의 RLMObject 서브클래스를 갖도록만 설계되었습니다. 자세한 내용은 RLMArray를 참조하세요.

Person 모델()이 정의되었고 Dog 모델을 같이 만들어 봅니다:

// Dog.h
@interface Dog : RLMObject
@property NSString *name;
@end
class Dog: RLMObject {
    dynamic var name = ""
}

대 일(To-One)

다 대 일 혹은 일 대 일 관계를 대상으로 간단하게 RLMObject 서브클래스 타입으로 속성을 정의합니다:

// Dog.h
@interface Dog : RLMObject
... // other property declarations
@property Person *owner;
@end
class Dog: RLMObject {
    ... // other property declarations
    dynamic var owner: Person?
}

다른 속성과 마찬가지로 이 속성도 사용할 수 있습니다.

Person *jim = [[Person alloc] init];
Dog    *rex = [[Dog alloc] init];
rex.owner = jim;
let jim = Person()
let rex = Dog()
rex.owner = jim

RLMObject 속성을 사용할 때, 일반 속성 문법으로 중첩 속성을 접근할 수 있습니다. 예를 들어 rex.owner.address.country 이 예제는 객체그래프를 탐색하고 자동적으로 Realm으로부터 필요한 객체를 반환합니다.

대 다(To-Many)

RLMArray<Object> 속성으로 대 다(To-Many) 관계를 정의할 수 있습니다. RLMArray는 하나의 RLMObject 타입을 갖고 NSMutableArray와 유사한 인터페이스를 제공합니다.

“dogs” 속성을 Person 모델에 추가하여 복수의 dogs와 연결하려면 RLMArray<Dog>타입을 먼저 정의해야 합니다. 이것은 모델 인터페이스에 해당하는 아래의 매크로를 통해 완료됩니다.

//Dog.h
@interface Dog : RLMObject
... // property declarations
@end

RLM_ARRAY_TYPE(Dog) // Defines an RLMArray<Dog> type
// Not needed in Swift

RLMArray<Dog>의 속성을 정의할 수 있습니다:

// Person.h
@interface Person : RLMObject
... // other property declarations
@property RLMArray<Dog> *dogs;
@end
class Person: RLMObject {
    ... // other property declarations
    dynamic var dogs = RLMArray(objectClassName: Dog.className())
}

RLMArray 속성에 이와 같이 접근하고 할당 할 수 있습니다:

// Jim is owner of Rex and all dogs named "Fido"
RLMArray *someDogs = [Dog objectsWhere:@"name contains 'Fido'"];
[jim.dogs addObjects:someDogs];
[jim.dogs addObject:rex];
let someDogs = Dog.objectsWhere("name contains 'Fido'")
jim.dogs.addObjects(someDogs)
jim.dogs.addObject(rex)

역관계

역관계(backlinks로도 알려진)로 특정 속성을 통해 주어진 객체와 연결된 모든 객체를 조회할 수 있습니다. 예를 들어, Dog 객체에서 -linkingObjectsOfClass:forProperty:를 호출하면 지정한 속성과 특정 클래스에 연결된 모든 객체를 반환합니다.

@interface Dog : RLMObject
@property NSString *name;
@property NSInteger age;
@property (readonly) NSArray *owners; // Realm doesn't persist this property because it is readonly
@end

@implementation Dog
// Define "owners" as the inverse relationship to Person.dogs
- (NSArray *)owners {
    return [self linkingObjectsOfClass:@"Person" forProperty:@"dogs"];
}
@end
class Dog: RLMObject {
    dynamic var name = ""
    dynamic var age = 0
    var owners: [Person] {
        // Realm doesn't persist this property because it only has a getter defined
        // Define "owners" as the inverse relationship to Person.dogs
        return linkingObjectsOfClass("Person", forProperty: "dogs") as [Person]
    }
}

알림

Realm 객체는 쓰기 트랜잭션이 커밋될 때마다 여러 쓰레드의 다른 객체에 알림을 보냅니다. 이러한 알림은 블록을 등록하여 구독할 수 있습니다:

// Observe Realm Notifications
self.token = [realm addNotificationBlock:^(NSString *note, RLMRealm * realm) {
    [myViewController updateUI];
}];
// Observe Realm Notifications
let token = realm.addNotificationBlock { note, realm in
    viewController.updateUI()
}

반환되는 알림 토큰에 레퍼런스를 참조하는 동안 만큼 알림은 구독됩니다. 토큰이 메모리 해제되었을 때 자동적으로 알림이 구독 취소되므로 업데이트를 등록한 클래스 내의 토큰에 참조를 걸어두세요.

보다 자세한 내용은 [Realm addNotificationBlock:][Realm removeNotificationBlock:] 을 확인하세요.

마이그레이션

다른 어떤 데이터베이스로 작업할 때와 마찬가지로 데이터 모델은 시간이 지남에 따라 변경됩니다. 표준 Objective‑C 인터페이스로 Realm의 데이터 모델을 정의하고 다른 Objective‑C 인터페이스를 수정하는 것처럼 모델을 수정하기 간편합니다. 예를 들어, 아래와 같은 ‘Person.h’가 있다고 가정합니다:

@interface Person : RLMObject
@property NSString *firstName;
@property NSString *lastName;
@property int age;
@end
class Person: RLMObject {
    dynamic var firstName = ""
    dynamic var lastName = ""
    dynamic var age = 0
}

성과 이름을 따로 나누기 보다 ‘fullName’ 속성을 추가하길 바랍니다. 이러한 작업을 위해 다음과 같이 간단하게 객체 인터페이스를 수정합니다:

@interface Person : RLMObject
@property NSString *fullName;
@property int age;
@end
class Person: RLMObject {
    dynamic var fullName = ""
    dynamic var age = 0
}

이 시점에 어떠한 데이터라도 저장했다면 코드에 정의된 모델과 디스크에 기록된 모델 사이에 불일치가 발생합니다. 이러한 상황이 발생할 때 마이그레이션이 진행되지 않고 예외 처리됩니다.

마이그레이션 실행

마이그레이션을 정의하고 [RLMRealm setSchemaVersion:forRealmAtPath:withMigrationBlock:] 호출로 관련된 버전을 정의합니다. 마이그레이션 블럭은 이전 스키마에서 새로운 스카마로 데이터 모델을 변경하는 모든 로직을 제공합니다. [RLMRealm setSchemaVersion:forRealmAtPath:withMigrationBlock:] 호출한 후, 마이그레이션이 필요한 모든 Realm은 자동으로 제공된 버전으로 수정된 마이그레이션 블럭을 적용합니다.

예를 들어, 상단의 Person 서브클래스를 마이그레이션한다고 가정합니다. 최소한의 필요한 마이그레이션 블럭은 아래와 같습니다.

// Inside your [AppDelegate didFinishLaunchingWithOptions:]

// Notice setSchemaVersion is set to 1, this is always set manually. It must be
// higher than the previous version (oldSchemaVersion) or an RLMException is thrown
[RLMRealm setSchemaVersion:1
            forRealmAtPath:[RLMRealm defaultRealmPath] 
        withMigrationBlock:^(RLMMigration *migration, NSUInteger oldSchemaVersion) {
  // We haven’t migrated anything yet, so oldSchemaVersion == 0
  if (oldSchemaVersion < 1) {
    // Nothing to do!
    // Realm will automatically detect new properties and removed properties
    // And will update the schema on disk automatically
  }
}];

// now that we have called `setSchemaVersion:withMigrationBlock:`, opening an outdated
// Realm will automatically perform the migration and opening the Realm will succeed
[RLMRealm defaultRealm];
// Inside your application(application:didFinishLaunchingWithOptions:)

// Notice setSchemaVersion is set to 1, this is always set manually. It must be
// higher than the previous version (oldSchemaVersion) or an RLMException is thrown
RLMRealm.setSchemaVersion(1, forRealmAtPath: RLMRealm.defaultRealmPath(),
                         withMigrationBlock: { migration, oldSchemaVersion in
  // We haven’t migrated anything yet, so oldSchemaVersion == 0
  if oldSchemaVersion < 1 {
    // Nothing to do!
    // Realm will automatically detect new properties and removed properties
    // And will update the schema on disk automatically
  }
})
// now that we have called `setSchemaVersion:withMigrationBlock:`, opening an outdated
// Realm will automatically perform the migration and opening the Realm will succeed
// i.e. RLMRealm.defaultRealm()

최소한 해야할 일은 Realm이 (자동적으로) 업그레이드한 스카마를 가르키는 빈 블럭과 버전을 수정하는 일입니다.

// Inside your [AppDelegate didFinishLaunchingWithOptions:]

[RLMRealm setSchemaVersion:1 
            forRealmAtPath:[RLMRealm defaultRealmPath] 
        withMigrationBlock:^(RLMMigration *migration, NSUInteger oldSchemaVersion) {
  // We haven’t migrated anything yet, so oldSchemaVersion == 0
  if (oldSchemaVersion < 1) {
    // The enumerateObjects:block: method iterates
    // over every 'Person' object stored in the Realm file
    [migration enumerateObjects:Person.className
                          block:^(RLMObject *oldObject, RLMObject *newObject) {    
      
      // combine name fields into a single field
      newObject[@"fullName"] = [NSString stringWithFormat:@"%@ %@",
                                         oldObject[@"firstName"],
                                         oldObject[@"lastName"]];          
    }];
  }
}];
// Inside your application(application:didFinishLaunchingWithOptions:)

RLMRealm.setSchemaVersion(1, forRealmAtPath: RLMRealm.defaultRealmPath(),
                         withMigrationBlock: { migration, oldSchemaVersion in
  if oldSchemaVersion < 1 {
    // The enumerateObjects:block: method iterates
    // over every 'Person' object stored in the Realm file
    migration.enumerateObjects(Person.className()) { oldObject, newObject in
      // combine name fields into a single field
      let firstName = oldObject["firstName"] as String
      let lastName = oldObject["lastName"] as String
      newObject["fullName"] = "\(firstName) \(lastName)"
    }
  }
})

마이그레이션이 성공적으로 완료되면 앱을 통해 Realm 및 모든 개체를 평소와 같이 접근 할 수 있습니다.

버전 추가

두 버전의 Person 클래스가 있다고 가정합니다:

// v0
@interface Person : RLMObject
@property NSString *firstName;
@property NSString *lastName;
@property int age;
@end

// v1
@interface Person : RLMObject
@property NSString *fullName; // new property
@property int age;
@end

// v2
@interface Person : RLMObject
@property NSString *fullName;
@property NSString *email;   // new property
@property int age;
@end
// v0
class Person: RLMObject {
    dynamic var firstName = ""
    dynamic var firstName = ""
    dynamic var age = 0
}

// v1
class Person: RLMObject {
    dynamic var fullName = "" // new property
    dynamic var age = 0
}

// v2
class Person: RLMObject {
    dynamic var fullName = ""
    dynamic var email = "" // new property
    dynamic var age = 0
}

마이그레이션 블록의 로직은 다음과 같습니다.

[RLMRealm setSchemaVersion:2 forRealmAtPath:[RLMRealm defaultRealmPath] 
                         withMigrationBlock:^(RLMMigration *migration, 
                                              NSUInteger oldSchemaVersion) {
  // The enumerateObjects:block: method iterates
  // over every 'Person' object stored in the Realm file
  [migration enumerateObjects:Person.className 
                        block:^(RLMObject *oldObject, RLMObject *newObject) {
    // Add the 'fullName' property only to Realms with a schema version of 0
    if (oldSchemaVersion < 1) {
      newObject[@"fullName"] = [NSString stringWithFormat:@"%@ %@",
                                oldObject[@"firstName"],
                                oldObject[@"lastName"]];
    }

    // Add the 'email' property to Realms with a schema version of 0 or 1
    if (oldSchemaVersion < 2) {
      newObject[@"email"] = @"";
    }
  }];
}];

// now that we have called `setSchemaVersion:withMigrationBlock:`, opening an outdated
// Realm will automatically perform the migration and opening the Realm will succeed
[RLMRealm defaultRealm];
RLMRealm.setSchemaVersion(2, forRealmAtPath: RLMRealm.defaultRealmPath(), 
                         withMigrationBlock: { migration, oldSchemaVersion in
  // The enumerateObjects:block: method iterates
  // over every 'Person' object stored in the Realm file
  migration.enumerateObjects(Person.className()) { oldObject, newObject in
    // Add the 'fullName' property only to Realms with a schema version of 0
    if oldSchemaVersion < 1 {
      let firstName = oldObject["firstName"] as String
      let lastName = oldObject["lastName"] as String
      newObject["fullName"] = "\(firstName) \(lastName)"
    }

    // Add the 'email' property to Realms with a schema version of 0 or 1
    if oldSchemaVersion < 2 {
        newObject["email"] = ""
    }
  }
})

// Realm will automatically perform the migration and opening the Realm will succeed
let realm = RLMRealm.defaultRealm()

데이터 스키마의 마이그레이션 구현에 대한 더욱 자세한 내용은 마이그레이션 예제 앱을 확인하세요.

선형 마이그레이션

어떤 앱의 두 사용자 JP와 Tim이 있다고 가정해 봅니다. JP는 매우 자주 수정을 합니다만 Tim은 몇몇 버전은 건너뛰었습니다. JP는 앱의 모든 새 버전의 앱을 보고 매번 스키마를 업그레이드했을 것입니다. 그는 앱을 다운로드 할때마다, v0에서 v1으로 v1에서 v2로 수정했을 겁니다. 반면에 Tim이 다운로드를 하였을 경우에는 갑자기 v0에서 v2로 변경하여야 했을 겁니다. 따라서 시작하는 버전에 관계 없도록 비종속적으로 if (oldSchemaVersion < X)와 같이 모든 업그레이드가 보이도록 마이그레이션 블럭을 구조화 해야 합니다.

또 다른 시나리오는 버전을 건너뛰는 사용자에게서 발생 할 수 있습니다. 만약 Version 2에서 “email” 속성을 삭제하고, Version 3에서 다시 재도입하였고, 사용자는 Version 1에서 Version 3로 건너뛰었을 때, 그 코드 안의 해당 속성에 대한 스키마와 디스크상의 스키마에 불일치가 없어서 Realm은 “email” 속성의 삭제를 자동적으로 감지하지 못합니다. Tim의 Person 객체는 v3의 address 속성은 v1의 address 속성을 따라 갑니다. 이것은 v1 및 v3 사이의 속성에 대해서 내부 저장소의 표현을 변경(말하자면, ISO 주소 표현을 사용자 정의로 변경하는 것과 같은)하지 않는 한 문제가 되지 않을 수도 있습니다. 이와 같은 것을 피하기 위해서 모든 버전에 대해서 또는 “email” 속성에 대해 if (oldSchemaVersion < 3)와 같이 사용하여 모든 Realm이 version 3로 업그레이드 되어 정상적인 데이터셋을 가지도록 보장 할 것을 권고합니다.

암호화

iOS에서 Realm 파일을 약간의 오버헤드를 감수하고 NSFileProtection API 사용으로 암호화할 수 있습니다. 암호화에 대한 두가지 주요 주의점입니다. 1) Realm 파일을 플랫폼 간에 이식하지 않는다. (NSFileProtection은 iOS 전용입니다.) 그리고 2) Realm 파일을 iOS 장비 내에서 사용자 암호로 암호화하지 않는다. 두가지 제한을 피하기 위해서는 (혹은 OS X 앱을 개발하고 있다면) Realm 암호화를 사용해야 합니다.

Realm은 Realm 생성 시에 64-바이트 암호화 키를 제공하면 AES-256+SHA2 방식으로 디스크 내 데이터베이스 파일의 암호화를 지원합니다.

// Generate a random encryption key
NSMutableData *key = [NSMutableData dataWithLength:64];
SecRandomCopyBytes(kSecRandomDefault, key.length, (uint8_t *)key.mutableBytes);

// Open the encrypted Realm file
NSError *error;
RLMRealm *realm = [RLMRealm realmWithPath:RLMRealm.defaultRealmPath
                                      key:key
                                 readOnly:NO
                                    error:&error];
if (!realm) {
    // If the encryption key is wrong, `error` will say that it's an invalid database
    NSLog(@"Error opening realm: %@", error);
    return;
}

// Use the Realm as normal
RLMResults *dogs = [[Dog objectsInRealm:realm where:@"name contains 'Fido'"]];
// Generate a random encryption key
let key = NSMutableData(length: 64)!
SecRandomCopyBytes(kSecRandomDefault, UInt(key.length),
    UnsafeMutablePointer<UInt8>(key.mutableBytes))

// Open the encrypted Realm file
var error: NSError?
let realm = RLMRealm(path: RLMRealm.defaultRealmPath(),
    encryptionKey: key, readOnly: false, error: &error)
if realm == nil {
    // If the encryption key is wrong, `error` will say that it's an invalid database
    println("Error opening realm: \(error)")
    return
}

// Use the Realm as normal
let dogs = Dog.objectsInRealm(realm, "name contains 'Fido'")

디스크 내 저장된 모든 데이터를 필요시에 AES-256 방식으로 암호화하고 복호화합니다. 그리고 SHA-2 HMAC 방식으로 확인합니다. RLMRealm 객체를 생성할 때 동일한 암호화 키를 사용해야 합니다. Realm이 주어진 파일 경로에 대해서 Realm을 열 때마다 자동으로 사용하도록 암호화 키를 메모리에 저장하게 할 수 있습니다. 예를 들어, 기본 Realm에 키를 설정합니다. (편하신 방법대로 사용하시면 됩니다.):

// Generate a random encryption key
NSMutableData *key = [NSMutableData dataWithLength:64];
SecRandomCopyBytes(kSecRandomDefault, key.length, (uint8_t *)key.mutableBytes);

// Set the encryption key for the default Realm
[RLMRealm setEncryptionKey:key forRealmsAtPath:RLMRealm.defaultRealmPath];

// Use the Realm as normal
RLMResults *dogs = [[Dog objectsWhere:@"name contains 'Fido'"]];
// Generate a random encryption key
let key = NSMutableData(length: 64)!
SecRandomCopyBytes(kSecRandomDefault, UInt(key.length),
    UnsafeMutablePointer<UInt8>(key.mutableBytes))

// Set the encryption key for the default Realm
RLMRealm.setEncryptionKey(key, forRealmsAtPath: RLMRealm.defaultRealmPath())

// Use the Realm as normal
let dogs = Dog.objectsWhere("name contains 'Fido'")

암호화 키를 생성하고 키체인에 저장하고 Realm에서 사용하기까지 면밀히 살펴보기 위해 Objective‑CSwift 예제를 보세요.

구현된 암호화 방식에 따라 암호화된 Realm을 열 때 디버거가 동작하지 않을 수 있습니다. 그러니 개발 빌드에서는 암호화 기능을 사용하지 않길 선호할 겁니다.

암호화된 Realm을 사용할 때에는 다소 속도 하락(대체로 20% 이하)이 존재합니다.

디버깅

LLDB 지원과 실시간으로 Realm Browser을 통해 앱 내부 데이터를 볼 수 있어서 손쉽게 Realm 앱을 디버깅할 수 있습니다.

LLDB 지원 스크립트

속성 값이 nil 혹은 0으로만 표시되는 대신에 Xcode 플러그인에 Xcode UI에서 RLMObject, RLMResults, RLMArray 객체 디버깅을 지원하는 LLDB 스크립트가 포함되어 있습니다.

Xcode 스크린샷

알림: 해당 스크립트는 현재 Objective‑C만 지원합니다. Swift 지원을 준비 중입니다.

테스팅

Realm 앱 테스팅

Realm을 사용하고 테스트하기에 가장 적절한 방법은 기본 Realm을 사용하는 방법입니다. 테스트를 위해 어플리케이션 데이터를 덮어씌우기보다 간단하게 테스트 전에 +[RLMRealm setDefaultRealmPath:]를 호출하세요.

Realm과 관련된 코드를 테스트하는 또 다른 방법은 테스트하려는 모든 메소드가 RLMRealm 객체를 받고 테스트의 경우 다른 Realm 객체를 넘겨주는 방법입니다. 예를 들어, JSON API로부터 유저 정보를 GET 요청하는 메소드가 있다면 로컬 유저 정보가 즉각적으로 생성되는지 테스트할 수 있습니다:

// Application Code
+ (void)updateUserFromServer
{
  NSURL *url = [NSURL URLWithString:@"http://myapi.example.com/user"];
  [[[NSURLSession sharedSession] dataTaskWithURL:url
                               completionHandler:^(NSData *data,
                                                   NSURLResponse *response,
                                                   NSError *error) {
    [self createOrUpdateUserInRealm:[RLMRealm defaultRealm] withData:data];
  }] resume];
}

+ (void)createOrUpdateUserInRealm:(RLMRealm *)realm withData:(NSData *)data
{
  id object = [NSJSONSerialization JSONObjectWithData:data options:nil error:nil];
  [realm transactionWithBlock:^{
    [User createOrUpdateInRealm:realm withObject:object];
  }];
}

// Test Code
- (void)testThatUserIsUpdatedFromServer
{
  RLMRealm *testRealm = [RLMRealm realmWithPath:kTestRealmPath];
  NSData *jsonData = [@"{\"email\": \"[email protected]\"}"
                      dataUsingEncoding:NSUTF8StringEncoding];
  [ClassBeingTested createOrUpdateUserInRealm:testRealm withData:jsonData];
  User *expectedUser = [User new];
  expectedUser.email = @"[email protected]";
  XCTAssertEqualObjects([User allObjectsInRealm:testRealm][0],
                        expectedUser,
                        @"User was not properly updated from server.");
}
// Application Code
class func updateUserFromServer() {
  let url = NSURL(string: "http://myapi.example.com/user")
  NSURLSession.sharedSession().dataTaskWithURL(url) { data, _, _ in
    self.createOrUpdateUserInRealm(RLMRealm.defaultRealm(), withData: data)
  }
}

public class func createOrUpdateUserInRealm(realm: RLMRealm, withData data: NSData) {
  let object: [String: String] = 
          NSJSONSerialization.JSONObjectWithData(data, options: nil, error: nil)
  realm.transactionWithBlock {
    User.createOrUpdateInRealm(realm, withObject: object)
  }
}

// Test Code
func testThatUserIsUpdatedFromServer() {
  let testRealm = RLMRealm(path: kTestRealmPath)
  let jsonString: NSString = "{\"email\": \"[email protected]\"}"
  let jsonData = jsonString.dataUsingEncoding(NSUTF8StringEncoding)
  ClassBeingTested.createOrUpdateUserInRealm(testRealm, withData: jsonData)
  let expectedUser = User()
  expectedUser.email = "[email protected]"
  XCTAssertEqual(User.allObjectsInRealm(testRealm).first as User,
                 expectedUser,
                 "User was not properly updated from server.")
}

"Object type '...' not persisted in Realm"와 같은 예외 처리 문구와 함께 테스트가 실패했다면, Realm.framework를 테스트 타깃에 링크했기 때문입니다. Realm을 테스트 타깃에서 제외하세요.

CocoaPods을 사용한다면 Podfile은 다음과 같아야 합니다.

target 'MyApp'
  pod 'Realm'
end

target 'MyAppTests', exclusive: true do
  pod 'Realm/Headers'
end

Realm 테스트

Realm은 모든 커밋마다 실행되는 테스트 단위가 있습니다. 그래서, Realm을 테스트하기보다는 앱에서 Realm 사용량을 테스트하는게 적절합니다.

Realm은 오픈소스이므로, 바인딩, 테스트, 문서에 대한 참여를 적극적으로 환영합니다.

REST API

Realm은 손쉽게 REST API와 연동되고 지역 지속성없이 REST API를 사용하는 경우와 달리 몇가지 장점을 갖습니다.

  • REST API는 항상 연결되어야 하는 반면에 Realm 내부에 데이터를 캐싱하면 연결이 끊어져 있어도 오프라인 경험을 제공할 수 있습니다.
  • 모든 데이터를 Realm 내부에 캐싱하면 REST API만으로는 불가능한 지역적으로 쿼리하고 검색하며 사용성을 확장할 수 있습니다.
  • Realm에 데이터를 저장하면 새로운 추가되거나 변경된 데이터만을 서버에 요청함으로써 서버 측면의 부담을 줄일 수 있습니다.

적절한 사례

  1. 비동기 요청 - 네트워크 요청과 다른 블록킹 오퍼레이션은 사용자 인터페이스의 블록킹을 막도록 백그라운드 쓰레드에서 동작해야 합니다. Realm에서 많은 수의 객체 입력과 수정은 백그라운드 쓰레드에서 하길 추천합니다. 백그라운드에서 일어나는 변경 사항에 대응하기 위해 알림를 사용할 수 있습니다.
  2. 대용량 데이터 캐싱 - 가능한 자주 사전조회하고 Realm에 로컬 저장하는 방식을 추천합니다. 이러한 방법은 지역적으로 모든 데이터셋에 쿼리를 가능케 합니다.
  3. 삽입 또는 수정 - 데이터셋이 기본키와 같은 고유 식별자를 가지고 있다면 REST API로부터 응답을 받았을 때 [RLMObject createOrUpdateInDefaultRealmWithObject:]를 삽입 혹은 수정 로직을 구현하는데에 손쉽게 사용할 수 있습니다. 이 메소드는 자동적으로 이미 레코드가 존재하는지 검사하고 새 레코드가 생성되는 동안에도 해당 레코드에 변경 사항이 적용됩니다.

예제

아래에는 REST API를 이용하여 Realm을 어떻게 사용할수 있는지에 대한 간단한 예제입니다. 이 예제에서는 foursquare API를 통해서 JSON형식의 데이터를 검색하고, Realm객체를 통해서 기본 Realm에 데이터를 저장합니다.

실시간으로 동작하는 비슷한 예제는 이곳의 비디오 데모를 통해서 확인이 가능합니다.

먼저 API를 통해 데이터셋을 가져오기 위해서 기본 Realm의 인스턴스를 생성합니다. 간단하게 이 예제에서 [NSData initWithContentsOfURL]를 사용했습니다.

// Call the API
NSData *response = [[NSData alloc] initWithContentsOfURL:
                    [NSURL URLWithString:@"https://api.foursquare.com/v2/venues/search?near=San%20Francisco&limit=50"]];

// Deserialize the response to JSON
NSDictionary *json = [[NSJSONSerialization
                       JSONObjectWithData:response
                                  options:kNilOptions
                                    error:&error] objectForKey:@"response"];
// Call the API
let url = NSURL(string: "https://api.foursquare.com/v2/venues/search?near=San%20Francisco&limit=50")
let response = NSData(contentsOfURL: url)

// De-serialize the response to JSON
let json = NSJSONSerialization.JSONObjectWithData(response,
    options: NSJSONReadingOptions(0),
      error: nil)["response"]

이와 같은 JSON 배열이 응답에 포함되어 있습니다:

{
  "venues": [
    {
      "id": "4c82f252d92ea09323185072",
      "name": "Golden Gate Park",
      "contact": {
        "phone": "4152522590"
      },
      "location": {
        "lat": 37.773835608329,
        "lng": -122.41962432861,
        "postalCode": "94103",
        "cc": "US",
        "state": "California",
        "country": "United States"
      }
    }
  ]
}

Realm에는 이러한 JSON을 가져올 수 있는 몇가지 방법이 있습니다. 사용자 지정 삽입을 통해 NSDictionary를 읽고 하나의 RLMObject로 속성을 대응시킬 수 있습니다. 이 예제의 경우 Realm에 직접적으로 NSDictionary를 삽입하는 대신에 RLMObject의 계층구조에 때에 따라 자동적으로 대응되도록 만듭니다. 이 작업을 위해 JSON의 모든 키에 정확히 매칭시킬 RLMObject 구조의 속성이 필요합니다. JSON 키가 RLMObject의 속성에 매칭되지 않을 때에는 삽입되지 않습니다. 아래의 RLMObject 정의는 정상적으로 동작합니다:

// Contact.h
@interface Contact : RLMObject
@property NSString *phone;
@end

@implementation Contact
+ (NSString)primaryKey {
    return @"phone";
}
@end
RLM_ARRAY_TYPE(Contact)

// Location.h
@interface Location : RLMObject
@property double lat; // latitude
@property double lng; // longitude
@property NSString *postalCode;
@property NSString *cc;
@property NSString *state;
@property NSString *country;
@end

@implementation Location
@end
RLM_ARRAY_TYPE(Location)

// Venue.h
@interface Venue : RLMObject
@property NSString *id;
@property NSString *name;
@property Contact  *contact;
@property Location *location;
@end

@implementation Venue
+ (NSString)primaryKey {
    return @"id";
}
@end
RLM_ARRAY_TYPE(Venue)
class Contact: RLMObject {
    dynamic var phone = ""

    class func primaryKey() -> String! {
        return "phone"
    }
}

class Location: RLMObject {
    dynamic var lat = 0.0  // latitude
    dynamic var lng = 0.0  // longitude
    dynamic var postalCode = ""
    dynamic var cc = ""
    dynamic var state = ""
    dynamic var country = ""
}

class Venue: RLMObject {
    dynamic var id = ""
    dynamic var name = ""
    dynamic var contact = Contact()
    dynamic var location = Location()

    class func primaryKey() -> String! {
        return "id"
    }
}

결과 셋은 배열로 반환되기 때문에 [Venue createInDefaultRealmWithObject:] 호출을 통해 각 엘레멘트를 객체로 만들어야 합니다. 이렇게 JSON으로부터 Venue와 자식 객체가 만들어지고 새롭게 만들어진 객체는 기본 Realm에 추가됩니다.

//Extract the array of venues from the response
NSArray *venues = json[@"venues"];

RLMRealm *realm = [RLMRealm defaultRealm];
[realm beginWriteTransaction];
// Save one Venue object (and dependents) for each element of the array
for (NSDictionary *venue in venues) {
    [Venue createOrUpdateInDefaultRealmWithObject:venue];
}
[realm commitWriteTransaction];
//Extract the array of venues from the response
let venues = json["venues"] as [NSDictionary]

let realm = RLMRealm.defaultRealm()
realm.beginWriteTransaction()
// Save one Venue object (and dependents) for each element of the array
for venue in venues {
    Venue.createOrUpdateInDefaultRealmWithObject(venue)
}
realm.commitWriteTransaction()

추가 지원

예제를 통해서 앱상에서 Realm을 어떻게 사용하는지 확인 할 수 있습니다. (더욱 많은 예제가 준비되어 있습니다!)

즐거운 해킹하세요! 언제나 realm-cocoa(영어)에서 개발자와 대화할 수 있습니다. 또한 Realm 페이스북 그룹에서 한국 개발자와 이야기하고, 한국어로 요청사항을 kr@realm.io으로 보내주세요!

현재 제한 사항

Realm은 현재 베타이며 1.0 공개를 향해 지속적으로 기능을 추가하고 문제점을 보완하고 있습니다. 가장 일반적인 제한 사항을 정리해놨습니다.

알려진 문제점을 알아보기 위해 GitHub 이슈에 방문해주세요.

일반적인 제한

Realm은 사용성과 속도의 균형을 목표로 하고 있습니다. 이러한 목표를 달성하기 위해, 현실적인 제한이 Realm에서 정보를 저장하는 다양한 측면에서 존재합니다. 예를 들어:

  1. 클래스 이름은 0~63 바이트 길이이어야 합니다. UTF8 문자열을 지원합니다. 제한 길이를 넘어서게 되면 앱을 초기화할 때에 예외 처리됩니다.
  2. 속성 이름은 0~63바이트 길이이어야 합니다. UTF8 문자열을 지원합니다. 제한 길이를 넘어서게 되면 앱을 초기화할 때에 예외 처리됩니다.
  3. NSData 속성은 16MB 크기로 제한됩니다. 더 큰 데이터를 저장하기 위해서는 16MB 단위의 작은 파일로 나누거나 파일 시스템에 직접 저장하고 경로를 Realm에 저장하세요. 단일 속성이 16MB를 초과하는 데이터를 저장하려고 시도하면 예외 처리됩니다.
  4. NSDate 속성은 초 단위로만 저장할 수 있습니다. +[NSDate distantFuture]+[NSDate distantPast]로 표현할 수 없습니다. 자세한 정보는 NSDate entry in Current Limitations below에서 확인하세요.
  5. 단일 Realm 파일 크기는 iOS가 대응하는 앱의 메모리 영역 크기를 넘어설 수 없습니다. 이는 디바이스별로 다르고 해당 시점에 얼마나 메모리 공간이 단편화되었는지에 따라 다릅니다 (해당 이슈에 대한 레이더가 있습니다: rdar://17119975). 더 많은 데이터를 저장할 필요성이 있다면 다수의 Realm 파일과 연동할 수 있습니다.

세부적인 알림은 지원하지 않습니다.

알림은 받아볼 수 있지만 (알림) 현재로써는 알림으로부터 무엇이 추가되고/삭제되고/옮겨지고/수정되었는지를 알 수 없습니다. 근시일 내에 이 기능을 추가합니다.

NSDate는 초 단위로 처리됩니다.

NSDate는 초 단위로 끊어서 처리됩니다. 현재 이 부분을 개선중입니다. 자세한 정보는 GitHub issue #875에 방문해주세요. 현재로는, NSTimeInterval 속성으로 손실없이 저장할 수 있습니다.

Realm 객체 Setter 및 Getter는 재정의할 수 없습니다.

Realm이 내부 데이터베이스로 속성을 다루는 위해 setter와 getter를 재정의하기 때문에 객체를 대상으로 재정의할 수 없습니다. 해결책은 재정의가 가능한 realm-ignore 속성을 새로 할당하고 호출하는 방법입니다.

KVO는 지원하지 않습니다.

Realm은 KVO를 지원하지 않지만 고유의 알림 방법을 가지고 있습니다. (확인하세요. 알림)

Realm 파일은 동시에 여러 프로세스에서 접근할 수 없습니다.

Realm 파일이 동시에 여러 쓰레드에서 접근이 가능하지만, 특정 시점에 하나의 프로세스에서만 접근할 수 있습니다. 이 부분은 iOS 8 extensions 과 OS X 어플리케이션의 영향입니다. 서로 다른 프로세스는 Realm 파일을 복사거나 프로세스별로 생성해야만 합니다. 멀티 프로세스 지원은 곧 예정되어 있습니다.

파일 크기 및 중간 버전 추적

SQLite보다 더 적은 용량으로 데이터를 디스크에 저장하길 기대하실 겁니다. Realm 파일이 크다면, Realm이 일시적으로 객체를 추적하지 않도록 invalidate을 호출해야할지도 모릅니다.

데이터의 일관된 뷰를 보여주기 위해 Realm은 오직 run loop 내의 interation 중에서 유효한 버전만을 수정합니다. 여러 다른 쓰레드에서 Realm에 쓰기 작업 중이고 데이터를 조회하고 오래 실행 중인 오퍼레이션에서 해당 쓰레드가 블로킹되고 있다면 해당 버전은 수정되지 않고 Realm은 필요하지 않을지도 모르지만 데이터의 중간 버전을 유지해야만 합니다. 그로인해 파일 크기가 점차 커지게 됩니다. (이러한 추가 공간은 나중에 쓰기 작업에 의해 다시 사용되거나 압축됩니다. 예를 들어, writeCopyToPath:error:. 이러한 처리 대신에 invalidate를 호출하여 조회하고 있는 어떤 객체든 필요하지 않다고 명시적으로 전달할 수 있고 객체의 중간 버전을 추적하는 작업을 중단할 수 있습니다.

FAQ

Realm 라이브러리는 얼마나 큰가요?

한번 앱이 릴리즈를 위해 빌드가 되면, 원래의 크기에서 Realm은 오직 1MB 정도만 추가합니다. 배포되는 버전은 여러 아키텍쳐(ARM, ARM64, x86 for the simulator)를 지원하고 디버그 심볼 등을 포함하고 있어 상당히 크지만(iOS에서 37MB & OSX에서 2.4MB), 앱을 빌드하게 되면 Xcode에 의해 자동적으로 제거됩니다.

상용 어플리케이션에도 Realm을 사용할 수 있나요?

Realm은 2012년부터 상용 제품에 사용되었습니다.

Realm의 Objecitve-C와 Swift API가 커뮤니티의 피드백과 함께 더 많은 기능과 개선 사항으로 개선되고 있다고 기대하셔도 됩니다.

Realm을 사용하기 위해 비용을 지불해야 하나요?

아닙니다. Realm은 상용 프로젝트를 포함하여 사용이 무료입니다.

어떻게 수익을 낼 계획입니까?

이미 기술을 중심으로 기업제품과 서비스를 판매하여 수익을 창출하고 있습니다. 만약 릴리즈나 realm-cocoa보다 현재상태에 대해 더욱 알고 싶으시다면, 이메일로도 대화를 할 수 있습니다. 그리고 공개적인 realm-cocoa를 개발하기 위해, 또한 무료 및 Apache 2.0 라이센스 하에 오픈소스로 유지 될 수 있도록 최선을 다하고 있습니다.

코드 안에 “tightdb”나 “core”로 참조되어 있는 것을 보았습니다. 이것은 무엇인가요?

TightDB는 핵심 C++ 스토리지 엔진의 옛 이름입니다. 그 코어는 현재 오픈 소스가 아니지만 내부를 정리하고 이름을 정리하고 주요 기능을 정하면 역시 Apache 2.0 라이센스로 오픈소스화를 할 계획입니다. 또한, 해당 바이너리는 Realm Core(TightDB)의 바이너리 라이센스 내에서 사용할 수 있습니다.

</div>