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

Realm 자바는 손 쉽게 앱 모델 영역을 안전하고, 영속적이고 빠르게 만듭니다. 어떤 모양인지 살펴보세요.

// RealmObject를 확장하여 모델을 정의합니다
public class Dog extends RealmObject {
    @Required // Name은 null이 될 수 없음
    private String name;
    private int age;

    // ... 생성된 getter와 setter
}
public class Person extends RealmObject {
    @Required // Name은 null이 될 수 있음
    private String name;
    private String imageUrl; // imageUrl는 선택적인 필드
    private RealmList<Dog> dogs; // 사람은 여러 개를 가질 수 있음 (관계)

    // ... 생성된 getter와 setter
}

// 일반적인 자바 객체처럼 사용합니다
Dog dog = new Dog();
dog.setName("Rex");
dog.setAge("1");
Log.v(TAG, "Name of the dog: " + dog.getName());

// 패키지의 "files" 디렉터리의 Realm파일을 지정하는 RealmConfiguration 생성하기.
RealmConfiguration realmConfig = new RealmConfiguration.Builder(context).build();

// 이 스레드의 Realm 인스턴스 얻습니다
Realm realm = Realm.getInstance(realmConfig);

// 2살 이하의 모든 개에 대한 Realm 질의
RealmResults<Dog> puppies = realm.where(Dog.class).lessThan("age", 2).findAll();
puppies.size(); // => Realm에 아직 개가 추가되지 않았기 때문에 0

// 데이터를 손쉽게 영속적으로 만들기
realm.beginTransaction();
realm.copyToRealm(dog);
realm.commitTransaction();

// 질의는 실시간으로 갱신됩니다.
puppies.size(); // => 1

// 질의하고 다른 스레드에서 비동기적으로 갱신하기
realm.executeTransaction(new Realm.Transaction() {
    @Override
    public void execute(Realm realm) {
        // 트랜잭션을 시작하고 닫는 것이 이루어집니다
        Dog theDog = realm.where(Dog.class).equals("age", 1).findFirst();
        theDog.setAge(3);
    }
}, new Realm.Transaction.Callback() {
    @Override
    public void onSuccess() {
        // 원래 질의와 Realm 객체는 자동으로 갱신됩니다  (RealmChangeListener으로 알림도 가고)
        puppies.size(); // => 0 (2살 미만의) 강아지가 더 이상 없기 때문에
        dog.getAge();   // => 3. 강아지의 나이가 갱신되었습니다 
    }
});

시작하기

Android Realm 다운로드 또는 깃헙 저장소 realm-java에서 소스코드를 직접 받을 수 있습니다.

요구사항

  • 현재로서는 Android 이외의 Java 환경은 지원하지 않습니다.
  • Android Studio >= 0.8.6 - Eclipse에서 사용하기 위해서는 아래를 참고하세요.
  • 최신 버전의 Android SDK
  • JDK 버전 >= 7.
  • API Level 9 이상의 모든 Android 버전을 지원합니다. (Android 2.3 진저브레드 버전 이상)

설치하기

Maven을 사용하거나 수동으로 jar 파일을 프로젝트에 추가할 수 있습니다.

Maven

  1. 의존 저장소로 jcenter 를 사용하는지 확인하세요. (최신 Android Gradle 플러그인의 기본 설정)
  2. 프로젝트 의존성 부분에 compile 'io.realm:realm-android:0.87.4'를 추가하세요.
  3. Android Studio 메뉴에서: Tools->Android->Sync Project with Gradle Files

Jar

  1. 릴리즈 패키지를 받고 압축을 푸세요.
  2. Android Studio에서 새로운 프로젝트를 생성하세요.
  3. realm-VERSION.jar 파일을 app/libs 경로에 복사하세요.
  4. Android Studio 메뉴에서: Tools->Android->Sync Project with Gradle Files

우리는 여러분의 앱에서 크래쉬 리포팅 도구를 사용하는 것을 강력하게 추천합니다. 이는 네이티브 에러를 추적할 수 있고 무언가 앱에서 잘못되었을 때 여러분을 도와주기 쉽습니다.

  1. 릴리즈 패키지를 받고 압축을 푸세요.
  2. jar 파일과 librealm-jni.so 파일이 포함된 폴더를 distribution/eclipse 경로에서 앱의 libs 폴더로 복사하세요.
  3. libs 폴더의 jar 파일에서 마우스 오른쪽 클릭한 후, “Build Path” -> “Add to Build path” 메뉴를 선택하세요.
  4. 프로젝트에서 마우스 오른쪽 클릭하고 “Properties”를 선택합니다. “Java Compiter” -> “Annotation Processing” 메뉴를 선택하고 “Enable project specific settings” 메뉴를 선택한 후 “Apply”를 선택합니다.
  5. “Annotation Processing” -> “Factory Path” 메뉴를 선택하고 “Enable project specific settings” 메뉴를 선택합니다. “Click Add JARs” 메뉴를 선택한 후 “libs” 폴더의 Realm jar 파일을 선택하고 빌드합니다.
  6. 어노테이션 프로세서를 실행하기 위해서는 RealmObject 서브클래스에 @RealmClass 어노테이션을 추가해야 합니다.

ProGuard

Realm은 컴파일 시점에 모든 RealmObject에 대해서 각각 proxy 클래스를 생성합니다. ProGuard와 같은 정적 분석 툴과 난독화 프로그램을 실행한 후 proxy 클래스를 사용하기 위해서는 아래 ProGuard 설정 파일을 참고해서 설정을 추가하세요. 사용하는 Realm 버전이 0.84.1 버전 이상이어야 합니다.

-keep class io.realm.annotations.RealmModule
-keep @io.realm.annotations.RealmModule class *
-keep class io.realm.internal.Keep
-keep @io.realm.internal.Keep class * { *; }
-dontwarn javax.**
-dontwarn io.realm.**

Realm 브라우저

현재는 Mac OS X 에서만 사용 가능합니다! 윈도우와 리눅스 버전은 준비하고 있습니다.

.realm 데이터베이스를 읽고 수정하기 위해서 맥앱 스토어에 단일 앱의 형태로도 제공합니다.

Realm Browser

Tools > Generate demo database 를 사용하여 새로운 테스트 데이터베이스를 예제 데이터와 함께 생성할 수 있습니다.

Realm 파일을 찾는데 도움이 필요하면 StackOverflow 답변에서 상세한 설명을 읽어보세요.

Realm 브라우저는 맥 앱스토어에서 찾을 수 있습니다. 만약 이미 Realm Java를 0.83으로 올렸다면 Realm 브라우저 깃헙 페이지의 릴리즈 된 브라우저가 호환됩니다.

API 문서

전체 API 문서에서 모든 클래스, 메서드 와 그 외 다양한 부분을 살펴볼 수 있습니다.

예제

실제 앱에서 Realm이 실제적으로 쓰이는 것은 examples에서 살펴보세요. Android Studio에서 Import Project를 선택한 후 run을 실행해보세요.

introExample는 현재 API를 어떻게 사용하는지를 보여주는 간단한 예제를 담고 있습니다.

gridViewExample은 GridView을 위한 뒷단의 스토어로 Realm을 어떻게 쓰는지 보여주는 작은 앱입니다. 또한 이 앱은 GSON을 이용해서 JSON으로 데이터베이스를 채우는 것과 최종 APK의 크기를 줄이기 위해 ABI를 어떻게 나누는지를 보여주고 있습니다.

threadExample은 다중 스레등 환경에서 Realm을 어떻게 쓰는지 보여주는 간단한 앱입니다.

adapterExample는 RealmResults를 편리하게 안드로이드 리스트에 연결하기 위해 RealmBaseAdapter를 어떻게 쓰는지 보여줍니다.

jsonExample은 새 Realm JSON 기능을 어떻게 사용하는지 설명합니다.

encryptionExample은 Realm에서 암호화를 어떻게 해야 하는지 보여줍니다.

rxJavaExamples는 RxJava와 어떻게 같이 동작하는지 보여줍니다.

unitTestExample는 Realm과 유닛 테스트를 함께 쓰는 법을 설명합니다.

도움을 구하려면

모델

Realm 데이터 모델은 Java Bean 모델을 따릅니다. RealmObject를 상속하고 Realm 주석 프로세서가 프록시 클래스를 생성합니다.

public class User extends RealmObject {

    @PrimaryKey
    private String          name;
    private int             age;

    @Ignore
    private int             sessionId;

    // Standard getters & setters generated by your IDE…
    public String getName() { return name; }
    public void   setName(String name) { this.name = name; }
    public int    getAge() { return age; }
    public void   setAge(int age) { this.age = age; }
    public int    getSessionId() { return sessionId; }
    public void   setSessionId(int sessionId) { this.sessionId = sessionId; }
}

RealmObject의 역할로 생성된 프록시 클래스는 getter와 setter를 재정의합니다. getter와 setter에 추가한 로직은 실행되지 않습니다.

필드 타입

Realm은 다음의 필드 타입을 지원합니다. boolean, short, int, long, float, double, String, Date and byte[]. 정수 타입의 short, int, long은 Realm 내에서 모두 같은 long 타입으로 대응됩니다. 추가로, RealmObject의 서브클래스와 RealmList<? extends RealmObject> 가 관계 모델을 지원합니다.

박스 타입 Boolean, Byte, Short, Integer, Long, Float, Double도 모델에 사용할 수 있습니다. 이 타입들을 사용하면 필드에 null 값을 넣을 수 있습니다.

자동 갱신 객체

Realm 객체와 RealmResults는 기반 데이터에 의해 라이브로 자동 갱신되는 뷰입니다. 이 것은 결과를 다시 가져올 필요가 없다는 의미입니다. 오브젝트를 수정하면 질의는 즉시 결과로 반영됩니다.

realm.beginTransaction();
Dog myDog = realm.createObject(Dog.class);
myDog.setName("Fido");
myDog.setAge(1);
realm.commitTransaction();

Dog myPuppy = realm.where(Dog.class).equals("age", 1).findFirst();
realm.beginTransaction();
myPuppy.setAge(2);
realm.commitTransaction();

myDog.getAge(); // => 2

Realm 객체와 RealmResults의 이런 특징은 Realm을 더 빠르고 효율적으로 하는 동시에 여러분의 코드를 더 간결하고 반응성 있게 합니다. 예를 들어 액티비티와 프래그먼트가 질의의 결과에 의존하고, 매 접근에 앞서 데이터를 최신으로 갱신할 필요 없이 Realm 객체나 RealmResults를 필드에 저장하여 쓸 수 있습니다.

Realm 데이터 갱신을 알고 그에 따라 앱의 UI를 변경하기 위해 RealmResults를 다시 가져오는 것 대신에 Realm 알림을 구독할 수도 있습니다.

Required 필드와 널 값

null이 필드의 값으로 부적당한 경우가 있습니다. @Required 어노테이션을 null 값을 허용하지 않기를 강제하기 위해 사용할 수 있습니다. null을 대입할 수 없는 기본형 타입에 대해서는 @Required를 붙일 필요가 없습니다.

속성 무시

@Ignore 어노테이션을 사용하여 필드가 디스크에 저장되지 않도록 할 수 있습니다. 필드를 무시하는 것은 사용자의 입력이 모델의 필드보다 많을 때 쓰지 않을 데이터 필드를 다루는 특별 케이스를 만들 필요를 없애 주기 때문에 매우 유용합니다.

속성 색인

@Index 어노테이션은 해당 필드에 검색 색인을 추가합니다. 삽입 처리가 느리며 데이터의 파일을 크게 만들지만 질의 처리를 빠르게 할 수 있습니다. 읽기 성능에 특정해서 최적화를 하는 경우에만 색인을 추가하길 추천합니다. String, byte, short, int, long, boolean, Date 필드용 색인을 지원합니다.

기본 키

필드를 기본 키로 다루기 위해서 @PrimaryKey 어노테이션을 사용해야 하며 필드 타입은 문자열이거나 정수(short, int, long)이어야 합니다. 여러 필드(혼합 키)를 기본 키로 사용할 수는 없습니다. 문자열을 기본 키로 사용하면 해당 필드는 인덱싱됩니다. (@PrimaryKey 어노테이션은 암묵적으로 @Index 어노테이션을 설정합니다.)

Realm 객체가 생성될 때 모든 필드는 초기 값이 설정됩니다. 같은 기본 키 값을 가지고 있는 항목과 충돌을 피하기 위해서는 독립적인 객체를 만들고 필드의 값을 할당을 한 후 (copyToRealm() 메서드를 사용하여) Realm에 객체를 복사하길 권장합니다. 독립적인 객체를 다루기 위해서 아래를 참고하세요. 그리고, createOrUpdate() 메서드를 사용하여 객체를 생성하거나 수정할 수 있습니다. 해당 메서드는 기본 키 값이 존재하지 않으면 새 객체를 생성합니다. 객체가 존재한다면 수정됩니다.

기본 키를 사용하여 속도를 향상시킬 수 있습니다. 질의는 다소 속도 향상을 기대할 수 있는 반면에 객체를 생성하고 수정할 때에는 다소 속도 하락이 있을 수 있습니다. 데이터 크기에 따라 속도 편차가 존재하기 때문에 정확한 수치를 명시하기 어렵습니다.

Realm.createObject()을 호출하면 모든 필드가 기본 값으로 설정된 새로운 객체를 얻습니다. 이 경우 기본 키가 기본 값으로 설정이 되어 기존 객체와 충돌할 수 있습니다. 이것을 막기 위해 단독 객체를 만들어 값을 설정한 후 copyToRealm()를 통해 Realm으로 복사하는 것을 제안합니다.

MyObject obj = new MyObject();
obj.setId(42);
obj.setName("Fish");
realm.beginTransaction();
// Realm에서 새로운 객체를 만듭니다
// realm.copyToRealm(obj);
// 기존 객체와 같은 id를 적으면 갱신하고 아니면 대신 새로운 객체를 만듭니다
realm.copyToRealmOrUpdate(obj);
realm.commitTransaction();

기본 키는 null로 설정할 수 없습니다. @PrimaryKey 어노테이션은 암묵적으로 @Required를 가집니다.

제약사항

Proxy 클래스가 getter와 setter를 override 하는 방식으로 인하여 모델 클래스에서 할수 있는 것에 제약이 있습니다.

질의

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

Realm 질의 엔진은 여러 절(multi-clause)의 질의를 구현하기 위해서 Fluent interface를 사용합니다.

User 클래스를 사용합니다.

public class User extends RealmObject {

    @PrimaryKey
    private String          name;
    private int             age;

    @Ignore
    private int             sessionId;

    // Standard getters & setters generated by your IDE…
    public String getName() { return name; }
    public void   setName(String name) { this.name = name; }
    public int    getAge() { return age; }
    public void   setAge(int age) { this.age = age; }
    public int    getSessionId() { return sessionId; }
    public void   setSessionId(int sessionId) { this.sessionId = sessionId; }
}

John이나 Peter라는 이름을 가진 모든 사용자를 질의하기 위해서 다음과 같이합니다.

// Build the query looking at all users:
RealmQuery<User> query = realm.where(User.class);

// Add query conditions:
query.equalTo("name", "John");
query.or().equalTo("name", "Peter");

// Execute the query:
RealmResults<User> result1 = query.findAll();

// Or alternatively do the same all at once (the "Fluent interface"):
RealmResults<User> result2 = realm.where(User.class)
                                  .equalTo("name", "John")
                                  .or()
                                  .equalTo("name", "Peter")
                                  .findAll();

이 명령은 John 혹은 Peter라는 이름을 가진 사용자가 포함된 RealmResults 객체를 반환합니다. 객체는 복사되지 않습니다. 매칭되는 객체 레퍼런스의 리스트를 얻으면 질의 결과에 매칭되는 객체를 직접적으로 다룰 수 있습니다. RealmResults는 Java의 AbstractList를 상속받아 유사하게 동작합니다. 예를들어 RealmResults는 순서가 있는데 개별 객체를 인덱스를 통해 접근할 수 있습니다.

맞는 질의가 없더라도 RealmResults는 null을 반환하지 않고 size() 메서드가 0을 반환하게 됩니다.

만약 RealmResults의 어느 객체를 수정하거나 삭제하려면 쓰기 트랜잭션을 사용해야 합니다.

조건

이름만으로 설명되는 조건이 있습니다.

  • between(), greaterThan(), lessThan(), greaterThanOrEqualTo(), lessThanOrEqualTo()
  • equalTo(), notEqualTo()
  • contains(), beginsWith(), endsWith()

모든 조건이 모든 데이터 타입을 지원하진 않습니다. 자세한 정보는 API 레퍼런스를 참고하세요. RealmQuery.

수식어

CASE_INSENSITIVE 수식어로 문자열 조건은 A-Z와 a-z 내에서 대소문자를 구별하지 않을 수 있습니다.

논리 연산자

각 조건은 암묵적으로 and 연산이 적용됩니다. or 연산을 위해서는 명시적으로 or()를 사용하세요.

User 클래스를 사용합니다.

public class User extends RealmObject {

    @PrimaryKey
    private String          name;
    private int             age;

    @Ignore
    private int             sessionId;

    // Standard getters & setters generated by your IDE…
    public String getName() { return name; }
    public void   setName(String name) { this.name = name; }
    public int    getAge() { return age; }
    public void   setAge(int age) { this.age = age; }
    public int    getSessionId() { return sessionId; }
    public void   setSessionId(int sessionId) { this.sessionId = sessionId; }
}

또한 그룹 조건의 연산 순서를 명시하기 위해서 “괄호”를 사용할 수 있습니다. beginGroup() 은 “왼쪽 괄호”이고 endGroup()는 “오른쪽 괄호” 역할을 합니다.

RealmResults<User> r = realm.where(User.class)
                            .greaterThan("age", 10)  //implicit AND
                            .beginGroup()
                                .equalTo("name", "Peter")
                                .or()
                                .contains("name", "Jo")
                            .endGroup()
                            .findAll();

게다가, not()으로 부정 조건을 만들 수 있습니다. not() 연산자는 하위 조건을 부정하기 위해서는 오직 beginGroup()/endGroup()과 함께 사용할 수 있습니다.

정렬

질의를 마쳤을 때 아래와 같이 결과를 정렬할 수 있습니다.

RealmResults<User> result = realm.where(User.class).findAll();
result.sort("age"); // 오름차순으로 정렬
result.sort("age", Sort.DESCENDING);

연속 질의

결과가 복사되지 않고 요청 시에 바로 연산되기 때문에 데이터를 필터링하기 위해서 효율적으로 연속 질의를 사용하실 수 있습니다.

RealmResults<Person> teenagers = realm.where(Person.class).between("age", 13, 20).findAll();
Person firstJohn = teenagers.where().equalTo("name", "John").findFirst();

연속 질의를 자식 객체들에게도 행할 수 있습니다. 위의 Person 객체가 Dog 개체들의 목록을 가진다고 가정해봅시다.

public class Dog extends RealmObject {
    private int age;
    // 게터와 세터들 ...
}

public class Person extends RealmObject {
    private int age;
    private RealmList<Dog> dogs;
    // 게터와 세터들 ...
}

1살인 개를 한 마리 이상을 가진 13세와 20세 사이의 모든 사람들을 질의할 수 있습니다.

RealmResults<Person> teensWithPups = realm.where(Person.class).between("age", 13, 20).equalTo("dogs.age", 1).findAll();

더 자세한 내용은 연결 질의을 참고하세요.

자동 갱신 결과

RealmResults는 기반 데이터에 의해 라이브로 자동 갱신되는 뷰입니다. 이 것은 결과를 다시 가져올 필요가 없다는 의미입니다. 데이터를 수정하면 질의는 즉시 결과로 반영됩니다.

RealmResults<Dog> puppies = realm.where(Dog.class).lessThan("age", 2).findAll();
puppies.size(); // => 0

realm.beginTransaction();
Dog dog = realm.createObject(Dog.class);
dog.setName("Fido");
dog.setAge(1);
realm.commitTransaction();

puppies.size(); // => 1

이는 RealmResults 전체에 적용됩니다. 모든 객체, 필터처리, 연결된 처리에 사용됩니다.

Realm 객체와 RealmResults의 이런 특징은 Realm을 더 빠르고 효율적으로 하는 동시에 여러분의 코드를 더 간결하고 반응성 있게 합니다. 예를 들어 액티비티와 프래그먼트가 질의의 결과에 의존하고, 매 접근에 앞서 데이터를 최신으로 갱신할 필요 없이 Realm 객체나 RealmResults를 필드에 저장하여 쓸 수 있습니다.

Realm 데이터 갱신을 알고 그에 따라 앱의 UI를 변경하기 위해 RealmResults를 다시 가져오는 것 대신에 Realm 알림을 구독할 수도 있습니다.

결과는 자동 갱신되기 때문에 어떤 고정적으로 유지되는 인덱스나 카운트에 의존하지 않는 것이 중요합니다.

타입으로부터 객체 가져오기

Realm에서 객체를 가져오는 가장 기본적인 방법은 질의된 모델 클래스의 전체 인스턴스의 RealmResults를 반환하는 realm.allObjects()입니다.

정렬 기능을 제공하는 특별한 버전의 allObjects()도 있습니다. 예를 들어 필드별로 순서를 정렬할 수도 있습니다. 자세한 내용은 realm.allObjectsSorted()를 참고하세요.

집합

RealmResults는 다양한 집합 메서드를 가지고 있습니다.

RealmResults<User> results = realm.where(User.class).findAll();
long   sum     = results.sum("age").longValue();
long   min     = results.min("age").longValue();
long   max     = results.max("age").longValue();
double average = results.average("age");

long   matches = results.size();

반복자

RealmResults의 모든 객체에서 반복자를 사용하기 위해서 Iterable를 사용할 수 있습니다.

RealmResults<User> results = realm.where(User.class).findAll();
for (User u : results) {
    // ... do something with the object ...
}

또는 for 루프를 사용할 수 있습니다.

RealmResults<User> results = realm.where(User.class).findAll();
for (int i = 0; i < results.size(); i++) {
    User u = results.get(i);
    // ... do something with the object ...
}

결과가 자동 갱신되기 때문에 고정적인 인덱스나 카운트에 의존하지 않는 것이 좋습니다. 만약 루프 중에 RealmResults을 고치길 원한다면 RealmResults에서 거꾸로 반복하세요.

// 2살 이상의 강아지들을 1살로 바꾸기
RealmResults results = realm.where(Dog.class).greaterThanOrEqualTo("age", 2).findAll();
realm.beginTransaction();
for (int i = results.size() -1; i >=0; i--) {
   results.get(i).setAge(1);
}
realm.commitTransaction();

어떻게 리버스 루프 (i--)가 수행되는지를 주목하세요. 나이 age 프로퍼티가 변경되고 질의 결과는 자동으로 갱신되기 때문에 이 코드는 성공합니다. 이는 results.size()의 값을 바꿉니다. 뒤로 반복하는 것은 ArrayIndexOutOfBoundsException를 만나지 않는 것을 보장합니다.

자바 반복자의 전체 지원은 현재 개발중이며 이 티켓에서 볼 수 있습니다.

삭제

Realm에서 질의 결과를 삭제할 수 있습니다.

// obtain the results of a query
RealmResults<Dog> results = realm.where(Dog.class).findAll();

// All changes to data must happen in a transaction
realm.beginTransaction();

// remove single match
results.remove(0);
results.removeLast();

// remove a single object
Dog dog = results.get(5);
dog.removeFromRealm();

// Delete all matches
results.clear();

realm.commitTransaction()

비동기 질의

질의를 백그라운드에서 수행할 수 있습니다.

Realm의 대부분의 질의는 심지어 UI 스레드에서 동기적으로 사용할 수 있을 정도로 충분히 빠릅니다. 하지만 복잡한 질의와 대규모 데이터 집합을 백그라운드에서 질의하는 것은 이점이 있습니다.

예제: “John”과 “Peter” 이름으로 사용자를 찾기

질의 만들기

RealmResults<User> result = realm.where(User.class)
                              .equalTo("name", "John")
                              .or()
                              .equalTo("name", "Peter")
                              .findAllAsync();

질의가 블록되지 않고 즉시 RealmResults<User>을 반환하는 것을 유의하세요. 이것은 표준 자바의 Future와 흡사한 promise 입니다. 이 질의는 백그라운드에서 계속되며 한번 완료되면 반환된 RealmResults 인스턴스를 갱신합니다.

만약 RealmResults가 갱신이 완료되었을 때 노티를 받기 원한다면 RealmChangeListener를 등록합니다. 이 리스너는 Realm의 마지막 값이 바뀔 때마다 RealmResults를 갱신합니다.

콜백 등록하기

private RealmChangeListener callback = new RealmChangeListener() {
    @Override
    public void onChange() { // 질의과 완료될 때 한번 호출되고 매 업데이트마다 호출됩니다
    // use the result
    }
};

public void onStart() {
    RealmResults<User> result = realm.where(User.class).findAllAsync();
    result.addChangeListener(callback);
}

액티비티나 프래그먼트를 나갈 때는 메모리 릭을 막기 위해 리스너들을 해제하는 것을 기억합시다.

public void onStop () {
    result.removeChangeListener(callback); // 특정 리스너를 제거합니다
    // or
    result.removeChangeListeners(); // 전체 등록된 리스너들을 제거한다
}

질의가 끝났는지 확인하기

if (result.isLoaded()) {
  // 결과는 지금 사용할 수 있습니다
}

동기적으로 획득한 RealmResults의 isLoaded 를 호출하면 항상 true를 반환합니다.

강제로 질의를 동기적으로 로딩하기

선택적으로 질의 완료를 대기할 수 있습니다. 현재 스레드를 블록하고 질의를 다시 동기적으로 합니다. (Future.get()와 같은 컨셉입니다.)

result.load() // 주의하세요. 이 코드는 현재 스레드를 반환될 때까지 멈추게 합니다

비 루퍼 스레드 (Non-Looper threads)

루퍼 스레드에서만 비동기 질의를 사용할 수 있습니다. 비동기 질의는 결과를 일관되게 전달하기 위해 Realm의 Handler를 사용합니다. Looper 없이 스레드 내에서 비동기 질의를 하게 되면 IllegalStateException을 발생시킵니다.

관계

모든 두 RealmObject는 같이 연결할 수 있습니다.

public class Email extends RealmObject {
    private String address;
    private boolean active;
    // ... setters and getters left out
}

public class Contact extends RealmObject {
    private String name;
    private Email email;
    // ... setters and getters left out
}

Realm에서 관계는 연산에 부담이 없습니다. 관계 연산은 속도 면에서 부담이 없고 관계를 표현하는 내부 구현은 메모리 사용에 상당히 효율적입니다.

다 대 일

RealmObject 서브클래스 타입으로 속성을 선언하세요.

public class Contact extends RealmObject {
    private Email email;
    // Other fields…
}

각 contact(Contact 인스턴스)는 0 혹은 1 개의 email(Email 인스턴스)을 갖습니다. Realm에서는 여러 contact에서 동일한 email 객체를 사용할 수 있고 이러한 관계가 다-대-일 관계가 될 수 있습니다. 보통 이러한 구현은 일-대-일 관계가 일반적입니다.

RealmObject 필드를 null로 설정하면 레퍼런스는 정리하지만 다른 객체들은 Realm에서 지워지지 않습니다.

다 대 다

RealmList 필드 선언을 통해서 하나의 객체에서 0 개, 1 개 또는 그 이상의 객체와 관계를 설정할 수 있습니다.

public class Contact extends RealmObject {
    private RealmList<Email> emails;
    // Other fields…
}

RealmList는 기본적으로 RealmObject의 컨테이너이고 Java의 List와 유사합니다. 서로 다른 RealmList 동일한 객체를 두 번(혹은 이상) 사용할 수 있습니다. 그리고 이러한 방법으로 일-대-다, 다-대-다 관계를 모델링할 수 있습니다.

링크에 있는 데이터에 접근하기 위해서 표준 getter와 setter를 추가할 수 있습니다.

realm.beginTransaction();
Contact contact = realm.createObject(Contact.class);
contact.setName("John Doe");

Email email1 = realm.createObject(Email.class);
email1.setAddress("john@example.com");
email1.setActive(true);
contact.getEmails().add(email1);

Email email2 = realm.createObject(Email.class);
email2.setNumber("jd@example.com");
email2.setActive(false);
contact.getEmails().add(email2);

realm.commitTransaction();

특정 데이터 타입을 모델링할 경우엔 재귀적인 관계를 설정할 수 있습니다.

public class Person extends RealmObject {
    private String name;
    private RealmList<Person> friends;
    // Other fields…
}

Realm은 현재 순환을 탐지하지 않으므로 재귀적인 관계를 사용하고 쉽게 무한 루프를 끝낼 수 있습니다.

RealmList 필드에 null 값을 설정하면 리스트가 초기화됩니다. 이것은 리스트가 비는 것(길이가 0)이고요. 어떤 객체도 지워지지 않습니다. RealmList의 게터는 null을 절대 반환하지 않습니다. 반환 값은 항상 리스트인데 길이가 0이 될 수 있습니다.

연결 질의

연결 질의나 관계를 사용할 수 있습니다. 아래 모델을 고려해보세요.

public class Person extends RealmObject {
  private String id;
  private String name;
  private RealmList<Dog> dogs;
  // 게터와 세터들 ...
}

public class Dog extends RealmObject {
  private String id;
  private String name;
  private String color;
  // 게터와 세터들 ...
}

각 User 객체는 이 테이블 다이어그램에서 보이 듯 여러 개의 관계를 가집니다.

Table Diagram

몇몇 사용자들의 연결 질의를 봅시다.

// persons => [U1,U2]
RealmResults<User> persons = realm.where(User.class)
                                .equalTo("dogs.color", "Brown")
                                .findAll();

먼저 equalsTo 조건의 필드 이름에 ().으로 분리되는) 관계를 포함한 경로를 사용합니다.

위의 질의는 읽고 ‘Brown’인 개들을 가진 User들을 전부 찾아야 합니다. 결과가 가진 Dog 객체들이 조건에 맞지 않다는 것을 이해하는 게 중요합니다. 그들은 User 객체의 부분으로서 온 것입니다.

persons.get(0).getDogs(); // => [A,B]
persons.get(1).getDogs(); // => [B,C,D]

아래의 두 질의를 통해 더 자세히 설명하겠습니다.

// r1 => [U1,U2]
RealmResults<User> r1 = realm.where(User.class)
                             .equalTo("dogs.name","Fluffy")
                             .findAll();

// r2 => [U1,U2]
RealmResults<User> r2 = r1.where()
                          .equalTo("dogs.color", "Brown")
                          .findAll();

첫 번째 질의가 어떻게 두 User 객체를 반환하는 것을 유의합시다. 두 사용자에게 조건이 맞았기 때문입니다. 질의 결과의 각 UserDog 객체의 목록을 가지고 있고 그들이 가진 전체 개 객체들입니다. (이 개들은 원래 질의 조건에 안 맞을 수 있습니다.) 우리가 개들을 찾는게 아니라 특정 (이름이나 색상의) 개를 가진 사람을 찾는 것을 기억합시다. 두 번째 질의는 첫 번째 User 질의 결과(r1)와 각각 User에 속해있는 개들을 평가합니다. 두 번째 질의도 각 사용자들이 조건에 맞았는데 이번에는 개들의 색상 때문입니다.

개념을 굳히는데 돕고자 더 깊게 가봅시다. 아래의 예제를 살펴보세요.

// r1 => [U1,U2]
RealmResults<User> r1 = realm.where(User.class)
                             .equalTo("dogs.name", "Fluffy")
                             .equalTo("dogs.color", "Brown")
                             .findAll();

// r2 => [U2]
RealmResults<User> r2 = realm.where(User.class)
                             .equalTo("dogs.name", "Fluffy")
                             .findAll()
                             .where()
                             .equalTo("dogs.color", "Brown")
                             .findAll();
                             .where()
                             .equalTo("dogs.color", "Yellow")
                             .findAll();

첫 번째 질의가 ‘Fluffy’란 이름을 가진 개들을 가진 모든 사용자들을 찾고 Brown인 모든 개들을 가진 사용자를 찾아서 교집합을 우리에게 줍니다.

r1 질의가 뒤에서 어떻게 수행되는지 전체적으로 이해해봅시다. 두 조건은 equalTo("dogs.name", "Fluffy")equalTo("dogs.color", "Brown")입니다. 첫 번째 조건은 U1U2를 만족시킵니다. 이는 집합 C1입니다. 두 번째 조건은 U1U2에 맞습니다. 이는 집합 C2입니다. 질의에서 논리곱은 두 집합 C1C2에 대한 교집합과 같습니다. C1C2의 교집합은 U1U2입니다. 그래서 r1U1U2입니다.

r2의 배후는 조금 다릅니다. 이 질의를 나누어 봅시다. 질의의 첫 번째 부분은 이렇습니다. RealmResults<User> r2a = realm.where(User.class).equalTo("dogs.name", "Fluffy").findAll() 이는 U1U2로 평가됩니다. 그다음으로 r2b = r2a.where().equalTo("dogs.color", "Brown").findAll();는 역시 U1U2로 평가됩니다. (두 사용자 모두 브라운 개들을 가집니다.) 마지막 질의로 r2 = r2b.where().equalTo("dogs.color", "Yellow").findAll();는 단지 U2만 평가를 합니다. 브라운 개를 결과 집합에 가지면서 노란 개를 가진 사용자는 U2가 유일하기 때문입니다.

쓰기

읽기 오퍼레이션은 암묵적입니다. 즉, 언제든지 객체를 접근하거나 조회할 수 있습니다. 모든 쓰기 오퍼레이션(추가, 수정, 삭제)은 반드시 쓰기 트랜잭션 내에서 이루어져야 합니다. 쓰기 트랜잭션은 커밋하거나 취소할 수 있습니다. 커밋 도중, 모든 수정사항을 디스크에 쓰고 반영되었다면 커밋은 성공적으로 끝납니다. 쓰기 트랜잭션을 취소한다면 모든 수정사항은 취소됩니다. 쓰기 트랜잭션을 사용해서 데이터를 항상 영구적인 상태로 보존할 수 있습니다.

쓰기 트랜잭션은 쓰레드 안전을 보장하기 위해 사용합니다.

// Obtain a Realm instance
Realm realm = Realm.getInstance(this);

realm.beginTransaction();

//... add or update objects here ...

realm.commitTransaction();

쓰기 트랜잭션 내에서 RealmObject를 다루는 작업 동안 변경 사항을 취소하려는 곳에서 영역을 끝내야 합니다. 객체를 커밋하는 대신에 되돌리려면 쓰기 트랜잭션을 간단하게 취소할 수 있습니다.

realm.beginTransaction();
User user = realm.createObject(User.class);

//  ... 

realm.cancelTransaction();

쓰기 오퍼레이션을 서로를 블로킹하므로 다른 쓰레드에서 쓰기 오퍼레이션이 진행 중이면 해당 쓰레드가 블로킹됩니다. 백그라운드 스레드에서 쓰기 작업 중에 UI 스레드에서 쓰기를 시도하는 것은 ANR 에러를 유발합니다. 이를 막기 위해 UI 스레드에서 쓰기 트랜잭션을 사용할 경우에는 비동기 트랜잭션을 사용합시다.

고맙게도 Realm은 MVCC 아키텍처를 사용하기 때문에 쓰기 트랜잭션을 열었을 때도 읽기는 막히지 않습니다! 한 번에 여러 스레드에서 동시에 쓸 필요가 없다면 나누어진 많은 쓰기 트랜잭션보다 한 번에 큰 쓰기 트랜잭션을 하는 것을 선호하게 될 겁니다. Realm에 한번 쓰기 트랜잭션을 커밋하면 다른 모든 Realm 인스턴스들은 노티를 받게 되고 암묵적 읽기 트랜잭션은 자동으로 Realm 객체를 갱신하게 됩니다.

Realm의 읽기, 쓰기 접근은 ACID라는 것을 유의하세요.

객체 생성하기

RealmObject 객체들이 Realm과 긴밀하게 연결되어있기 때문에 Realm을 통해 직접적으로 인스턴스가 생성되어야 합니다.

realm.beginTransaction();
User user = realm.createObject(User.class); // Create a new object
user.setName("John");
user.setEmail("john@corporation.com");
realm.commitTransaction();

대안으로 realm.copyToRealm()를 사용해서 처음에 객체의 인스턴스를 생성하고 나중에 추가할 수 있습니다. Realm은 public no arguments 생성자을 포함해 많은 사용자 정의 생성자를 지원합니다.

User user = new User("John");
user.setEmail("john@corporation.com");

// Copy the object to Realm. Any further changes must happen on realmUser
realm.beginTransaction();
User realmUser = realm.copyToRealm(user);  
realm.commitTransaction();

realm.copyToRealm()을 사용할 때 반환되는 객체를 Realm이 관리한다는 점이 중요합니다. 원본 객체애 대한 이후 변경점은 영구적이지 않습니다.

트랜잭션 블록

수동으로 realm.beginTransaction(), realm.commitTransaction(), realm.cancelTransaction()을 관리하는 대신에 자동으로 begin/commit을 관리하고 에러가 발생했을 때 cancel 하도록 지원하는 realm.executeTransaction 메서드를 사용할 수 있습니다.

realm.executeTransaction(new Realm.Transaction() {
	@Override
	public void execute(Realm realm) {
		User user = realm.createObject(User.class);
		user.setName("John");
		user.setEmail("john@corporation.com");
	}
});

비동기 트랜잭션

트랜잭션이 다른 트랜잭션 들에 의해 블록되기 때문에 UI 스레드를 블록하는 것을 막기 위해 백그라운드 스레드에서 모든 쓰기 작업을 하는 것은 의의가 있습니다. 비동기 트랜잭션을 사용하면 Realm은 트랜잭션을 백그라운드 스레드에서 수행하고 트랜잭션이 끝나면 보고합니다.

동기 트랜잭션을 비동기 트랜잭션을 바꾸기 위해서는 Realm.Transaction.Callback 파라미터를 추가합니다.

realm.executeTransaction(new Realm.Transaction() {
            @Override
            public void execute(Realm bgRealm) {
                User user = bgRealm.createObject(User.class);
                user.setName("John");
                user.setEmail("john@corporation.com");
            }
        }, new Realm.Transaction.Callback() {
            @Override
            public void onSuccess() {
            }

            @Override
            public void onError(Exception e) {
                // 트랜잭션이 자동으로 되돌려집니다. 어떤 정리를 여기에서 합니다
            }
        });

콜백은 선택적입니다. 트랜잭션이 성공하거나 실패하면 호출이 됩니다. 콜백은 Looper에 의해 제어되고 null 콜백만 루퍼 없는 스레드에 허용됩니다.

RealmAsyncTask transaction = realm.executeTransaction(new Realm.Transaction() {
            @Override
            public void execute(Realm bgRealm) {
                User user = bgRealm.createObject(User.class);
                user.setName("John");
                user.setEmail("john@corporation.com");
            }
        }, null);

비동기 트랜잭션은 RealmAsyncTask 객체로 표현됩니다. 이 객체는 트랜잭션이 끝나기 전에 액티비티나 프래그먼트를 끝내야 할 때 대기 중인 트랜잭션을 취소하기 위해서도 사용합니다. 트랜잭션을 취소하는 것을 잊으면 콜백이 UI 갱신을 함에 따라 앱이 충돌할 수 있습니다.

public void onStop () {
    if (transaction != null && !transaction.isCancelled()) {
        transaction.cancel();
    }
}

Realms

Realm은 데이터베이스를 의미합니다. 다양한 객체를 가지고 있고 디스크에서 하나의 파일에 대응됩니다.

Realm 설정하기

Realm을 어떻게 생성하는지 모든 측면을 제어하기 위해 RealmConfiguration 객체를 사용합니다. 최소한의 Realm 설정을 위해서는 아래의 것이 사용됩니다.

RealmConfiguration config = new RealmConfiguration.Builder(context).build();

위 설정은 Context.getFilesDir()에 위치한 default.realm 파일을 지정합니다.

전형적인 설정은 아래의 모습과 같습니다.

// RealmConfiguration은 빌더 패턴에 의해 생성됩니다.
// realm 파일은 Context.getFilesDir()에 위치하면 이름은 "myrealm.realm"입니다.
RealmConfiguration config = new RealmConfiguration.Builder(context)
  .name("myrealm.realm")
  .encryptionKey(getKey())
  .schemaVersion(42)
  .setModules(new MySchemaModule())
  .migration(new MyMigration())
  .build();
// 설정을 사용합니다.
Realm realm = Realm.getInstance(config);

또 여러 개의 RealmConfiguration을 쓰는 것도 가능합니다. 이런 방법에서는 버전, 스키마, 위치등을 Realm마다 독립적으로 제어할 수 있습니다.

RealmConfiguration myConfig = new RealmConfiguration.Builder(context)
  .name("myrealm.realm").
  .schemaVersion(2)
  .setModules(new MyCustomSchema())
  .build();

RealmConfiguration otherConfig = new RealmConfiguration.Builder(context)
  .name("otherrealm.realm")
  .schemaVersion(5)
  .setModules(new MyOtherSchema())
  .build();

Realm myRealm = Realm.getInstance(myConfig);
Realm otherRealm = Realm.getInstance(otherConfig);

Realm의 절대 경로는 항상 Realm.getPath()에서 절대 경로를 얻을 수 있다.

Realm 인스턴스는 스레드 싱글톤이다는 점은 중요하다. 매 스레드마다 정적 생성자가 동일한 인스턴스를 반환한다는 것을 의미하기 때문이다.

기본 RealmConfiguration

RealmConfiguration를 기본 설정으로 저장할 수 있습니다. 커스텀 Application 클래스에 기본 설정을 넣고 다른 코드에서 사용 가능한지 확인합시다.

public class MyApplication extends Application {
  @Override
  public void onCreate() {
    super.onCreate();
    // Context.getFilesDir()에 "default.realm"란 이름의 Realm 파일이 위치한다
    RealmConfiguration config = new RealmConfiguration.Builder(this).build();
    Realm.setDefaultConfiguration(config);
  }
}

public class MyActivity extends Activity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Realm realm = Realm.getDefaultInstance();
    // ... 무언가 합니다 ...
    realm.close();
  }
}

인 메모리 (In-memory) Realm

비 영속적인 인 메모리 Realm 인스턴스를 정의해봅시다.

RealmConfiguration myConfig = new RealmConfiguration.Builder(context)
    .name("myrealm.realm")
    .inMemory()
    .build();

이렇게 설정하면 디스크에 저장하지 않는 인 메모리 Realm이 생성됩니다. 인 메모리 Realm은 메모리가 부족하면 여전히 디스크를 이용합니다. 하지만 모든 파일들은 메모리 상에서 생성되고 Realm이 닫히면 삭제됩니다.

일반적인 (영속적인) Realm과 같은 이름의 인 메모리 Realm은 허용되지 않는다는 것에 유의해주세요.

개별 이름을 가지는 모든 인 메모리 Realm 인스턴스들은 더 이상 어떤 레퍼런스도 가리키지 않을 때 Realm의 모든 데이터를 해제합니다. 앱의 생애 동안 생성된 인 메모리 Realm은 레퍼런스를 잘 관리해야 합니다.

동적 Realms

전통적인 Realm을 사용할 때 데이터 모델은 RealmObject의 서브클래스로 정의하였습니다. 형 안정성에 많은 이점이 있었지만 어떤 경우에는 컴파일 시점에 형이 사용 가능하지 않을 수 있습니다. 마이그레이션 동안이나 CSV 파일과 같은 문자열 기반의 데이터를 다룰 때를 예로 들겠습니다.

DynamicRealm은 전통적인 Realm의 변형으로 RealmObject의 서브클래스 없이 Realm 데이터를 다룰 수 있습니다. 단 모든 접근은 클래스 대신 문자열을 통하게 되죠.

동적 Realm을 열 때의 설정은 전통적인 Realm을 사용할 때와 같습니다. 하지만 동적일 때는 어떤 스키마, 마이그레이션, 스키마 버전은 무시됩니다.

RealmConfiguration realmConfig = new RealmConfiguration.Builder(context).build();
DynamicRealm realm = DynamicRealm.getInstance(realmConfig);

// DynamicRealm에서 모든 객체들은 DynamicRealmObjects입니다
DynamicRealmObject person = realm.createObject("Person");

// 모든 필드는 문자열로 접근가능합니다.
String name = person.getString("name");
int age = person.getInt("age");

// 하부의 스키마는 존재하기에 존재하지 않는 필드에 예외를 발생시킵니다
person.getString("I don't exist");

// 질의는 여전히 평범하게 동작합니다
RealmResults<DynamicRealmObject> persons = realm.where("Person").equalTo("name", "John").findAll();

DynamicRealm은 안정성과 성능을 유연성과 교환합니다. 그렇기 때문에 정말로 유연성이 필요할 때만 사용하세요.

Realm 객체 닫기

Realm은 네이티브 메모리 해제와 파일 서술자를 다루기 위해 Closeable을 구현합니다. Realm을 사용한 후 Realm 객체를 닫는 게 중요합니다.

Realm 인스턴스는 레퍼런스 카운팅 됩니다. 즉, 하나의 쓰레드에서 getInstance()를 두 번 호출하면 사용이 끝난 후 close()도 두 번 호출해야 합니다. 어떠한 쓰레드가 실행될지 걱정 없이 Runnable 클래스를 구현하면 됩니다. 간단하게 getInstance()로 시작해서 close()로 끝냅니다!

UI 쓰레드를 대상으로 한다면 onDestroy() 메서드에서 realm.close()을 호출하는 게 가장 간단합니다.

AsyncTask를 대상으로 한다면 이러한 패턴이 좋습니다(모든 Closeable를 의미합니다):

protected Long doInBackground(Context... contexts) {
    Realm realm = null;
    try {
        realm = Realm.getInstance(contexts[0]);

        // ... Use the Realm instance
    } finally {
        if (realm != null) {
            realm.close();
        }
    }
}

또 다른 Looper 쓰레드 생성이 필요하다면 UI 쓰레드 대신에 다른 쓰레드에서 이러한 패턴을 사용할 수 있습니다.

public class MyThread extends Thread {
    private final Context;

    public MyThread(Context context) {
        this.context = context;
    }

    public void run() {
        Looper.prepare();
        Realm realm = null;
        try {
            realm = Realm.getInstance(context);

            //... Setup the handlers using the Realm instance
            Lopper.loop();
        } finally {
            if (realm != null) {
                realm.close();
            }
        }
    }
}

minSdkVersion >= 19 이상인 앱에서 동작하길 원하면 try-with-resources를 사용할 수 있습니다.

try (Realm realm = Realm.getInstance(context)) {
	// No need to close the Realm instance manually
}

자동 리프레쉬

Looper (기본적으로 UI 쓰레드)와 관련된 쓰레드에서 Realm 인스턴스를 선언했다면 해당 Realm 인스턴스는 자동 리프레쉬 기능을 지원합니다. 즉, 이벤트 루프 내의 모든 일에서 최신 버전으로 자동 수정된다는 의미입니다. 매우 적은 노력으로 항상 최신 콘텐츠로 UI를 표시할 수 있는 편리한 기능입니다.

Looper를 사용하지 않는 쓰레드에서 Realm 인스턴스를 생성했다면 refresh()를 호출하기 전까지 자동 리프레쉬되지 않습니다. 오랜 버전의 데이터를 유지하는 건 많은 메모리 사용과 디스크 공간을 필요로 하고 데이터 버전이 증가할수록 더 필요로 합니다. 그래서 해당 쓰레드에서 Realm 인스턴스를 사용한 후 닫아주는 게 중요합니다.

Realm 객체의 자동 리프레쉬 기능이 활성화되어있는지 확인하려면 isAutoRefresh() 메서드를 사용할 수 있습니다.

Realm 파일 찾기

App에서 사용하고 있는 Realm 파일을 찾고 싶으시다면 이 StackOverflow 답변 에서 자세한 방법을 확인하실 수 있습니다.

스레드

Realm을 다양한 스레드에서 사용하기 위해 실제로 알고 행해야 하는 것은 매우 적습니다. 여기에서 가져가야할 가장 핵심은 Realm은 여러 스레드에서 데이터를 적은 노력으로 다룰 수 있게 되어 있다는 부분입니다. 객체질의가 언제나 자동 업데이트 되기 때문에 불일치나 성능에 대해 걱정할 필요가 없습니다.

다른 스레드에서 라이브 오브젝트를 다룰 수 있고 그들을 읽고 쓸 수 있습니다. 데이터를 바꾸어야 한다면 트랜잭션을 쓸 수 있습니다. 다른 스레드의 다른 객체들은 거의 실시간에 갱신됩니다. (이 말은 현재 다른 프로세스에서 무언가를 하고 있다면 런룹의 다름 차례에 갱신된다는 의미입니다.)

단 하나의 제약은 스레드 간에 Realm 객체를 자유롭게 전달 할 수 없다는 것입니다. 만약 다른 스레드에서 같은 데이터를 원한다면 단지 다른 스레드에서 해당 데이터에 권한 질의를 하면 됩니다. Realm 반응형 아키텍처를 이용해 변경을 볼 수도 있습니다. 스레드 간 모든 데이터는 최신으로 유지되고요. Realm은 데이터가 갱신될 때 알려줍니다

아래의 예제를 살펴보세요.

Realm 스레드 예제

고객 목록을 출력하는 앱을 가정해봅시다. 백그라운드 스레드(안드로이드 서비스)이 리모트 백 단에서 새 고객을 가져오고 Realm에 저장합니다. 백그라운드 스래드에서 새 고객을 추가하면 UI 스레드는 데이터를 자동 갱신합니다. RealmChangeListener는 UI 위젯 자체가 갱신되어야 할 시점에 UI 스레드에 변경을 알립니다. Realm은 항상 최신으로 갱신하기 때문에 재질의할 필요가 없습니다.

// 프래그먼트나 액티비티 등에서
@Override
public void onActivityCreated(Bundle savedInstanceState) {
    // ... 간결함을 위해 여러 코드를 생략합니다
    realm = Realm.getDefaultInstance();
    // 전체 고객을 가져옵니다
    RealmResults<Customer> customers = realm.where(Customer.class).findAllAsync();
    // ... 리스트 어댑터를 만들고 이것을 ListView, RecyclerView 등에 추가합니다

    // Realm 변경 리스너를 설정합니다
    changeListener = new RealmChangeListener() {
        @Override
        public void onChange() {
            // 어떤 스레드에서 Realm 데이터베이스가 변경되면 이 메서드가 실행됩니다.
            // 변경 리스너는 오로지 루퍼 스레드에서만 돈다는 것을 주의하세요.
            // 비 루퍼 스레드에는 대신에 Realm.refresh()를 사용해야 합니다.
            listAdapter.notifyDataSetChanged(); // UI를 갱신합니다.
        }
    };
    // Realm이 고객 결과가 변경할 때마다 우리의 리스너에게 통보하도록합니다.
    // (항목 추가, 삭제, 갱신, 어떤 종류의 정렬 등)
    customers.addChangeListener(changeListener);
}

// 백그라운드 서비스 안의 다른 스레드
public class PollingService extends Service {
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Realm realm = Realm.getDefaultInstance();
        // 네트워크 호출이나 기타 등등을 해서 어떤 데이터를 가져와서 'json' 변수에 채웁니다
        String json = customerApi.getCustomers();
        realm.beginTransaction();
        realm.createObjectFromJson(Customer.class, json); // 새로운 객체들을 저장합니다
        realm.commitTransaction();
        // 지금 시점에서, UI 스레드의 데이터는 이미 최신입니다.
        // ...
    }
    // ...
}

한번 백그라운드 서비스가 새 고객들을 UI에 넣으면 여러 분이 어떤 추가적인 중재 없이 UI의 customers 목록은 자동으로 갱신됩니다. 개별 객체들도 동일합니다. 단지 하나의 객체만 관리한다고 가정해봅시다. 한 스레드에서 바뀌면 UI 스레드는 자동으로 새로운 데이터를 갖습니다. 만약에 변경에 대해 응답받고 싶다면 그냥 위에 보이듯 리스너를 추가하면 됩니다.

이게 여러분이 할 전부입니다.

여러 쓰레드에서 Realm 사용하기

여러 쓰레드에서 Realm을 사용하기 위한 유일한 규칙은 Realm, RealmObject, RealmResults 인스턴스가 쓰레드 간에 전달될 수 없다는 점입니다. 하지만 비동기 질의비동기 트랜잭션을 이용해서 작업을 백그라운드에 보내고 작업 결과를 원 스레드로 가져올 수 있습니다.

동일한 데이터를 여러 쓰레드에서 접근하고 싶다면 새로운 Realm 인스턴스를 생성해야 하고(예. Realm.getInstance(RealmConfiguration config)기타 설정 방법) 질의를 통해 객체에 접근해야 합니다.

객체는 디스크에서 동일한 데이터에 대응되고 어느 쓰레드에서든지 읽고 쓰기가 가능합니다.

스키마

Realm의 기본 스키마는 프로젝트 안의 모든 Realm 모델 클래스를 포함하도록 되어있습니다. 이 것은 변경이 가능합니다. 예를 들어, Realm이 클래스의 일부분만을 포함하도록 할 수 있습니다. 별도의 RealmModule 을 생성하시어 기 기능을 구현하실 수 있습니다.

// module 생성
@RealmModule(classes = { Person.class, Dog.class })
public class MyModule {
}

// RealmConfiguration 에서 모듈에 대한 설정을 함으로써 선택한 클래스만 허용하도록 할 수 있습니다
RealmConfiguration config = new RealmConfiguration.Builder(context)
  .setModules(new MyModule())
  .build();

// 여러 개의 모델을 하나의 스키마로 합치는 것이 가능합니다
RealmConfiguration config = new RealmConfiguration.Builder(context)
  .setModules(new MyModule(), new MyOtherModule())
  .build();

스키마 공유

라이브러리 개발자 분들께: Realm 을 포함하는 라이브러리는 반드시 그 스키마를 RealmModule을 통해서 노출해야 합니다.

그렇게 함으로써 기본 RealmModule 이 라이브러리 프로젝트용으로 생성되는 것을 방지하여, app의 고유 RealmModule과 충돌이 나는 것을 방지할 수 있습니다. 라이브러리의 RealmModule은 또한 라이브러리가 자신의 Realm class를 앱에 노출하는 방법이기도 합니다.

// 라이브러리는 모듈을 생성하고서 library = true 로 설정해야 합니다. 이 설정은 기본 모듈이 생성되는 것을 막아줍니다.
// 라이브러리의 모든 클래스 리스트를 노출하는 대신 라이브러리의 모든 클래스를 위해 allClasses = true를 사용하실 수 있습니다.
@RealmModule(library = true, allClasses = true)
public class MyLibraryModule {
}

// 따라서 라이브러리 프로젝트는 자신만을 위한 모듈을 명시적으로 설정해야 합니다.
RealmConfiguration libraryConfig = new RealmConfiguration.Builder(context)
  .name("library.realm")
  .setModules(new MyLibraryModule())
  .build();

// 앱은 라이브러리 RealmModule을 자신의 스키마에 추가할 수 있습니다.
RealmConfiguration config = new RealmConfiguration.Builder(context)
  .name("app.realm")
  .setModules(Realm.getDefaultModule(), new MyLibraryModule())
  .build();

RealmModule이 어떻게 라이브러리와 app 프로젝트에서 동작하는지를 여기에 있는 예제를 통해서 확인해 보세요.

JSON

문자열, JSONObject InputStream으로 표현된 JSON을 Realm에 직접 RealmObject로 추가하는 것이 가능합니다. Realm은 RealmObject에 정의되지 않는 JSON 프로퍼티들은 무시합니다. Realm.createObjectFromJson()을 이용해서 단일 객체를 추가할 수 있고 Realm.createAllFromJson()를 이용하여 객체들의 리스트를 추가할 수 있습니다.

// A RealmObject that represents a city
public class City extends RealmObject {
    private String city;
    private int id;
    // getters and setters left out ...
}

// Insert from a string
realm.beginTransaction();
realm.createObjectFromJson(City.class, "{ city: \"Copenhagen\", id: 1 }");
realm.commitTransaction();

// Insert multiple items using a InputStream
InputStream is = new FileInputStream(new File("path_to_file"));
realm.beginTransaction();
try {
    realm.createAllFromJson(City.class, is);
    realm.commitTransaction();
} catch (IOException e) {
    realm.cancelTransaction();
}

Realm에서 JSON 파싱은 다음의 룰을 따릅니다.

  • null 값을 필드로 가진 JSON으로 객체를 만듭니다.
    • 널이 허용된 필드는 기본 값으로 null을 설정합니다.
    • 널이 허용되지 않은 필드는 예외를 던집니다.
  • null값의 필드를 가진 JSON으로 갱신합니다.
    • 널이 허용된 않는 필드는 null로 설정합니다.
    • 널이 허용되지 않은 필드는 예외를 던집니다.
  • JSON이 필드가 없습니다.
    • 널이 허용된 필드와 허용되지 않은 필드의 값 모두 그대로 둡니다.

노티피케이션

Change listeners는 루퍼 스레드에서만 동작합니다. 루퍼 스레드 밖에서는 Realm.refresh()를 대신 사용하세요.

백그라운드 스레드에서 데이터를 Realm에 추가하면 UI나 다른 스레드는 Realm에 추가된 리스너에 의해 변경점을 알림 받습니다. 이는 Realm이 (다른 스레드나 프로세스에 의해) 변경될 때마다 이루어집니다.

public class MyActivity extends Activity {
    private Realm realm;
    // 가비지 컬렉터에 의해 지워지는 것을 막기 위해
    // RealmChangeListener에 대한 레퍼런스를 잡고 있어야 합니다
    private RealmChangeListener realmListener;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      realm = Realm.getDefaultInstance();
      reamlListener = new RealmChangeListener() {
        @Override
        public void onChange() {
            // ... 업데이트를 위해 무언가 합니다 (UI, etc.) ...
        }};
      realm.addChangeListener(realmListener);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 리스너를 제거합니다.
        realm.removeChangeListener(realmListener);
        // Realm 인스턴스를 닫습니다.
        realm.close();
    }
}

전체 리스너를 닫을 필요가 있다면 쉽게 닫을 수 있습니다.

realm.removeAllChangeListeners();

리스너는 Realm에 제한되지 않습니다. 리스너는 RealmObject에 연결될 수 있고 RealmResults에 연결될 수도 있습니다. 이를 통해 객체나 질의 결과의 변화에 반응할 수 있습니다. 더 나아가 오브젝트나 결과가 변화될때 변경 리스너가 자동으로 호출됩니다. 객체를 갱신할 필요가 없습니다.

public class MyActivity extends Activity {
    private Realm realm;
    private RealmChangeListener puppiesListener;
    private RealmChangeListener dogListener;

    private RealmResults<Dog> puppies;
    private Dog dog;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      realm = Realm.getDefaultInstance();
      puppiesListener = new RealmChangeListener() {
        @Override
        public void onChange() {
            // ... puppies 인스턴스와 작업합니다.
        }};

      // Find all the puppies
      puppies = realm.where(Dog.class).lessThanOrEqualTo("age", 2).findAll();
      puppies.addChangeListener(puppiesListener);

      dogListener = new RealmChangeListener() {
        @Override
        public void onChange() {
            // ... Dog 인스턴스를 업데이트하며 작업합니다.
        }};

      dog = realm.where(Dog.class).equals("name", "Fido").findFirst();
      dog.addChangeListener(dogListener);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 리스너들을 제거합니다.
        puppies.removeChangeListener(puppiesListener);
        dog.removeChangeListener(dogListener);
        // Realm 인스턴스를 닫습니다.
        realm.close();
    }
}

참조 형이 변환되었을 때 마지막으로 형 기반의 변경 리스너가 통보받습니다. 아래의 예제를 보세요.

Person person = realm.where(Person.class).findFirst();
person.getDogs(); // => 2 - 목록에 두마리 개가 있다고 가정합니다
person.addChangeListener(new RealmChangeListener() {
                             @Override
                             public void onChange() {
                                 // 사람 인스턴스의 변화에 반응합니다.
                                 // 어떤 참조된 개가 갱신되어도 반응합니다.
                             }
                         });
Dog dog = person.getDogs().get(0);
realm.beginTransaction();
dog.setAge(5);
realm.commitTransaction();
// 참조된 강아지가 변경되었기 때문에
// 사람 변경 리스너는 런 루프의 다음 단계에서 호출됩니다.

마이그레이션

어떤 데이터베이스와 작업하든 데이터 모델(예를 들어 데이터베이스 스키마)은 항상 변경됩니다. Realm의 데이터 모델이 표준 객체로 정의되어 있어 RealmObject의 서브클래스에 상응하는 인터페이스 변경은 쉽습니다.

예전 데이터베이스 스키마가 디스크에 저장한 데이터가 없다면 단지 정의를 바꾸는 것은 괜찮습니다. 이렇게 할 때 Realm은 코드에 정의된 코드와 디스크에 보이는 데이터 Realm을 비교하여 불일치가 있다면 예외를 던지게 됩니다. 이는 RealmConfiguration에 마이그레이션을 코드와 스키마 버전을 설정함으로써 해결합니다.

RealmConfiguration config = new RealmConfiguration.Builder(context)
    .schemaVersion(2) // 스키마가 바뀌면 값을 올려야만 합니다
    .migration(new MyMigration()) // 예외 발생대신에 마이그레이션을 수행하기
    .build()

이를 이용하면 마이그레이션 코드는 필요시 자동으로 수행됩니다. 우리는 디스크의 스키마와 이전 버전 스키마를 위해 저장된 데이터를 갱신하는 여러 내장 메서드를 제공합니다.

// 새 클래스를 추가하는 마이그레이션 예제
RealmMigration migration = new RealmMigration() {
  @Override
  public void migrate(DynamicRealm realm, long oldVersion, long newVersion) {

     // DynamicRealm는 편집 가능한 스키마를 노출합니다
     RealmSchema schema = realm.getSchema();

     // 버전 1로 마이그레이션: 클래스를 생성합니다
     if (oldVersion == 0) {
        schema.create("Person")
            .addField("name", String.class)
            .addField("age", int.class);
        oldVersion++;
     }

     // 버전 2로 마이그레이션: 기본 키를 넣고 객체를 참조합니다
     if (oldVersion == 1) {
        schema.get("Person")
            .addField("id", long.class, FieldAttribute.PRIMARY_KEY)
            .addRealmObjectField("favoriteDog", schema.get("Dog"))
            .addRealmListField("dogs", schema.get("Dog"));
        oldVersion++;
     }
  }
}

자세한 내용은 마이그레이션 샘플 앱에서 확인하세요.

Realm을 실행할 때 파일이 없다면 마이그레이션은 필요하지 않습니다. Realm은 그냥 새로운 .realm 파일과 코드에 정의된 최신 모델에 기반한 스키마를 만듭니다. 이는 한참 개발중이고 스키마를 자주 바꾸고 전체 데이터를 날려도 된다면 마이그레이션을 작성하는 대신 (포함된 전체 데이터셋이 담긴) .realm 파일을 디스크에서 지울 수 있다는 것입니다. 앱 개발 주기의 초기에서 모델을 덕지적지 고치며 작업해보는데 도움이 됩니다.

RealmConfiguration config = new RealmConfiguration.Builder(context)
    .deleteRealmIfMigrationNeeded()
    .build()

암호화

미국으로부터의 수출 제한이나 금수 조치가 있는 국가에 거주하는 경우 Realm 사용에 대한 제한이 있을 수 있으므로, 저희 라이센스의 수출 규정 준수 조항을 참고하십시오.

512 비트 암호화 키를 RealmConfiguration.encryptionKey()에 전달하면 Realm 파일을 암호화할 수 있습니다.

RealmConfiguration config = new RealmConfiguration.Builder(context)
  .encryptionKey(getKey())
  .build();

Realm realm = Realm.getInstance(config);

표준 AES-256 암호화를 통해 투명성있는 암호화 하여 모든 데이터를 저장하며 복호화할 수 있습니다. 파일을 생성할 때 같은 암호화 키를 매번 Realm 인스턴스에 제공해야 합니다.

다른 애플리케이션이 볼 수 없는 안드로이드 키 스토어에 키를 저장하는 완전한 예제를 examples/encryptionExample에서 볼 수 있습니다.

안드로이드와 함께

독립적으로 Realm은 안드로이드와 이음새 없이 잘 동작합니다. RealmObject 객체들이 스레드 제약이라는 점은 염두해야 합니다. Realm 오브젝트를 액티비티 간이나 백그라운드 서비스, 브래드 캐스트 리시버 등으로 전달 할 때 이런 부분을 이해하는 것이 매우 중요합니다.

어댑터

Realm은 RealmResult에서 온 데이터를 UI 위젯에 바인딩하는 것을 돕기 위해 추상 유틸리티 클래스를 제공합니다. RealmBaseAdapter 클래스는 getView() 메서드를 구현하기 위해 필요한 모든 로직을 다루고 있습니다.

public class Person extends RealmObject {
    private String name;
    public String getName() {
        return this.name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

public class MyAdapter extends RealmBaseAdapter<Person> implements ListAdapter {

    private static class MyViewHolder {
        TextView name;
    }

    public MyAdapter(Context context, int resId,
                     RealmResults<Person> realmResults,
                     boolean automaticUpdate) {
        super(context, realmResults, automaticUpdate);
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder viewHolder;
        if (convertView == null) {
            convertView = inflater.inflate(android.R.layout.simple_list_item_1, 
                                           parent, false);
            viewHolder = new ViewHolder();
            viewHolder.name = (TextView) convertView.findViewById(android.R.id.text1);
            convertView.setTag(viewHolder);
        } else {
            viewHolder = (ViewHolder) convertView.getTag();
        }

        Person item = realmResults.get(position);
        viewHolder.name.setText(item.getName());
        return convertView;
    }

    public RealmResults<Person> getRealmResults() {
        return realmResults;
    }    
}

인텐트 서비스

RealmObject를 직접 전달 할 수 없기 때문에 우리는 사용하는 객체의 식별자를 전달하는 것을 추천합니다. 기본적인 예제는 객체가 기본 키를 가지는 것입니다. 기본 키 값을 인텐트의 엑스트라 번들을 통해 전달합시다.

// @PrimaryKey 'id' 필드를 가진 person객체를 가정합시다...
Intent intent = new Intent(getActivity(), ReceivingService.class);
intent.putExtra("person_id", person.getId());
getActivity().startService(intent);

(액티비티, 서비스, 인텐트 서비스, 브로드캐스트리시버 등의) 수신 컴포넌트에 번들로 전달된 기본 키 값을 받고 수신 컴포넌트에서 Realm을 열고 RealmObject를 질의합시다.

// onCreate(), onHandleIntent() 등에서...
String personId = intent.getStringExtra("person_id");
Realm realm = Realm.getDefaultInstance();
Person person = realm.where(Person.class).equalTo("id", personId).findFirst();
// 사람에 대해 어떤 작업을 합시다 ...
realm.close();

동작하는 전체 예제는 스레드 예제Object Passing 부분에서 찾을 수 있습니다. 예제는 어떻게 id를 전달하고 RealmObject를 받는지 안드로이드의 기본 사용례로 보여줍니다.

안드로이드 프레임워크 스레드를 다루는 전략

이 클래스들을 사용할 때는 조심하세요.

AsyncTask 클래스는 백그라운드 스레드를 수행하는 doInBackground() 메서드를 포함합니다. IntentSerive 클래스는 워커 스레드를 수행하는 onHandleIntent(Intent intent) 메서드를 가집니다.

이 둘에서 Realm을 쓰기를 원한다면 각각의 메서드에서 Realm을 열고 작업을 수행하고 끝나기 전에 Realm을 닫아야 합니다. 아래 두 예제가 있습니다.

AsyncTask

아래와 같이 doInBackground 메서드에서 Realm을 열고 닫습니다.

private class DownloadOrders extends AsyncTask<Void, Void, Long> {
    protected Long doInBackground(Void... voids) {
        // 이제 백그라운드 스레드입니다.

        // Realm을 엽니다.
        Realm realm = Realm.getDefaultInstance();
        // Realm을 사용합니다.
        realm.createAllFromJson(Order.class, api.getNewOrders());
        Order firstOrder = realm.where(Order.class).findFirst();
        long orderId = firstOrder.getId(); // order의 id
        realm.close();
        return orderId;
    }

    protected void onPostExecute(Long orderId) {
        // 안드로이드 메인스레드로 돌아왔습니다.
        // orderId를 가지고 order에 대해 Realm에 질의하거나
        // 어떤 연산을 수행합시다.
    }
}

IntentService

아래와 같이 onHandleIntent 메서드에서 Realm을 열고 닫습니다.

public class OrdersIntentService extends IntentService {
    public OrdersIntentService(String name) {
        super("OrdersIntentService");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        // 이제 백그라운드 스레드입니다.

        // Realm을 엽시다.
        Realm realm = Realm.getDefaultInstance();
        // Realm을 사용합니다.
        realm.createAllFromJson(Order.class, api.getNewOrders());
        Order firstOrder = realm.where(Order.class).findFirst();
        long orderId = firstOrder.getId(); // order의 아이디
        realm.close();
    }
}

다른 라이브러리

이 섹션은 Realm과 안드로이드의 다른 일반적인 라이브러리를 통합하는 법을 설명합니다.

GSON

GSON은 구글이 만든 JSON 비 직렬화, 직렬화 도구입니다. (최신 버전인) GSON 2.3.1을 함께 쓰려면 ExclusionStrategy를 지정해야 합니다.

// Using the User class
public class User extends RealmObject {
    private String name;
    private String email;
    // getters and setters left out ...
}

Gson gson = new GsonBuilder()
        .setExclusionStrategies(new ExclusionStrategy() {
            @Override
            public boolean shouldSkipField(FieldAttributes f) {
                return f.getDeclaringClass().equals(RealmObject.class);
            }

            @Override
            public boolean shouldSkipClass(Class<?> clazz) {
                return false;
            }
        })
        .create();

String json = "{ name : 'John', email : 'john@corporation.com' }";
User user = gson.fromJson(json, User.class);

우리의 GridViewExample에서 어떻게 Realm과 GSON이 상호 작용하는지 예를 보세요.

직렬화

Retrofit과 같은 라이브러리와 잘 호환하기 위해서 객체가 직렬화, 비 직렬화가 되길 원할 수 있습니다. GSON이 게터와 세터 대신에 필드 값을 사용하기 때문에 Realm 객체를 JSON으로 직렬화하는 것이 잘되지 않습니다.

각 객체마다 직렬화가 되도록 하기 위한 커스텀 JsonSerializer를 작성하고 TypeAdapter에 등록해야 GSON 직렬화가 작동합니다.

기스트에서 어떻게 해야 하는지를 보여주고 있습니다.

기본형 리스트

어떤 JSON API들은 정수, 문자열 등의 기본형 목록을 반환하는데 아직 Realm이 지원하지 못 하는 것입니다. JSON API를 변경하는 것이 불가능하다면 JSON의 기본형을 Realm의 래퍼 객체로 변환하는 GSON용 TypeAdapter를 만들 수 있습니다.

Gist 정수형들에 대한 래퍼 객체를 사용하는 것을 보여주는데 이런 방법으로 Realm이 지원하는 모든 기본형 데이터 타입 배열에 적용할 수 있습니다.

문제 해결

Realm 객체는 내부적으로 순환 참조를 가질 수 있습니다. 이런 순환 참조는 GSON이 StackOverflowError를 발생하게 할 수 있습니다. Drawable 필드를 가진 Realm 객체에서도 이런 일을 볼 수 있습니다.

public class Person extends RealmObject {
	@Ignore
	Drawable avatar;
	// 다른 필드와 기타.
}

위의 Person 클래스는 안드로이드 Drawable을 가지고 있고 @Ignore을 적용하고 있습니다. GSON의 역직렬화 과정에서 Drawable을 탐색하고 StackOverflowError(GitHub 이슈)를 발생시킵니다. 이를 완화하기 위해 shouldSkipField에 아래 코드를 추가합니다.

public boolean shouldSkipField(FieldAttributes f) {
  return f.getDeclaringClass().equals(RealmObject.class) || f.getDeclaringClass().equals(Drawable.class);
}

Drawable.class의 평가를 보세요. 이 코드는 역직렬화 과정에서 이 필드를 건너 넘어가게 합니다. 이런 코드를 추가하면 StackOverflowError를 완화할 수 있습니다.

Jackson-databind

Jackson-databind는 JSON 데이터와 자바 클래스를 연결하는 라이브러리입니다.

Jackson은 데이터 바인딩을 위해 리플렉션을 사용합니다. 이는 Realm의 RxJava 지원과 충돌하고 RxJava는 클래스로더를 사용할 수 없습니다. 이는 다음과 같은 예외를 일으킵니다.

java.lang.NoClassDefFoundError: rx.Observable
at libcore.reflect.InternalNames.getClass(InternalNames.java:55)
...

이는 RxJava를 프로젝트에 추가하거나 아래와 같은 형태의 비어있는 더미 파일을 생성하여 고칠 수 있습니다.

package rx;

public class Observable {
    // RxJava가 프로젝트 의존성에 없다면
    // Jackson-Databind 지원을 위해 더미 클래스가 필요합니다.
}

이 이슈는 Jackson 프로젝트의 이슈에 보고되어 있습니다.

Otto

OttoSquare가 만든 이벤트 버스입니다. Otto는 Realm과 별도로 잘 도는데 타이밍과 스레드에 관련된 알려진 몇 가지 이슈가 있습니다.

Otto는 일반적으로 보낸 곳과 같은 스레드에서 이벤트를 받습니다. 이는 아무 문제없이 RealmObject를 이벤트에 추가하고 리시버 메서드가 보낸 데이터를 읽을 수 있다는 뜻입니다. 하지만 여기에 설명된 핵을 사용하는 경우 항상 메인스레드로 보낼 수 있고, UI 스레드에서만 이벤트를 보내지 않는다면 이벤트 데이터의 부분으로 RealmObject를 가지는 게 불가능할 수 있습니다. 메인 스레드에서 받은 응답으로 UI 요소를 조작하는 것은 일반적으로 가능합니다.

RealmObject가 한 스레드에서 바뀌면 Realm은 다른 스레드의 Realm 데이터를 Handler를 통해 리프레쉬하도록 일정을 조율합니다. 반면에 Otto.post(event)는 이벤트를 즉시 보냅니다. 만약 Realm 데이터 변경을 알리기 위해 이벤트를 다른 스레드로 보내면 수동으로 realm.refresh()를 호출하여 마지막 데이터를 가져와야 합니다.

@Subscribe
public void handleEvent(OttoEvent event) {
    realm.refresh();
    // Continue working with Realm data loaded in this thread
}

Parceler

ParcelerParcelable 인터페이스를 사용하는 객체를 만들기 위해 필요한 보일러플레이트를 자동으로 만드는 라이브러리입니다. Realm에서 프록시 클래스를 사용하기 때문에 Parceler에서 Realm 모델을 쓰기 위해 아래 설정이 필요합니다.

// 모든 클래스는 짝이맞는 RealmProxy 클래스가 어노테이션 프로세서에 의해 생성될 수 있도록 RealmObject을
// 확장해야합니다. Parceller는 이 점을 알고 만들어져야 합니다. 프로젝트가 최소 한번 컴파일되기 전까지는
// 클래스는 사용 가능하지 않다는 점을 주의합시다.
@Parcel(implementations = { PersonRealmProxy.class },
        value = Parcel.Serialization.BEAN,
        analyze = { Person.class })
public class Person extends RealmObject {
    // ...
}

만약 Parceler를 쓰기 위해 그래들을 쓴다면 아래 라인을 확인하세요. (자세한 내용은 여기를 참고하세요.)

compile "org.parceler:parceler-api:1.0.3"
apt "org.parceler:parceler:1.0.3"

Parceler를 쓸 때 알아야 할 몇 가지 중요한 제약들이 있습니다. 1) 모델 클래스는 RealmList를 가지고 있다면 특별 어뎁터를 등록해야합니다. 2) 객체가 싸여지면 이것은 Realm에서 분리되고 그 시점에 데이터의 스냅션을 담은 독립 객체처럼 행동하게 됩니다. 또 이 객체에 대한 변경은 Realm의 영속화되지 않습니다.

Retrofit

RetrofitSquare가 만든 라이브러리로 형 안정한 방식으로 REST API를 쉽게 사용할 수 있는 도구입니다.

Retrofit 1.*

Retrofit이 GSON을 내부적으로 쓰기 때문에 네트워크 JSON 데이터를 RealmObject로 비 직렬화를 하려면 GsonConverter를 제대로 설정해야 합니다.

Gson gson = new GsonBuilder()
        .setExclusionStrategies(new ExclusionStrategy() {
            @Override
            public boolean shouldSkipField(FieldAttributes f) {
                return f.getDeclaringClass().equals(RealmObject.class);
            }

            @Override
            public boolean shouldSkipClass(Class<?> clazz) {
                return false;
            }
        })
        .create();

// 적절한 GSON 변환기를 이용하도록 Retrofit 설정하기
RestAdapter restAdapter = new RestAdapter.Builder()
    .setEndpoint("https://api.github.com")
    .setConverter(new GsonConverter(gson))
    .build();

GitHubService service = restAdapter.create(GitHubService.class);

Retrofit 2.*

Retrofit 2부터 GSON는 더 이상 기본으로 사용되지 않지만 변환기 모듈로 사용할 수 있습니다.

dependencies {
    compile 'com.squareup.retrofit2:retrofit:2.0.0-beta3'
    compile 'com.squareup.retrofit2:converter-gson:2.0.0-beta3'
Gson gson = new GsonBuilder()
        .setExclusionStrategies(new ExclusionStrategy() {
            @Override
            public boolean shouldSkipField(FieldAttributes f) {
                return f.getDeclaringClass().equals(RealmObject.class);
            }

            @Override
            public boolean shouldSkipClass(Class<?> clazz) {
                return false;
            }
        })
        .create();

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://api.github.com")
                .addConverterFactory(GsonConverterFactory.create(gson)
                .build();

영속 객체

Retrofit은 자동으로 데이터를 Realm에 넣지는 않고 대신 realm.copyToRealm()이나 realm.copyToRealmOrUpdate() 메서드를 사용해야 추가해야 합니다.

GitHubService service = restAdapter.create(GitHubService.class);
List<Repo> repos = service.listRepos("octocat");

// 객체를 영속화하기 위해 Retrofit으로부터 Realm으로 복사합니다.
realm.beginTransaction();
List<Repo> realmRepos = realm.copyToRealmOrUpdate(repos);
realm.commitTransaction();

Robolectric

Robolectric은 JUnuit 테스트를 폰이나 에뮬레이터 대신 JVM에서 직접 수행하게 해주는 라이브러리입니다. 현재 Roboletric은 번들된 Realm과 같은 네이티브 라이브러리를 지원하지 못합니다. 지금 상황에서는 Realm을 테스트하기 위해 Roboletric을 사용할 수 없다는 의미입니다.

기능 요청에서 진행상황을 확인할 수 있습니다. https://github.com/robolectric/robolectric/issues/1389

RxJava

RxJava는 넷플릭스가 옵저버 패턴를 확장하여 만든 리엑티브 확장입니다. 데이터를 구성가능한 순열로서 관찰할 수 있게 합니다.

Realm은 RxJava를 일급 클래스로 지원합니다. 이는 다음 Realm 클래스들의 Observable을 노출한다는 것입니다. Realm, RealmResults, RealmObject, DynamicRealm, DynamicRealmObject.

// Realm, Retrofit, RxJava를 결합합니다. (간결성을 위해 RetroLambda 문장을 사용합니다.)
// 전체 사람을 불러와서 그들의 Github 최신 상태가 있다면 병합합니다.
Realm realm = Realm.getDefaultInstance();
GitHubService api = retrofit.create(GitHubService.class);
realm.where(Person.class).isNotNull("username").findAllAsync().asObservable()
    .filter(persons.isLoaded)
    .flatMap(persons -> Observable.from(persons))
    .flatMap(person -> api.user(person.getGithubUserName())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(user -> showUser(user));

비동기 질의가 블록하지 않으며 위의 코드는 즉시 RealmResults 인스턴스를 반환한다는 것을 주목하세요. 확실히 읽어온 리스트를 다루기 위해 필터 오퍼레이터를 사용하고 RealmResults<E>.isLoaded() 메서드를 호출하여 리스트를 검사하여 Observable을 필터링합니다. RealmResult가 로딩되었느냐를 확인하면 질의의 완료 여부를 판단할 수 있습니다.

더 자세한 내용은 RxJava sample project을 참고하세요.

설정

RXJava는 선택적인 의존성을 가지고 있고 Realm은 자동으로 이를 포함하지 않습니다. 이는 RxJava의 어떤 버전을 사용할 것인지 선택할 수 있고 RxJava를 사용하지 않는 프로젝트에 메서드 개수를 늘리는 문제를 막을 수 있는 이점이 있습니다. RxJava는 build.gradle에 직접 추가할 수 있습니다.

dependencies {
  compile 'io.realm:realm-android:0.87.4'
  compile 'io.reactivex:rxjava:1.1.0'
}

사용자 RxObservableFactory를 만들어 Realm이 Obserbavle들을 어떻게 생성할지 설정할 수 있습니다. 이는 RealmConfiguration에서 설정합니다.

``java RealmConfiguration config = new RealmConfiguration.Builder(context) .rxFactory(new MyRxFactory()) .build()

`RxObservableFactory`가 정의되지 않은 경우 Realm은 기본으로 [`RealmObservableFactory`](api/io/realm/rx/RealmObservableFactory )를 사용합니다. Realm이 제공하는 RxJava <= 1.1.* 버전을 지원하는 클래스입니다.

<span id="debugging"></span>

## 테스팅과 디버깅

Realm에서 JUnit3, JUnit4, Robolectric, Mockito, PowerMock을 어떻게 연동하는지 정보는 [`unitTestExample`](https://github.com/realm/realm-java/tree/master/examples/unitTestExample)에서 확인하세요.

### 안드로이드 스튜디오 디버깅

안드로이드 스튜디오나 인텔리제이에서 사용할 때 알아두어야 할 것들이 있습니다. 디버거는 사용하는 디버깅 뷰에 따라 잘못된 값을 제공할 수 있습니다.

예를 들어 `RealmObject`에 대해 안드로이드 스튜디오에서 볼 때 필드의 값이 표시되됩니다. 불운하게도 필드의 값이 사용되지 않기 때문에 이 값들은 틀렸습니다. Realm은 프록시 오브젝트를 뒤에 생성하여 게터와 세터들을 오버라이드합니다. 이 오버라이드된 메서드들이 Realm의 영속 데이터를 참고하지요. (자세한 내용은 [모델](#section-5)을 참고하세요). 이 액세스를 지켜보면 정확한 값을 얻을 수 있습니다. 이미지를 참고하세요.

<img src="../../../../assets/img/docs/java/realmobject-watch-panel.png" class="img-responsive img-rounded center-block" alt="Android Studio, IntelliJ Debug RealmObject" />

위의 이미지에서 디버거는 113라인에 멈추었습니다. 3개의 워치 밸류가 있는데, `person` 변수, `person.getName()`과 `person.getAge()` 억세서입니다. 107 라인부터 111까지 name과 age를 변경해서 `person` 인스턴스를 변경합니다. 이 값들은 트랜잭션에서 영속적이게 됩니다. 디버거가 멈춘 113 라인에서 `person` 웟치 인스턴스는 필드의 값을 보이고 있고 이 값들은 정확하지 않습니다. 억세서 `person.getName()`와 `person.getAge()`를 사용하는 웟치 벨류들은 정확합니다.

`.toString()` 메서드가 항상 정확한 값을 출력하지만 (`RealmObject`의 변수를 웟치할 때) 웟치 패널은 그렇지 못하다는 점을 유의하세요.

### NDK 디버깅

Realm은 네이티브 코드를 가진 라이브러리입니다. 우리는 [Crashlytics](http://www.crashlytics.com)과 같은 크래쉬 리포팅 도구를 쓰는 것을 추천합니다. 네이티브 에러들을 추적할 수 있고 무언가 문제가 생겼을 때 우리가 더 잘 도울 수 있습니다.

NDK 디버깅은 우리가 사용할 수 있는 것이 기본 스택 트레이스의 제한적인 정보밖에 없을 정도로 까다롭습니다. Crashlytics는 의미 있는 NDK 크래쉬 정보도 잡아줍니다. Crashlytics에서 네이티브 크래쉬 보고를 활성화하려면 [여기 가이드라인](https://fabric.io/downloads/gradle/ndk)을 따라가세요.

여러분의 프로젝트에 NDK 크래쉬 보고를 활성화하려면 아래를 루트의 build.gradle 파일에 추가하세요. `androidNdkOut`와 `androidNdkLibsOut` 값은 필요하지 않습니다.

```groovy
crashlytics {
  enableNdk true
}

현재 제약 사항

Realm은 현재 베타 버전이고 지속적으로 기능을 넣고 이슈를 고치며 1.0 릴리즈를 향하고 있습니다. 지금까지의 가장 일반적인 제한 사항을 정리해놨습니다.

깃헙 이슈에서 알려진 이슈 종합 목록을 확인할 수 있습니다.

일반적인 제한

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

  1. 클래스 이름은 57자로 제한됩니다. Android 버전 Realm은 class_ 접두어를 모든 클래스 이름에 추가하고 브라우저에서 일부 확인할 수 있습니다.
  2. 필드 이름의 길이는 63자로 제한됩니다.
  3. 날짜는 초 단위로 줄입니다. 32 비트와 64 비트 디바이스 간에 호환성을 유지하기 위해서 1900-12-13 이전의 날짜와 2038-01-19 이후의 날짜는 저장할 수 없습니다.
  4. 중첩된 트랜잭션은 지원하지 않고 발견 시 예외 처리됩니다.
  5. String 과 byte array(byte[])는 16MB를 넘을 수 없습니다.
  6. 대소문자를 구분하는 문자열 검색은 ‘Latin Basic’, ‘Latin Supplement’, ‘Latin Extended A’, ‘Latin Extended B’ (UTF-8 range 0-591) character set에서만 허용됩니다.

객체

Proxy 클래스가 모델 클래스의 getter와 setter를 override 방식으로 인하여 모델 클래스에서 할 수 있는 것에는 아래와 같은 제약이 있습니다.

  • private instance 필드 만을 허용
  • 기본 getter, setter method만 허용
  • public 이나 private에 대해 static 필드만 허용
  • Static method 만 허용
  • method가 없는 interface의 구현 만 허용

이는 현재로서는 RealmObject를 extent 하거나 toString() or equals()와 같은 method를 override 하는 것 이외에는 불가능 하다는 것을 뜻합니다. 또한 인터페이스를 구현하는 것 만이 가능합니다. 현재 이 제약사항을 없애기 위한 작업 을 진행하고 있습니다.

정렬

정렬은 현재 ‘Latin Basic’, ‘Latin Supplement’, ‘Latin Extended A’, ‘Latin Extended B’ (UTF-8 range 0-591) 문자셋만 지원합니다. 다른 문자셋인 경우 RealmResults 객체에 반영되지 않습니다.

대소문자 구분 질의

equalTo, contain, endsWith, beginsWith 에서 caseSensitive flag를 설정하셔도 locale이 English일 때에만 동작합니다. 이 제약 사항에 대해서 여기에서 자세히 읽으실 수 있습니다.

쓰레드

동시에 다수의 쓰레드에서 Realm 파일에 접근할 수 있지만 쓰레드 간에 Realm, RealmObject, RealmQuery, RealmResults를 다룰 수 없습니다. 게다가 비동기적인 질의는 현재 지원하지 않습니다. 다중쓰레드 환경에서 Realm 사용법은 쓰레드 예제에서 볼 수 있습니다.

마이그레이션

마이그레이션 지원은 아직 부족하고 많은 내부 클래스 구현에 의존하셔야 합니다. 장기적으로 사용하기 편한 마이그레이션 지원을 계획하고 있지만 현재로써는 마이그레이션 예제가 도움이 될 겁니다.

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

Realm 파일은 동시적으로 여러 스레드에서 접근할 수 있지만 한 번에 하나의 프로세스에서만 접근할 수 있습니다. 다른 프로세스는 Realm 파일을 복사하거나 그들의 새로운 파일을 만들어야 합니다. 다중 프로세스 지원은 곧 나올 예정입니다.

모범 사용 예

ANR (Application Not Responding) 에러 예방하기

Realm은 일반적으로 안드로이드의 메인 스레드에서 읽고 쓸 만큼 빠릅니다. 하지만 쓰기 트랜젝션은 여러 스레드에서 블록을 유래할 수 있고 우연한 ANR을 막기 위해 모든 쓰기 작업은 메인 스레드가 아닌 백그라운드에서 수행하는 것이 권장됩니다. 백그라운드 스레드에서 연산을 수행하는 것은 Realm 비동기 트랜잭션을 참고하세요.

Realm 인스턴스들의 생명주기 관리하기

RealmObjectsRealmResults는 데이터 전체를 느긋하게 가져옵니다. 이런 이유로 Realm 오브젝트나 질의 결과를 접근할 때 가능한 오래 Realm 인스턴스를 유지하는 것이 중요합니다. Realm 데이터 커넥션을 열고 닫는 추가 비용을 줄이기 위해 레퍼런스 카운트화된 캐시를 가집니다. 이는 Realm.getDefaultInstance()를 같은 스레드에서 여러 번 호출하는 것은 비용이 들지 않고 내부의 리소스는 자체적으로 모든 인스턴스가 닫히면 해제됨을 의미합니다.

모든 액티비티와 프래그먼트의 UI 스레드에서 Realm 인스턴스를 열고 Activity나 Fragment가 파괴될 때 닫는 것은 쉽고 안전한 접근 법입니다.

// 애플리케이션에서 Realm 설정하기
public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        RealmConfiguration realmConfiguration = new RealmConfiguration.Builder(this).build();
        Realm.setDefaultConfiguration(realmConfiguration);
    }
}

// 액티비티들을 전환하며 onCreate()/onDestroy()가 중첩되면 Activity 2의 onCreate가
// Activity 1의 onDestroy()보다 먼저 호출 됩니다.
public class MyActivity extends Activity {
    private Realm realm;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        realm = Realm.getDefaultInstance();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        realm.close();
    }
}

// 프래그먼트에서 onStart()/onStop()를 사용합니다.
// 프래그먼트의 onDestroy()는 호출되지 않을 수 있습니다.
public class MyFragment extends Fragment {
    private Realm realm;

    @Override
    public void onStart() {
        super.onStart();
        realm = Realm.getDefaultInstance();
    }

    @Override
    public void onStop() {
        super.onStop();
        realm.close();
    }
}

RealmResults와 RealmObject를 재사용하기

모든 RealmObjectRealmResults는 Realm이 갱신되면 자동으로 갱신됩니다. 이에 따라 RealmChangedListener에 반응해서 객체들을 다시 가져올 필요는 없습니다. 객체는 이미 갱신되었고 화면에 다시 그려질 준비가 되었습니다.

public class MyActivity extends Activity {

    private Realm realm;
    private RealmResults<Person> allPersons;
    private RealmChangeListener realmListener = new RealmChangeListener() {
        @Override
        public void onChange() {
            // 그냥 다시 뷰를 그립니다. `allPersons`는 이미 최신 데이터를
            // 가지고 있습니다.
            invalidateView();
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        realm = Realm.getDefaultInstance();
        realm.addRealmChangeListener(listener);
        allPerson = realm.where(Person.class).findAll(); // "라이브" 질의 결과 만들기
        setupViews(); // 뷰 초기화 설정
        invalidateView(); // 데이터에 따라 뷰를 다시 그리기
    }

    // ...
}

Realm 레시피

우리는 Realm을 어떤 업무들을 달성하기 위해 어떻게 쓰는지 다루는 모든 레시피를 한 번에 모아두었습니다. 우리는 정기적으로 더 추가할 것이니 종종 되돌아 확인해보세요. 보고 싶은 예제가 있다면 우리에게 알려주세요. help@realm.io

FAQ

어떻게 Realm 파일을 찾고 내부 콘텐츠를 볼 수 있나요?

StackOverflow 질문 에 Realm 파일을 찾는 방법이 소개되어 있습니다. Realm 브라우저를 통해서 내부 콘텐츠를 볼 수 있습니다.

Realm 라이브러리는 크기가 얼마인가요?

앱을 Release 타깃으로 빌드하고 배포했을 경우에 Realm이 앱에서 차지하는 크기는 최대 800KB에 지나지 않습니다. 배포된 Realm은 더 많은 아키텍트(ARM7, ARMv7, ARM64, x86, MIPS)를 지원하기 때문에 크기가 좀 더 큽니다. APK 파일은 모든 지원 가능한 아키텍트를 포함하지만 Android 인스톨러는 오직 해당 디바이스 아키텍트의 네이티브 코드만을 설치합니다. 결과적으로 설치된 앱은 APK 파일보다 크기가 줄어듭니다.

개별 아키텍처 버전으로 나누어 안드로이드 APK 자체의 크기를 줄일 수 있습니다. build.gradle에 다음을 추가하면 Android Build Tool ABI 분할 지원을 이용해 안드로이드 APK를 분리할 수 있습니다.

splits {
    abi {
        enable true
        reset()
        include 'armeabi', 'armeabi-v7a', 'arm64-v8a', 'mips', 'x86', 'x86_64'
    }
}

독립된 APK에 포함될 아키텍처를 선택하면 별도로 빌드 됩니다. 자세한 내용을 알고 싶다면 Android Tools 문서를 참고하세요.

예제는 GitHub에 포함되어 있습니다.

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

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

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

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

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

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

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

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

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

Java 객체와 Realm 객체의 차이가 무엇인가요?

Java 객체는 데이터를 보유하고 있고 Realm 객체는 데이터를 보유하지 않는 대신에 데이터베이스로부터 직접 속성에 접근합니다.

두 가지 차이점이 있습니다. Realm 객체는 일반적으로 Java 객체에 비해 경량화되어 있습니다. Realm 객체는 자동적으로 최신의 데이터를 유지하지만 Java 객체는 수동으로 수정해야 합니다.

왜 모델 클래스는 RealmObject를 확장해야 하나요?

Realm 특화 기능을 여러분의 모델 클래스에 추가하기 위해서입니다. 현재는 removeFromRealm() 가 추가되었고 향후 다른 메서드들이 추가될 예정입니다. 그리고 RealmObject 상속이 우리 API의 일반적인 부분을 사용할 수 있게 하고 읽고 쓰기 쉽게 합니다.

*RealmProxy 클래스들은 무엇인가요?

RealmProxy 클래스들은 Realm 객체가 어떤 데이터도 자체적으로 가지고 있지 않게 확신하는 방법입니다. 대신 데이터베이스의 데이터에 직접적으로 접근합니다.

프로젝트의 모든 객체 모델 클래스에서 Realm 어노테이션 프로세서는 관련된 RealmProxy 객체를 만들어 냅니다. 이 클래스는 여러분의 클래스를 확장하며 Realm.createObject()을 호출하면 반환되는 것입니다. 하지만 IDE의 관점에서는 차이를 느끼기 어렵습니다.

모든 필드를 위해 getter와 setter가 필요한가요?

프록시 클래스가 처리하는 과정때문입니다.

모든 필드 접근이 데이터의 복사 대신에 데이터베이스를 사용하도록 명시하기 위해서 모든 필드 접근자는 프록시 클래스로 재정의되어야 합니다. Java에서는 게터, 세터, private 필드만 가능합니다.

전체 프라이빗 메서드 내에서 게터와 세터를 써야하는 것은 Comparable와 같은 인터페이스를 구현하는 것에 영향을 줍니다.

이것은 이상적인 상황이 아니고 이런 요구 사항을 제거하고 싶습니다. AspectJ와 Javassist와 같은 솔루션들이 가능합니다만 다양한 가능성을 찾아보고 있습니다.

프록시 클래스가 게터와 세터를 오버라이드한 결과 우리는 커스텀 버전의 게터와 세터를 만들 수 없습니다. 우회할 방법은 (@Ignore 어노테이션)을 사용하여 무시할 필드를 정의하고 그것에 대한 게터와 세터를 커스터마이즈 용으로 쓰는 것입니다. 모델 클래스는 다음과 같습니다.

package io.realm.entities;

import io.realm.RealmObject;
import io.realm.annotations.Ignore;

public class StringOnly extends RealmObject {

    private String name;

    @Ignore
    private String kingName;

    // custom setter
    public void setKingName(String kingName) { setName("King " + kingName); }

    // custom getter
    public String getKingName() { return getName(); }

    // setter and getter for 'name'
}

setName() 대신에 커스텀 세터 setKingName()를 쓸 수도 있습니다. 커스텀 세터가 setName()를 사용하면 필드에 직접 대입이 안됩니다.

RealmOjbect를 직접 생성할 수 없나요?

RealmObject는 데이터베이스의 데이터에 대응되는 프록시입니다. 정확하게 동작하기 위해서는 Realm으로 관리해야 합니다.

RealmList와 같이 모델 객체를 Realm 객체에 대응시키는 기능을 추가할 예정입니다.

Realm 객체를 쓸 때 트랜잭션을 사용해야 하나요?

트랜잭션은 하나의 원자 오퍼레이션으로 여러 필드가 수정되는 걸 명시하기 위해 사용합니다. 완전히 정상적으로 종료할지 취소할지(에러 혹은 롤백) 수정의 범위를 정의해야 합니다. 트랜잭션 범위를 명시하면서 얼마나 자주(혹은 빨리) 수정 사항을 반영할지 조절할 수 있습니다. (예. 한 오퍼레이션에서 여러 객체 삽입)

SQLite 같은 일반적인 SQL 기반의 데이터베이스에서 삽입을 할 때 한 번에 여러 필드를 삽입합니다. 자동적으로 트랜잭션 내에서 처리되지만 사용자에게 보이진 않습니다. Realm에서는 모든 부분에 명시적입니다.

out-of-memory exception 이 발생하면 어떻게 해야 합니까?

Android용 Realm은 내부의 스토리지 엔진을 사용합니다. 이 스토리지 엔진은 JVM 힙이 아니라 네이티브 메모리에 할당합니다. 스토리지 엔진이 ‘네이티브’ 메모리 할당에 실패하거나 파일 시스템이 가득 차면 Realm은 io.realm.internal.OutOfMemoryError 예외를 발생하고 앱은 메모리가 꽉 차게 됩니다. 이런 메시지를 비어있는 catch 블록이나 광범위한 catch로 무시하지 않는 게 중요합니다. 만약 앱이 계속 동작하고 Realm 파일에 접근하면 충돌났거나 불일관성인 상태에 접근하게 됩니다. 이 io.realm.internal.OutOfMemoryError 경우, 앱을 종료하는 것이 안전합니다.

커다란 Realm 파일 사이즈

일반적으로, Realm 데이터베이스 파일이 SQLite 데이터베이스 파일보다 적은 공간을 차지합니다.

데이터에 대한 일관적인 접근을 위해서 Realm은 다양한 realm 버전에서 동작합니다. realm에서 데이터를 읽고 다른 쓰레드에서 realm에 쓰기를 하는 동안 오래 도는 동작으로 인해 쓰레드를 막고 있으면, 해당 버전은 업데이트되지 않고 원치 않는 Realm은 중간 상태 버전의 데이터에서 멈추게 되어 파일 사이즈가 지속적으로 증가하게 됩니다. (이 빈 공간은 언젠가는 미래의 쓰기 동작에서 사용되거나 compactRealmFile를 호출 함으로써 제거할 수 있습니다.)

‘Annotation processor may not have been executed.’이 무슨 뜻인가요?

app을 컴파일 하는 과정에서 모델 클레스을 프로세싱하여 proxy 클래스가 생성됩니다. 만야 annotation 하는 과정이 실패하면, 해당 method의 proxy 클래스는 찾을 수 없게 됩니다. app이 실행되면, Realm은 ‘Annotation processor may not have been executed.’ 메시지를 던지게 됩니다. 또만 Java 6를 사용하여도 이런 경우를 보게 되는데, 이는 annotation의 상속을 지원하지 않기 때문입니다. 모델 이전에 @RealmClass를 추가하셔야 합니다. 아니면 모든 생성된 파일 또는 중간 파일들을 삭제/clean up 하고 새로 빌드하면 해결되는 경우도 있습니다.

암호화가 이 장비에서 예외를 발생시켜요.

Realm 암호화 구현은 시그널 핸들러에 기반하고 있었습니다. 옛날 단말기 (예를 들어 HTC One X)에서는 시그널 핸들러가 제대로 동작하지 않았습니다. v0.82.2 버전의 Realm부터 암호화된 Realm을 열었을 때 문제가 발생하는 단말을 찾기 시작하였고 이런 단말에서 RealmEncryptionNotSupportedException 예외를 던졌습니다.

v0.85.0 버전부터 Realm의 암호화 구현은 모든 단말을 지원하기 위해 완전히 새로 작성되었습니다. 이제 RealmEncryptionNotSupportedException는 필요가 없기 때문에 v0.85.0 버전부터는 삭제하였습니다.

앱을 실행하면 Mixpanel로 네트워크 연결이 발생하는데 이게 뭐예요?

Realm은 소스코드의 어노테이션 프로세서가 수행될 때 익명으로 통계를 수집합니다. 완전하게 익명이며 어떤 버전의 Realm을 쓰고 어떤 운영체제를 쓰는지 어떤 환경에서는 더 이상 사용되지 않는지를 확인하여 제품의 개선을 돕습니다. 이 호출은 프로덕션 버전에서는 발생하지 않으며 개발자의 장비에서만 호출이 발생합니다. 개발자의 소스코드가 어노테이션을 사용하기 때문이죠. 실제로 어떻게 무슨 정보를 모으는지는 저희의 소스 코드에서 확인할 수 있습니다.

“librealm-jni.so” 파일을 불러오지 못해요.

앱이 64 비트 아키텍처를 지원하지 못하는 다른 네이티브 라이브러리를 사용하고 있다면 안드로이드에서 Realm은 ARM64 장비에 librealm-jni.so를 불러오는데 실패합니다. 안드로이드는 32 비트 네이티브 라이브러리와 64 비트 네이티브 라이브러리를 동시에 불러오지 못합니다. 최상의 방법은 모든 라이브러리의 지원 ABIs 집합을 동일하게 제공하는 것입니다. 하지만 써드 파티 라이브러리를 사용함에 있어서 그렇게 하기 힘든 경우가 있습니다. VLC and realm Library conflicts와의 문제를 참고하세요.

우회하는 방법은 앱의 build.gradle에 아래 코드를 추가하여 APK 파일에서 ARM64 라이브러리를 제거하는 방법이 있습니다. 자세한 정보를 보려면 안드로이드의 32 비트와 64 비트 의존성을 참고하세요.

android {
    //...
    packagingOptions {
        exclude "lib/arm64-v8a/librealm-jni.so"
    }
    //...
}

또 .so 파일을 jar 파일에 부적절하게 패키징하는 안드로이드 그래들 플러그인 1.4.0 베타 버그도 있습니다. Realm Java 이슈 1421를 참고하세요. 안드로이드 그래들 플러그인 1.3.0으로 돌아가가거나 1.5.0 이상의 버전을 쓰면 문제를 해결할 수 있습니다.