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

Realm 자바는 손쉽게 앱 모델 영역을 안전하고, 영속적이고 빠르게 만듭니다. 아래 코드 예제를 확인해보세요.

// RealmObject를 확장하여 모델을 정의합니다
public class Dog extends RealmObject {
    private String name;
    private int age;

    // ... 생성된 getter와 setter
}
public class Person extends RealmObject {
    @PrimaryKey
    private long id;
    private String name;
    private RealmList<Dog> dogs; // 일 대 다 관계를 정의합니다

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

// 일반적인 자바 객체처럼 사용합니다
Dog dog = new Dog();
dog.setName("Rex");
dog.setAge(1);

// Realm을 초기화합니다.
Realm.init(context);

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

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

// 트랜잭션을 통해 데이터를 영속화합니다
realm.beginTransaction();
final Dog managedDog = realm.copyToRealm(dog); // 비관리 객체를 영속화합니다
Person person = realm.createObject(Person.class); // 관리 객체를 직접 만듭니다 
person.getDogs().add(managedDog);
realm.commitTransaction();

// 데이터가 변하면 리스너들에게 알림이 갑니다
puppies.addChangeListener(new OrderedRealmCollectionChangeListener<RealmResults<Dog>>() {
    @Override
    public void onChange(RealmResults<Dog> results, OrderedCollectionChangeSet changeSet) {
        // 질의 결과는 세밀한 알림과 함께 실시간으로 갱신됩니다
        puppies.size(); // => 1
    }
});

// 백그라운드에서 비동기적으로 갱신합니다
realm.executeTransactionAsync(new Realm.Transaction() {
    @Override
    public void execute(Realm bgRealm) {
        Dog dog = bgRealm.where(Dog.class).equalTo("age", 1).findFirst();
        dog.setAge(3);
    }
}, new Realm.Transaction.OnSuccess() {
    @Override
    public void onSuccess() {
    	// 원래 질의와 Realm 객체는 자동으로 갱신됩니다 
    	puppies.size(); // => 0. 2살 미만의 강아지가 더 이상 없기 때문에
    	managedDog.getAge(); // => 3. 강아지의 나이가 갱신되었습니다 
    }
});

시작하기

Android Realm 다운로드

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

요구사항

  • 1.5.1 이상 버전의 Android Studio
  • 최신 버전의 Android SDK
  • 7.0 이상 JDK 버전
  • API Level 9 이상의 모든 Android 버전을 지원합니다. (Android 2.3 진저브레드 버전 이상)

주의: 현재 Realm은 Android 이외의 Java를 지원하지 않습니다. IDE로 Eclipse 역시 지원하지 않으므로 Android Studio를 사용해 주세요.

설치하기

Realm은 Gradle 플러그인으로 설치됩니다.

단계 1: 아래의 클래스 패스 의존성을 프로젝트 수준 build.gradle 파일에 추가합니다.

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath "io.realm:realm-gradle-plugin:3.1.2"
    }
}

프로젝트 수준의 build.gradle 파일은 여기에 위치합니다.

Project level build.gradle file

단계 2: realm-android 플러그인을 애플리케이션 수준 build.gradle 파일에서 적용시킵니다.

apply plugin: 'realm-android'

한번 이 두 가지가 변경이 되면 단순히 그래들 의존성을 리프레쉬하세요. 만약 v0.88 이전 버전에서 업그레이드한다면 남겨진 기존 설치 파일을 제거하기 위해 그래들 프로젝트를 클린 할 필요가 있습니다. (./gradlew clean)

수정된 두 build.gradle 파일에 대한 샘플은 여기에서 찾을 수 있습니다.

다른 빌드 시스템

Maven과 Ant 빌드 시스템은 지원되지 않습니다. 이 빌드 시스템의 지원에 관심이 있다면 아래 이슈에서 관심을 표현해주세요.

ProGuard

ProGuard 설정이 Realm 라이브러리에 포함되어있습니다. Realm에 특화된 ProGuard 규칙을 추가할 필요가 없다는 의미입니다.

Realm 브라우저

.realm 데이터베이스를 읽고 수정하기 위해서 맥 앱 스토어에서 애플리케이션을 받아 사용하실 수 있습니다.

Realm Browser

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

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

Realm 브라우저는 맥 앱스토어에서 찾을 수 있습니다. GitHub 페이지에서 다운로드할 수도 있습니다.

윈도우즈와 리눅스에서는 현재 브라우저가 지원되지 않습니다. 다른 플랫폼에서 사용한다면 대신에 Stetho-Realm를 사용하세요. Stetho는 Facebook이 만든 크롬 브라우저를 위한 안드로이드 디버그 브릿지입니다.

API 문서

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

예제

실제 앱에서 Realm이 실제적으로 쓰이는 코드를 확인하기 위해서는 예제에서 확인하세요. 예제를 수행하는 자세한 내용은 여기를 보세요.

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

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

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

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

adapterExampleRealmListView, RecyclerView를 우아하게 같이 쓰기 위해 어떻게 RealmBaseAdapterRealmRecyclerViewAdapter를 써야하는지 보여줍니다.

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

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

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

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

도움을 구하려면

모델

Realm 모델 클래스는 RealmObject 기반 클래스를 상속받아서 생성합니다.

public class User extends RealmObject {

    private String          name;
    private int             age;

    @Ignore
    private int             sessionId;

    // 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; }
}

Realm 모델 클래스는 또 커스텀 메서드에 대한 public, protected, private 필드를 지원합니다.

public class User extends RealmObject {

    public String name;

    public boolean hasLongName() {
      return name.length() > 7;
    }

    @Override
    public boolean equals(Object o) {
      // Custom equals comparison
    }
}

필드 타입

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

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

Required 필드와 널 값

null이 필드의 값으로 부적당한 경우가 있습니다. @Required 어노테이션을 null 값을 허용하지 않기를 강제하기 위해 사용할 수 있습니다. 단지 Boolean, Byte, Short, Integer, Long, Float, Double, String, byte[], Date만 Required로 지정될 수 있습니다. 다른 형에 대해 @Required 어노테이션이 주어지면 컴파일을 실패합니다. 기본형과 RealmList 형에 대한 필드들은 암묵적으로 Required로 취급됩니다. RealmObject 형의 필드들은 항상 널이 가능합니다.

무시된 속성

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

자동 갱신 객체

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

realm.executeTransaction(new Realm.Transaction() {
    @Override
    public void execute(Realm realm) {
        Dog myDog = realm.createObject(Dog.class);
        myDog.setName("Fido");
        myDog.setAge(1);
    }
});
Dog myDog = realm.where(Dog.class).equalTo("age", 1).findFirst();

realm.executeTransaction(new Realm.Transaction() {
    @Override
    public void execute(Realm realm) {
        Dog myPuppy = realm.where(Dog.class).equalTo("age", 1).findFirst();
        myPuppy.setAge(2);
    }
});

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

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

색인된 속성

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

기본 키

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

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

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

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

final MyObject obj = new MyObject();
obj.setId(42);
obj.setName("Fish");
realm.executeTransaction(new Realm.Transaction() {
    @Override
    public void execute(Realm realm) {
        // 이것은 Realm의 새 클래스를 만들거나 이미 객체가 있다면 예외를 던집니다.
        // (동일한 기본 키)
        // realm.copyToRealm(obj);

        // 이는 같은 기본 키가 있다면 기존 객체를 갱신하거나
        // 기본 키 = 42인 객체가 없다면 새로운 객체를 만듭니다
        realm.copyToRealmOrUpdate(obj);
    }
});

문자열 (String)과 박스 정수형 (Byte, Short, Integer, Long)으로 된 기본 키는 @PrimaryKey 어노테이션과 함께 @Required 어노테이션을 쓰지 않은 한 null 값을 가질 수 있습니다.

객체 커스터마이징

RealmObject를 거의 POJO 처럼 쓸 수 있습니다. RealmObject를 확장하고 모든 필드를 공개하고 세터와 게터 대신에 대입하기만 하면 됩니다. 이런 모델 클래스의 예입니다.

public class Dog extends RealmObject {
    public String name;
    public int age;
}

Dog을 다른 클래스처럼 사용할 수 있습니다. 관리된 Dog 객체를 Realm에서 만드려면 createObject()copyToRealm() 메서드를 쓸 수 있습니다.

realm.executeTransaction(new Realm.Transaction() {
    @Overrride
    public void execute(Realm realm) {
        Dog dog = realm.createObject(Dog.class);
        dog.name = "Fido";
        dog.age  = 5;
    }
};

요구사항에 더 맞도록 세터와 게터들에 로직을 추가할 수 있습니다. Realm에 값을 저장하기 전에 검증을 하는 등의 용도로 유용합니다. 또 RealmObject에 커스텀 메서드도 쉽게 추가할 수 있습니다.

RealmModel 인터페이스

RealmObject 기반 클래스를 상속받는 대신 RealmModel 인터페이스를 구현하고 @RealmClass 어노테이션을 추가할 수 있습니다.

@RealmClass
public class User implements RealmModel {

}

RealmObject의 모든 메서드는 스태틱 메서드를 통해 사용할 수 있습니다.

// RealmObject을 쓸 때
user.isValid();
user.addChangeListener(listener);

// RealmModel을 쓸 때
RealmObject.isValid(user);
RealmObject.addChangeListener(user, listener);

관계

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

public class Email extends RealmObject {
    private String address;
    private boolean active;
    // ... 세터와 게터들이 위치합니다
}

public class Contact extends RealmObject {
    private String name;
    private Email email;
    // ... 세터와 게터들이 위치합니다
}

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

다 대 일

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

public class Contact extends RealmObject {
    private Email email;
    // 다른 필드들...
}

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

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

다 대 다

RealmList<T> 필드 선언을 통해서 하나의 객체에서 어떤 수의 객체와 관계를 설정할 수 있습니다. 예로 연락처에 여러 이메일 주소가 있다고 가정해봅시다.

public class Contact extends RealmObject {
    public String name;
    public RealmList<Email> emails;
}

public class Email extends RealmObject {
    public String address;
    public boolean active;
}

RealmList는 기본적으로 RealmObject들의 컨테이너입니다. RealmList는 일반적인 자바의 List와 매우 흡사하게 행동합니다. 다른 RealimList들에서 같은 객체를 두번 (이상) 쓰는데 제한은 없습니다. 이를 두 개의 일대다 관계로 만들 수도 있고 다대다 관계로 만들 수 있습니다.

객체들을 만들고 Email 객체들을 Contact 객체에 추가하기 위해 RealmList.add()를 쓸 수 있습니다.

realm.executeTransaction(new Realm.Transaction() {
    @Override
    public void execute(Realm realm) {
        Contact contact = realm.createObject(Contact.class);
        contact.name = "John Doe";

        Email email1 = realm.createObject(Email.class);
        email1.address = "john@example.com";
        email1.active = true;
        contact.emails.add(email1);

        Email email2 = realm.createObject(Email.class);
        email2.address = "jd@example.com";
        email2.active = false;
        contact.emails.add(email2);
    }
});

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

public class Person extends RealmObject {
    public String name;
    public RealmList<Person> friends;
    // 다른 필드들...
}

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;
  // 게터와 세터들 ...
}

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

Table Diagram

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

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

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

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

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

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

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

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

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

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

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

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

첫 번째 질의가 ‘Fluffy’란 이름을 가진 개들을 가진 모든 Person들을 찾고 Brown인 모든 개들을 가진 Person들을 찾아서 교집합을 돌려줍니다.

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

r2의 배후는 조금 다릅니다. 이 질의를 나누어 봅시다. 질의의 첫 번째 부분은 이렇습니다. RealmResults<Person> r2a = realm.where(Person.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가 유일하기 때문입니다.

쓰기

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

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

// Realm 인스턴스를 얻습니다
Realm realm = Realm.getDefaultInstance();

realm.beginTransaction();

//... 객체를 여기에서 추가하거나 갱신합니다 ...

realm.commitTransaction();

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

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

//  ...

realm.cancelTransaction();

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

트랜잭션 중 Exception을 만나면 Realm 자체는 오류가 생길 수 있는 경우를 대비해서 Realm은 충돌에 안전합니다. 충돌이 발생하면 현재 트랜잭션의 데이터만 잃게 됩니다. Exception을 잡고 앱을 수행하기 위해 트랜잭션을 취소하는 것이 중요합니다. 만약에 executeTransaction()를 이용하는 경우에는 이 과정이 자동으로 진행됩니다.

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

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

객체 생성하기

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

realm.beginTransaction();
User user = realm.createObject(User.class); // 새 객체 만들기
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");

// 객체를 Realm으로 복사하기. 어떤 추가적인 변경도 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.executeTransactionAsync(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.OnSuccess() {
            @Override
            public void onSuccess() {
                // 트랜잭션이 성공하였습니다.
            }
        }, new Realm.Transaction.OnError() {
            @Override
            public void onError(Throwable error) {
                // 트랜잭션이 실패했고 자동으로 취소되었습니다.
            }
        });

OnSuccessOnError 콜백은 모두 선택적입니다. 그들이 제공되면 트랜잭션이 성공적으로 완료되거나 실패하면 각자 실행됩니다. 콜백은 Looper에 의해 제어되고 그들은 Looper 스레드에서만 허용됩니다.

RealmAsyncTask transaction = realm.realm.executeTransactionAsync(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();
    }
}

문자열과 바이트 배열 갱신하기

Realm은 전체 필드와 동작하고 문자열이나 바이트 배열의 개별 요소를 갱신하는 게 불가능합니다. 문자열의 다섯 번째 요소를 갱신한다면 아래와 같이 할 수 있습니다.

realm.executeTransaction(new Realm.Transaction() {
    @Override
    public void execute(Realm realm) {
        bytes[] bytes = realmObject.binary;
        bytes[4] = 'a';
        realmObject.binary = bytes;
    }
});

Realm의 MVCC 아키텍처가 기존의 데이터를 변경하는 것을 막아 다른 스레드와 프로세스가 안정된 상태의 데이터를 읽기 위함입니다.

질의

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

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

User 클래스를 사용합니다.

public class User extends RealmObject {

    @PrimaryKey
    private String          name;
    private int             age;

    @Ignore
    private int             sessionId;

    // 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);

// 질의 조건을 추가합니다
query.equalTo("name", "John");
query.or().equalTo("name", "Peter");

// 질의를 수행합니다
RealmResults<User> result1 = query.findAll();

// 같은 일들을 한 번에 합니다 ("Fluent interface"):
RealmResults<User> result2 = realm.where(User.class)
                                  .equalTo("name", "John")
                                  .or()
                                  .equalTo("name", "Peter")
                                  .findAll();

이 명령은 John 혹은 Peter라는 이름을 가진 사용자가 포함된 RealmResults 객체를 반환합니다. findAll() 메서드가 호출될 때 질의가 실행됩니다. 이 메서드는 findAll 메서드들의 일원으로, 예를 들어 findAllSorted()는 정렬 결과 집합을 반환하고, findAllAsync()은 백그라운드 스레드에서 비동기적으로 질의를 수행합니다. 전체 세부 사항을 보려면 API 참조 문서를 확인하세요.

객체는 복사되지 않으며, 일치하는 객체에 대한 참조 목록을 얻고 질의와 일치하는 원래 객체로 직접 작업합니다. RealmResults는 Java의 AbstractList를 상속받고 유사한 방법으로 동작합니다. 예를 들어 RealmResults를 정렬하고 개별 객체에 색인을 통해 접근할 수 있습니다.

질의와 일치하는 항목이 없는 경우 반환되는 RealmResults 객체는 null이 아니지만 size() 메서드는 0을 반환합니다.

RealmResults 집합에서 객체를 수정하거나 삭제하려면 반드시 쓰기 트랜잭션에서 수행해야 합니다.

관계 간 질의도 할 수 있습니다.

필터링

where() 메서드를 사용해서 RealmQuery 객체 표현에서 질의를 시작할 수 있습니다. RealmQuery가 있으면 다양한 술어(predicate)를 사용해서 질의가 반환한 데이터를 필터링할 수 있습니다. 대부분의 술어를 이해하기 쉽게 명명했습니다.

술어의 첫 번째 인수는 필드 이름입니다. 만약 술어가 해당 필드 타입을 지원하지 않으면 예외가 발생합니다. 보다 자세한 내용은 RealmQuery를 참조하세요.

모든 데이터 유형에 다음 술어를 사용할 수 있습니다.

  • equalTo()
  • notEqualTo()

필드를 값 리스트와 대조하려면 in()을 사용합니다. 예를 들어 “Jill”, “William”, “Trillian”이라는 이름을 찾으려면 in("name", new String[]{"Jill", "William", "Trillian"})이라고 사용합니다. in() 술어는 문자열, 바이너리 데이터, 숫자 필드에 적용할 수 있습니다.

Date를 포함한 숫자 데이터 유형에는 추가적으로 다음 술어를 사용할 수 있습니다.

  • between() (양 끝 숫자가 포함됩니다. 즉, 경계가 있는 간격에 사용합니다.)
  • greaterThan()
  • lessThan()
  • greaterThanOrEqualTo()
  • lessThanOrEqualTo()

문자열 필드에는 추가적으로 다음 술어를 사용할 수 있습니다.

  • contains()
  • beginsWith()
  • endsWith()
  • like()

네 개의 문자열 술어 모두 대소문자를 구분하기 위해 선택적으로 사용할 수 있는 세 번째 인수가 있습니다. 대소문자를 무시하려면 Case.INSENSITIVE를, 대소문자를 구분하려면 Case.SENSITIVE를 사용하세요. 기본값은 Case.SENSITIVE입니다.

like()와 같은 술어에 glob 스타일의 와일드카드로 찾을 수 있습니다. 찾는 패턴은 문자와 하나 이상의 와일드카드로 구성됩니다.

  • * 0개 이상의 Unicode 문자를 찾습니다.
  • ? 단일 Unicode 문자를 찾습니다.

William, Bill, Jill, Trillian이라는 값을 가진 name 필드를 가진 네 개의 객체가 Realm에 있는 경우를 예로 들겠습니다. like("name", "?ill*") 술어는 앞의 세 객체와 일치하며, like("name", "*ia?")는 첫 번째와 마지막 객체와 일치합니다.

바이너리 데이터, 문자열, RealmObject의 (RealmList) 리스트는 비어있을 수 있으며, 길이가 0일 수 있습니다. 다음 술어로 비어있는지 확인할 수 있습니다.

  • isEmpty()
  • isNotEmpty()

필수적인 필드가 아닌 경우, null 값을 가질 수 있습니다. 특히 RealmObject 필드는 필수가 아니므로 null이 될 수 있습니다. null 값을 찾으려면 다음 술어를 사용합니다.

  • isNull()
  • isNotNull()

논리 연산자

각 조건은 암묵적으로 논리 AND로 연결됩니다. 논리 OR로 연결하려면 명시적으로 or()를 사용하세요.

public class User extends RealmObject {

    @PrimaryKey
    private String          name;
    private int             age;

    @Ignore
    private int             sessionId;

    // 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)  // 암묵적인 AND
                            .beginGroup()
                                .equalTo("name", "Peter")
                                .or()
                                .contains("name", "Jo")
                            .endGroup()
                            .findAll();

not()으로 부정 조건을 만들 수 있습니다. not() 연산자는 단지 하위 조건을 부정하기 위해 beginGroup()/endGroup()와 함께 쓰일 수 있습니다. “Peter”나 “Jo”가 아닌 이름의 User를 모두 찾는 질의는 다음과 같습니다.

RealmResults<User> r = realm.where(User.class)
                           .not()
                           .beginGroup()
                                .equalTo("name", "Peter")
                                .or()
                                .contains("name", "Jo")
                            .endGroup()
                            .findAll();

이런 쿼리에서는 in()을 사용하는 것이 훨씬 편리합니다.

RealmResults<User> r = realm.where(User.class)
                           .not()
                           .in("name", new String[]{"Peter", "Jo"})
                           .findAll();

정렬

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

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

정렬은 기본적으로 오름차순이며, 이를 변경하려면 두 번째 선택적 인수로 Sort.DESCENDING를 사용합니다. 여러 필드를 동시에 사용해서 정렬할 수도 있습니다.

고유 값

고유한 값만 반환하려면 distinct() 술어를 사용합니다. 예를 들어 다음 예제로 Realm에 얼마나 많은 고유한 이름이 있는지 알 수 있습니다.

RealmResults<Person> unique = realm.where(Person.class).distinct("name");

이 기능은 정수나 문자열 필드만 지원합니다. 지원하지 않는 필드 타입에 distinct()를 사용하면 예외가 발생합니다. 정렬과 마찬가지로 고유 값도 여러 필드를 지정할 수 있습니다.

연속 질의

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

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();

연속된 질의가 RealmQuery가 아닌 RealmResults에 기반하고 있다는 것을 유의하세요. 더 많은 조건을 기존 RealmQuery에 추가한다면 이것은 질의를 수정하는 것이고 연속이 아닙니다. 더 자세한 내용은 연결 질의을 참고하세요.

자동 갱신 결과

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

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

realm.executeTransaction(new Realm.Transaction() {
    @Override
    void public execute(Realm realm) {
        Dog dog = realm.createObject(Dog.class);
        dog.setName("Fido");
        dog.setAge(1);
    }
});

puppies.addChangeListener(new RealmChangeListener() {
    @Override
    public void onChange(RealmResults<Dog> results) {
      // results와 puppies는 같은 시간에 값이 올라갑니다
      results.size(); // => 1
      puppies.size(); // => 1
    }
});

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

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

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

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

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

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

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

집합

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

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();

반복자와 스냅샷

모든 Realm 컬렉션은 자동 갱신됩니다. 항상 최신 상태를 반영합니다. 대부분의 경우 이 방식이 바람직하지만 원소를 수정하기 위해서 컬렉션을 순회하는 경우라면 어떨까요? 아래 예제와 같은 경우입니다.

RealmResults<Person> guests = realm.where(Person.class).equalTo("invited", false).findAll();
realm.beginTransaction();
for (int i = 0; guests.size(); i++) {
    guests.get(i).setInvited(true);
}
realm.commitTransaction();

일반적으로 이 간단한 루프를 통해 모든 손님을 초대할 수 있으리라 기대합니다. 하지만 RealmResults가 즉시 업데이트되므로 손님 중 절반만 초대됩니다. 초대된 손님이 컬렉션에서 즉시 제거되고 모든 요소의 위치가 이동하기 때문에 i 매개변수가 증가하면 요소가 누락됩니다.

이를 방지하려면 컬렉션의 데이터를 스냅샷 으로 찍어야 합니다. 스냅샷은 요소가 삭제되거나 수정된 경우에도 요소의 순서를 보장해줍니다.

Realm 컬렉션에서 생성된 Iterator는 자동으로 스냅샷을 사용합니다. RealmResultsRealmList에는 수동으로 스냅샷을 생성할 수 있는 createSnapshot() 메서드가 있습니다.

RealmResults<Person> guests = realm.where(Person.class).equalTo("invited", false).findAll();

// 전체 손님을 초대하기 위해 반복자를 사용합니다
realm.beginTransaction();
for (Person guest : guests) {
    guest.setInvited(true);
}
realm.commitTransaction();

// 모든 손님을 초대하기 위해 스냅샷을 사용합니다
realm.beginTransaction();
OrderedRealmCollectionSnapshot<Person> guestsSnapshot = guests.createSnapshot();
for (int i = 0; guestsSnapshot.size(); i++) {
    guestsSnapshot.get(i).setInvited(true);
}
realm.commitTransaction();

삭제

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

// 질의의 결과를 얻습니다
final RealmResults<Dog> results = realm.where(Dog.class).findAll();

// 데이터에 대한 모든 변경은 트랜잭션에서 이루어져야 합니다
realm.executeTransaction(new Realm.Transaction() {
    @Override
    public void execute(Realm realm) {
        // 맞는 데이터 하나를 삭제합니다
        results.deleteFirstFromRealm();
        results.deleteLastFromRealm();

        // 하나의 객체를 삭제합니다
        Dog dog = results.get(5);
        dog.deleteFromRealm();

        // 전체 맞는 데이터를 삭제합니다
        results.deleteAllFromRealm();
    }
});

비동기 질의

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

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 OrderedRealmCollectionChangeListener<RealmResults<User> callback = new OrderedRealmCollectionChangeListener<>() {
    @Override
    public void onChange(RealmResults<User> results, OrderedCollectionChangeSet changeSet) {
        if (changeSet == null) {
            // 이 비동기의 첫 반환 값은 null 체인지 셋입니다
        } else {
            // 매 갱신마다 호출됩니다
        }
    }
};

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

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

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

질의가 끝났는지 확인하기

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

동기적으로 획득한 RealmResultsisLoaded() 를 호출하면 항상 true를 반환합니다.

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

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

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

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

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

Realms

Realm은 데이터베이스를 의미합니다. 다양한 객체를 가지고 있고 디스크에서 하나의 파일에 대응됩니다. Realm을 사용하기 이전에 먼저 Realm을 초기화하여야 합니다. 다음과 같이 하면 됩니다.

Realm.init(context);

초기화는 한 번만 하면 되며 안드로이드 context를 제공해야 합니다. Realm을 초기화하기 좋은 곳은 애플리케이션 서브클래스의 onCreate()입니다.

public class MyApplication extends Application {
  @Override
  public void onCreate() {
    super.onCreate();
    Realm.init(this);
  }
}

앱의 AndroidManifest.xmlMyApplication를 넣을 필요가 있습니다.

<application
  android:name=".MyApplication"
  ...
/>

Realm 설정하기

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

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

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

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

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

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

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

RealmConfiguration otherConfig = new RealmConfiguration.Builder()
  .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 파일이 위치한다
    Realm.init(this);
    RealmConfiguration config = new RealmConfiguration.Builder().build();
    Realm.setDefaultConfiguration(config);
  }
}

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

인 메모리 (In-memory) Realm

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

RealmConfiguration myConfig = new RealmConfiguration.Builder()
    .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().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()을 호출하는 게 가장 간단합니다.

만약 UI 스레드가 아닌 다른 Looper 스레드를 만든다면 이 패턴을 쓸 수 있습니다.

public class MyThread extends Thread {

    private Realm realm;

    @Override
    public void run() {
        Looper.prepare();
        realm = Realm.getDefaultInstance();
        try {
            //... Realm 인스턴스를 사용하는 핸들러를 설정합니다 ...
            Lopper.loop();
        } finally {
            realm.close();
        }
    }
}

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

protected Void doInBackground(Void... params) {
    Realm realm = Realm.getDefaultInstance();
    try {
        // ... Realm 인스턴스를 사용합니다 ...
    } finally {
        realm.close();
    }

    return null;
}

ThreadRunnable를 짧은 생애 작업에 사용한다면 다음의 패턴을 추천합니다.

// Realm 인스턴스와 비 Looper 스레드를 실행합니다.
Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        Realm realm = Realm.getDefaultInstance();
        try {
            // ... Realm 인스턴스를 사용합니다 ...
        } finally {
            realm.close();
        }
    }
});

thread.start();

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

try (Realm realm = Realm.getDefaultInstance()) {
    // 수동으로 Realm 인스턴스를 닫을 필요가 없습니다
}

자동 새로 고침

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

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

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

Realm 파일 찾기

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

스레드

Realm을 다양한 스레드에서 Realm을 사용하기 위해서 복잡한 설명이 필요하지 않으며, 사용하는 것도 간단합니다. 여기에서 알아야 할 가장 핵심은 Realm은 여러 스레드에서 데이터를 적은 노력으로 다룰 수 있게 되어 있다는 부분입니다. 객체질의가 언제나 자동 업데이트 되기 때문에 불일치나 성능에 대해 걱정할 필요가 없습니다.

다른 스레드에서 라이브 오브젝트를 다룰 수 있고 그들을 읽고 쓸 수 있습니다. 데이터를 바꾸어야 한다면 트랜잭션을 쓸 수 있습니다. 다른 스레드의 다른 객체들은 거의 실시간에 갱신됩니다. (업데이트는 Looper에서 이벤트로 스케쥴 되므로 루퍼 스레드는 이벤트가 처리되는 즉시 업데이트됩니다.)

단 하나의 제약은 스레드 간에 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(RealmResults<Customer> results) {
            // 어떤 스레드에서 Realm 데이터베이스가 변경되면 이 메서드가 실행됩니다.
            // 변경 리스너는 오로지 루퍼 스레드에서만 돈다는 것을 주의하세요.
            // 비 루퍼 스레드에는 대신에 Realm.waitForChange()를 사용해야 합니다.
            listAdapter.notifyDataSetChanged(); // UI를 갱신합니다.
        }
    };
    // Realm이 고객 결과가 변경할 때마다 리스너에게 통보하도록합니다.
    // (항목 추가, 삭제, 갱신, 어떤 종류의 정렬 등)
    customers.addChangeListener(changeListener);
}

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

한번 백그라운드 서비스가 새 고객들을 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()
  .modules(new MyModule())
  .build();

// 여러 개의 모델을 하나의 스키마로 합치는 것이 가능합니다
RealmConfiguration config = new RealmConfiguration.Builder()
  .modules(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()
  .name("library.realm")
  .modules(new MyLibraryModule())
  .build();

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

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

현재는 하나의 파일이 여러 RealmModule을 가지는 게 불가능합니다. 만약 둘 이상의 RealmModule가 있다면 정확히 파일당 하나의 정의로 여러 파일로 분리하세요.

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;
    // 게터와 세터들이 있습니다 ...
}

// 문자열을 삽입합니다
realm.executeTransaction(new Realm.Transaction() {
    @Override
    public void execute(Realm realm) {
        realm.createObjectFromJson(City.class, "{ city: \"Copenhagen\", id: 1 }");
    }
});

// InputStream을 이용하여 여러 아이템을 삽입합니다
realm.executeTransaction(new Realm.Transaction() {
    @Override
    public void execute(Realm realm) {
        try {
            InputStream is = new FileInputStream(new File("path_to_file"));
            realm.createAllFromJson(City.class, is);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
});

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

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

알림

Realm, RealmResuls, RealmList가 갱신될 때마다 addChangeListener 메서드가 호출되도록 리스너를 등록할 수 있습니다.

removeChangeListener()removeAllChangeListeners() 메서드를 호출하여 알림 전달을 종료합니다. 알림 전달은 리스너가 등록된 객체가 GC되거나 연관된 Realm 인스턴스가 닫혀도 종료합니다. 리스너가 등록된 객체가 알림 전달이 필요한 만큼 강 레퍼런스(strong reference)를 유지하세요.

어떻게 알림이 전달되나요

알림은 항상 처음 연결된 스레드로 전달이 됩니다. 그 스레드는 항상 작동 중인 Looper를 가져야 합니다.

관련된 쓰기 트랜잭션이 다른 스레드에서 발생하면 알림 리스너는 트랜잭션이 커밋된 후 비동기적으로 호출이 됩니다.

관련된 쓰기 트랜잭션이 같은 스레드에서 발생하면 알림 리스너는 트랜잭션이 커밋될 때 동기적으로 호출이 됩니다. Realm 인스턴스가 최신 버전으로 바뀌고 관찰하던 Realm 엔트리들이 수정되거나 삭제되어 알림 호출이 필요하다면 리스너는 쓰기 트랜잭션이 시작될 때도 동기적으로 호출될 수 있습니다. 이 경우 리스너가 현재 쓰기 트랜잭션의 문맥에서 수행되기에 알림 핸들러에서 쓰기 트랜잭션을 여는 행위에 Realm이 예외를 발생시킬 수 있습니다. 만약 앱이 이런 방식으로 설계되었다면 이런 시나리오가 일어날 수 있습니다. 이미 트랜잭션의 내부인지 확인하려면 Realm.isInTransaction()를 사용할 수 있습니다.

루퍼 이벤트들을 통해 비동기 알림이 전달되기 때문에 루퍼 큐의 다른 이벤트들이 전달될 때 지연이 발생할 수 있습니다. 알림이 즉시 전달되지 못하면 여러 쓰기 트랜잭션의 변경이 하나의 알림에 합쳐질 수 있습니다.

Realm 알림

Realm이 변경되면 UI나 다른 스레드는 Realm에 추가된 리스너에 의해 변경점을 알림 받습니다.

public class MyActivity extends Activity {
    private Realm realm;
    private RealmChangeListener realmListener;

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

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

컬렉션 알림

컬렉션 알림은 Realm 알림과 조금 다릅니다. 마지막 알림 이후로 삽입, 삭제, 수정된 객체의 인덱스를 포함해서 상세한 수준에서 어떤 변화가 생겼는지 묘사하는 정보를 포함하고 있습니다.

컬렉션 알림은 비동기적으로 전달됩니다. 처음에는 최초 결과를 전달하고, 컬렉션 내의 객체 변화를 일으키는 각 쓰기 트랜잭션 이후에 전달됩니다.

이런 변화는 변경 리스너로부터 전달된 OrderedCollectionChangeSet 매개변수를 통해 접근할 수 있습니다. 이 객체는 삭제, 삽입, 변경 등에 영향을 받는 인덱스에 대한 정보를 가지고 있습니다.

전자의 둘, 삭제삽입 은 객체가 컬렉션의 한 부분이 될 때와 더 이상 아니게 될 때 색인을 기록합니다. Realm에 객체를 추가하거나 삭제하는 경우가 해당됩니다. RealmResults의 경우 특정 값을 필터링하고 객체가 변경되어 쿼리와 일치하게 되거나 더 이상 일치하지 않을 때적용됩니다.

이전부터 컬렉션의 일부인 객체의 필드가 변경될 때마다 변경 사항 에 대한 알림을 받습니다. 일 대 일이나 일 대 다 관계가 변경될 때도 마찬가지입니다.

public class Dog extends RealmObject {
  public String name;
  public int age;
}

public class Person exteds RealmObject {
  public String name;
  public RealmList<Dog> dogs;
}

위 모델 코드에서처럼 개의 주인 목록을 관찰한다고 가정해 보겠습니다. 다음 경우 일치하는 Person 객체의 수정 사항에 대한 알림이 표시됩니다.

  • Person`의 이름을 수정한 경우
  • Person이 소유한 개 목록에 Dog를 추가하거나 삭제한 경우
  • Person이 소유한 Dog의 나이 속성을 변경한 경우

이 경우 알림이 발생할 때마다 모든 내용을 다시 불러오는 대신 UI 내부 콘텐츠에 대한 애니메이션이나 시각적인 업데이트를 개별적으로 제어할 수 있습니다.

private final OrderedRealmCollectionChangeListener<RealmResults<Person>> changeListener = new OrderedRealmCollectionChangeListener<>() {
    @Override
    public void onChange(RealmResults<Person> collection, OrderedCollectionChangeSet changeSet) {
        // 처음에 `null`을 반환한 것은 비동기 질의를 의미합니다.
        if (changeSet == null) {
            notifyDataSetChanged();
            return;
        }
        // 삭제를 위해 어뎁터는 역순으로 어뎁터에 통보합니다.
        OrderedCollectionChangeSet.Range[] deletions = changeSet.getDeletionRanges();
        for (int i = deletions.length - 1; i >= 0; i--) {
            OrderedCollectionChangeSet.Range range = deletions[i];
            notifyItemRangeRemoved(range.startIndex, range.length);
        }

        OrderedCollectionChangeSet.Range[] insertions = changeSet.getInsertionRanges();
        for (OrderedCollectionChangeSet.Range range : insertions) {
            notifyItemRangeInserted(range.startIndex, range.length);
        }

        OrderedCollectionChangeSet.Range[] modifications = changeSet.getChangeRanges();
        for (OrderedCollectionChangeSet.Range range : modifications) {
            notifyItemRangeChanged(range.startIndex, range.length);
        }
    }
};

RealmRecyclerViewAdapter로 즉시 사용해보세요.

객체 알림

Realm은 객체 수준의 알림을 지원합니다. 객체가 삭제되거나 객체의 값을 가지고 있는 관리되는 (managed) 필드가 변경될 때마다 알림을 받기 위해 특정 RealmObject에 알림을 등록할 수 있습니다.

관리(managed) RealmObject에만 리스너를 등록할 수 있습니다.

변경 리스너에 전달되는 ObjectChangeSet 인자를 통해 이런 변경 사항에 접근할 수 있습니다. ObjectChangeSet은 어떤 필드가 변경되었는지 혹은 RealmObject가 변경되었는지 정보를 담습니다.

객체가 삭제되었다면 ObjectChangeSet.isDeleted()true를 반환합니다. 그 이후에는 리스너가 호출되지 않습니다.

객체에서 관리되는 필드가 변경된다면 ObjectChangeSet.getChangedFields()는 변경된 필드들의 이름들을 반환합니다. 주어진 필드가 바뀌었는지 확인하기 위해 ObjectChangeSet.isFieldChanged()를 사용할 수 있습니다.

private final RealmObjectChangeListener<Dog> listener = new RealmObjectChangeListener<Dog>() {
    @Override
    public void onChange(Dog dog, ObjectChangeSet changeSet) {
        if (changeSet.isDeleted()) {
            Log.i(TAG, "개가 삭제되었습니다.");
            return;
        }

        for (String fieldName : changeSet.getChangedFields()) {
            Log.i(TAG, "필드 " + fieldName + "가 변경되었습니다.");
        }
    }
};

마이그레이션

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

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

RealmConfiguration config = new RealmConfiguration.Builder()
    .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()
    .deleteRealmIfMigrationNeeded()
    .build()

암호화

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

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

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

Realm realm = Realm.getInstance(config);

이런 방법을 통해 디스크에 저장되는 데이터가 표준 AES-256 암호화를 통해 투명한 방법으로 암호화, 복호화 할 수 있습니다. 파일을 생성할 때 같은 암호화 키를 매번 Realm 인스턴스에 제공해야 합니다.

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

어댑터

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

  • RealmBaseAdapterListView와 함께 사용하세요. 예제는 여기에서 보세요.
  • RealmRecyclerViewAdapterRecyclerView와 함께 사용하세요. 예제는 여기에서 보세요.

RealmBaseAdapter을 사용하기 위해 애플리케이션 수준 build.gradle에 추가적인 의존성을 넣어야 합니다.

dependencies {
	compile 'io.realm:android-adapters:2.0.0'
}

인텐트

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();
// 사람에 대해 어떤 작업을 합니다 ...
try {
    Person person = realm.where(Person.class).equalTo("id", personId).findFirst();
    // do something with the person ...
} finally {
    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();
        try {
            // Realm을 사용합니다.
            realm.createAllFromJson(Order.class, api.getNewOrders());
            Order firstOrder = realm.where(Order.class).findFirst();
            long orderId = firstOrder.getId(); // Id of order
            return orderId;
        } finally {
            realm.close();
        }
    }

    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();
        try {
            // Work with Realm
            realm.createAllFromJson(Order.class, api.getNewOrders());
            Order firstOrder = realm.where(Order.class).findFirst();
            long orderId = firstOrder.getId(); // Id of order
        } finally {
            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().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 직렬화가 작동합니다.

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

기본형 리스트

어떤 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 프로젝트의 이슈에 보고되어 있습니다.

코틀린

Realm은 코틀린 프로그래밍 언어를 완전히 지원합니다. 하지만 몇 가지 알아야 할 부분이 있습니다.

Realm과 코틀린으로 동작하는 앱은 여기 예제를 보세요.

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를 쉽게 사용할 수 있는 도구입니다.

Realm은 Retrofit 1.이나 2.과 특별히 잘 동작합니다. 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.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/RxObservableFactory.html)를 사용합니다. Realm이 제공하는 RxJava <= 1.1.* 버전을 지원하는 클래스입니다.

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

## 테스팅과 디버깅 {#testing-and-debugging}

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

### 안드로이드 스튜디오 디버깅 {#android-studio-debugging}

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

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

<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 디버깅 {#ndk-debugging}

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은 모든 제약사항을 없애기 위해 노력하고 있습니다. 현재 커뮤니티에 기반해서 새로운 기능을 지속적으로 추가하고 있습니다. 하지만 Realm은 여전히 몇 가지 제약 사항이 있고 지금까지의 가장 일반적인 제한 사항을 정리해놨습니다.

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

일반적인 제한

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

  1. 클래스 이름은 57자로 제한됩니다. Android 버전 Realm은 class_ 접두어를 모든 클래스 이름에 추가하고 브라우저에서 일부 확인할 수 있습니다.
  2. 필드 이름의 길이는 63자로 제한됩니다.
  3. 다른 패키지에 같은 이름을 가지는 두 모델 클래스를 가질 수 없습니다.
  4. 중첩된 트랜잭션은 지원하지 않고 발견 시 예외 처리됩니다.
  5. String들과 byte array(byte[])는 16MB를 넘을 수 없습니다.

문자열에 대한 정렬과 질의

정렬과 대소문자를 가리지 않는 문자열 일치는 ‘Latin Basic’, ‘Latin Supplement’, ‘Latin Extended A’, ‘Latin Extended B’ (UTF-8 range 0-591)에 대해서만 지원하고 있습니다. 추가로 equalTo(), notEqualTo(), contains(), endsWith(), beginsWith(), like()를 사용하는 질의에서 대소문자 무관한 설정을 하려면 영어 로케일을 써야만 합니다. 이 제한에 대한 자세한 내용은 여기를 보세요.

스레드

동시에 다수의 스레드에서 Realm 파일에 접근할 수 있지만 스레드 간에 Realm, RealmObject, query, results를 전달 할 수 없습니다. 다중 스레드 환경에서 Realm 사용법은 스레드 예제에서 볼 수 있습니다.

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

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

RealmObject의 hashCode

RealmObject는 라이브 객체이며 다른 스레드에서 변경이 일어나면 갱신됩니다. RealmObject.equals()를 썼을 때 true를 리턴하는 두 개의 Realm 객체는 항상 같은 RealmObject.hashCode() 결과를 반환해야 하지만, 그 값은 안정적이지 않으며 HashMapHashSet`에서 키로 쓰면 안됩니다.

모범 사용 예

안드로이드와 함께

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

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

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

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

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

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

// 애플리케이션에서 Realm 설정하기
public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        Realm.init(this)
        RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().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(Realm realm) {
            // 그냥 다시 뷰를 그립니다. `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(); // 데이터에 따라 뷰를 다시 그리기
    }

    // ...
}

자동 증가 ID

자동 증가 ID는 Realm의 설계상 제공되지 않습니다. 가장 큰 이유는 분산 환경에서 이런 키를 생성하는 것이 불가능하며 로컬 Realm과 동기화된 Realm에 저장된 데이터 사이의 호환이 가장 우선이기 때문입니다. Realm은 관계를 만들기 위해 기본키가 _필요하지 않다_는 점을 유의하세요.

자동 증가 ID를 제공하는 용례를 다룰 때 효율을 위해 기본 키를 생성하는 것은 여전히 가능합니다. 하지만 _무엇_에 자동 증가 ID를 써서 식별하는지가 중요합니다.

1) 객체를 식별하기 위해 고유 식별자를 제공합니다. 이는 모든 실용적인 용도에서 고유함을 보장하고 오프라인에서도 장비에서 생성할 수 있는 GUID로 사용할 수 있습니다.

public class Person extends RealmObject {
    @PrimaryKey
    private String id = UUID.randomUUID().toString();
    private String name;
}

2) 느슨한 삽입 순서를 제공합니다. 예제는 정렬된 트윗입니다. 기본 키가 없다면 createdAt 필드로 대체 가능합니다.

public class Person extends RealmObject {
    @PrimaryKey
    private String id = UUID.randomUUID().toString();
    private Date createdAt = new Date();
    private String name;
}

3) 엄격한 삽입 순서를 제공합니다. 예제는 업무 목록입니다. 장비가 오프라인인 경우에도 삽입 순서를 보장하기 위해 RealmList를 사용하도록 모델을 만들 수 있습니다.

public class SortedPeople extends RealmObject {
    @PrimaryKey
    private int id = 0
    private RealmList<Person> persons;
}

public class Person extends RealmObject {
    private String name;
}

// 객체 생성할 때 래퍼 객체를 생성합니다.
RealmConfiguration config = new RealmConfiguration.Builder()
  .initialData(new Realm.Transaction() {
    @Override
    public void execute(Realm realm) {
        realm.insert(new SortedPeople());
    }
  });

// 래퍼를 통해 객체를 추가합니다
realm.executeTransaction(new Realm.Transaction() {
    @Override
    public void execute(Realm realm) {
        SortedPeople sortedPeople = realm.where(SortedPeople.class).findFirst();
        sortedPeople.getPersons().add(new Person());
    }
});

여전히 이 경우에는 자동 증가 ID가 더 맞다고 생각한다면 이런 도움 클래스를 사용할 수 있습니다. 하지만 이런 클래스를 사용해 생성된 키는 아래의 경우에는 유용하지 않습니다.

1) 여러 프로세스에서 Realm 객체를 생성합니다. 2) 향후 어떤 시점에 여러 단말 사이에 Realm을 공유합니다.

여러 프로세스에서 안전하게 자동 증가 ID를 생성하려면 매 트랜잭션마다 최대 값을 질의해야 합니다.

realm.beginTransaction();
Number maxValue = realm.where(MyObject.class).max("primaryKeyField");
long pk = (maxValue != null) ? maxValue + 1 : 0;
realm.createObject(MyObject.class, pk++);
realm.createObject(MyObject.class, pk++);
realm.commitTransaction();

Realm 레시피

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

동기화

Realm 모바일 플랫폼 (RMP)은 Realm 모바일 데이터 베이스를 네트워크로 확장하여 자동으로 여러 장비 간의 데이터를 동기화합니다. 동기화된 Realm 지원을 위해 새로운 타입과 클래스들이 제공됩니다. 새롭게 추가된 객체들은 기존 Realm 모바일 데이터베이스에 추가되었고 여기에 다룹니다.

Realm 모바일 플랫폼 활성화

Realm 모바일 플랫폼을 활성화하기 위해 앱의 build.gradle에 다음 내용을 추가해야 합니다.

realm {
  syncEnabled = true;
}

매우 간단합니다!

사용자

Realm 오브젝트 서버의 중심적인 객체는 Realm 사용자 컨셉입니다. User는 동기화된 Realm과 관련됩니다. User는 사용자 이름 / 암호 스키마나 여러 제3자 인증 방법에 의해 인증됩니다.

사용자를 만들고 로그인하기 위해 두 가지 객체가 필요합니다.

  • Realm 인증 서버가 접속할 (문자열로 된) URL
  • 사용자를 인증 메카니즘에 적합한 Credential (예: 사용자 / 암호, 액세스 키, 기타)

인증

인증은 사용자를 식별하고 로그인한다. Realm 모바일 플랫폼이 지원하는 인증 제공자의 목록은 인증 문서에서 참고하세요.

사용자에게 주어진 증명(credential) 여러 방법으로 만들어집니다.

  • 유효한 사용자 이름 / 암호 조합을 제공합니다
  • 지원되는 제3자 인증 서비스에서 얻은 토큰을 제공합니다
  • 사용자 인증 제공자와 토큰을 제공합니다 (사용자 인증을 보세요)

사용자 이름과 암호 인증은 전반적으로 Realm 오브젝트 서버에 의해 관리됩니다. 애플리케이션의 사용자 관리에 대한 전권을 제공합니다. 다른 인증 방법은 애플리케이션이 외부 서비스에 로그인하고 인증 토큰을 얻을 책임이 있습니다.

여기에 다양한 제공자에 맞게 증명을 설정하는 예가 있습니다.

사용자 / 암호
SyncCredentials myCredentials = SyncCredentials.usernamePassword(username, password, true);

usernamePassword()의 세 번째 인자는 사용자를 생성해야 한다는 의미로 항상 처음에는 true로 설정해야 합니다. 한번 사용자가 생성이 되면 인자는 false여야 합니다.

구글
String token = "..."; // 구글 로그인 API로 부터 얻은 토큰을 문자열에 사용
SyncCredentials myCredentials = SyncCredentials.google(token);
페이스북
String token = "..."; // 페이스북 로그인 API로 부터 얻은 토큰을 문자열에 사용
SyncCredentials myCredentials = SyncCredentials.facebook(token);

커스텀 인증

String token = "..."; // a string representation of a token obtained from your authentication server
Map<String, Object> customData = new HashMap<>();
SyncCredentials myCredentials = SyncCredentials.custom(
  token,
  'myauth',
  customData,
);

주의: 생성자의 세 번째 패러미터로 추가적인 로그인 정보를 전달하는 것이 가능합니다. API 참조 문서에 상세한 내용이 있습니다.

사용자 로그인

사용자에 필요한 모든 인자가 준비됐기 때문에 Realm 오브젝트 서버에 로그인할 수 있습니다.

String authURL = "http://my.realm-auth-server.com:9080/auth";
SyncUser user = SyncUser.login(myCredentials, authURL);

사용자 작업하기

독립적으로 작동하는 Realm에서는 Realm의 부가사항을 설정하기 위해 RealmConfiguration를 사용합니다. 안드로이드용 Realm 모바일 플랫폼은 확장된 설정 클래스 SyncConfiguration를 사용합니다. 설정은 인증된 사용자와 싱크 서버 URL에 묶여 있습니다. 싱크 서버 URL은 틸드 문자(“~”)를 포함할 수 있습니다. 이는 사용자의 유일한 식별자를 투명하게 확장하는데 사용될 수 있습니다. 스키마는 손쉽게 개별 사용자를 앱에서 대응할 수 있게 합니다. 디스크에서 저장될 공유 Realm의 위치는 프레임워크에 의해 관리되지만 필요에 의해 변경될 수 있습니다.

SyncUser user = getUserFromLogin();
String serverURL = "realm://my.realm-server.com:9080/~/default";
SyncConfiguration configuration = new SyncConfiguration.Builder(user, serverURL).build();

현재 사용자를 가져오는 것이 가능합니다. 현재 사용자는 마지막에 로그인하였고 인증정보가 아직 만기 되지 않은 사용자입니다.

SyncUser user = SyncUser.currentUser();

사용자와 그 인증정보를 표현한 JSON을 받을 수 있습니다.

String userJson = user.toJson();

사용자 표현을 저장하면 사용자는 더 이상 제3자 로그인 제공자에게 다시 로그인할 필요가 없습니다. 대신에 사용자 객체를 다시 생성하고 Realm 오브젝트 서버에 다음과 같이 액세스 토큰을 갱신하면 됩니다.

SyncUser user = SyncUser.fromJson(userJson);

Realm은 현재 사용자를 UserStore에 저장합니다. 기본 UserStore는 전용의 쉐어드 프리퍼런스 (Shared Preference) 파일에 저장이 되는데 이 행동은 SyncManager.setUserStore(userStore)를 사용하여 바꿀 수 있습니다.

사용자와 그들의 인증 정보를 민감한 데이터로 취급해야 한다는 것을 기억하는 것이 중요합니다.

로그아웃

다음처럼 간단하게 동기화된 Realm에서 로그아웃할 수 있습니다.

user.logout();

사용자가 로그아웃을 하면 동기화는 멈춥니다. 로그 아웃 한번만으로 사용자와 관련된 모든 Realm이 닫힙니다. 로그아웃한 사용자는 더 이상 SyncConfiguration를 통해 Realm을 열 수 없습니다.

동기화된 Realm 열기

SyncConfiguration가 생성되면 일반적인 Realm을 생성하는 것처럼 동기화된 Realm 인스턴스가 만들어집니다.

Realm realm = Realm.getInstance(syncConfiguration);

접근 제어

Realm 모바일 플랫폼은 유연한 접근 제어 방식을 제공해서 어떤 사용자가 어떤 Realm 파일과 동기화할 수 있는지에 대한 권한을 허용할 수 있게 합니다. 예를 들어 여러 사용자가 동일한 Realm에 쓸 수 있는 협업 앱을 만들 수 있습니다. 또한 한 사용자가 만든 데이터를 읽기 퍼미션을 가진 여러 사용자와 공유하는 게시자/구독자 시나리오에도 활용할 수 있습니다.

Realm의 접근 수준, 즉 권한 제어는 세 가지 축으로 분류됩니다.

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

권한을 명시적으로 바꾸지 않으면 Realm의 소유자만 접근할 수 있습니다. 예외는 관리자들입니다. 그들은 언제나 서버의 모든 Realm의 모든 권한을 가집니다.

접근 제어에 대한 자세한 내용은 Realm 오브젝트 서버 문서의 접근 제어 섹션을 참조하세요.

관리 Realm

모든 접근 레벨 관리 작업은 관리 Realm에 기록하는 것으로 수행할 수 있습니다. 관리 Realm은 일반적인 동기 Realm과 동일하지만, Realm 오브젝트 서버는 이 관리 Realm의 변경 사항에 기본적으로 대응하도록 특별히 설계됐습니다. 권한 변경된 객체를 관리 Realm에 추가해서 Realm 파일의 접근 제어 설정을 수정할 수 있습니다.

SyncUser.getManagementRealm()메서드를 호출해서 특정 사용자에 대한 관리 Realm을 얻을 수 있습니다. 이 Realm은 다른 일반적은 Realm의 규칙을 따르며 더 이상 사용되지 않으면 닫아야 합니다.

권한 수정

Realm 파일에 대한 접근 제어 설정을 수정하려면 변경 객체_나 _제공/응답 객체 둘 중 하나를 사용합니다.

PermissionChange

PermissionChange 객체는 직접적으로 Realm의 접근 설정을 변경할 수 있게 합니다. 관리 Realm에 변경 객체를 작성합니다.

SyncUser user = SyncUser.currentUser();
String realmURL = "realm://ros.example.com/~/default"; // 변경이 적용될 원격 Realm URL
String anotherUserID = "other-user-id"; // 이 변경들이 적용될 사용자 ID

Realm realm = user.getManagementRealm();
realm.executeTransaction(new Realm.Transaction() {
    @Override
    public void execute(Realm realm) {
        Boolean mayRead = true; // 읽기 권한 부여
        Boolean mayWrite = null; // 현재 권한 유지
        boolean mayManage = false; // 관리 권한 제거
        PermissionChange change = new PermissionChange(realmUrl,
                                                       anotherUserID,
                                                       mayRead,
                                                       mayWrite,
                                                       mayManage);
        realm.insert(change);
    }
});

동기화 사용자가 관리하는 모든 Realm에 대한 권한 변경 사항을 적용하려면, realmURL 값을 *로 지정하세요. 오브젝트 서버에 허가된 모든 동기화 사용자에 대한 사용 권한 변경 사항을 적용하려면 userID 값을 *로 지정하세요.

오브젝트 서버는 PermissionChange 객체에 인코딩 된 작업을 처리한 다음, 객체의 statusstatusMessage 속성을 설정합니다.

권한 변경 작업의 결과를 파악하려면 다른 Realm 객체를 KVO나 컬렉션 알림으로 구독하는 방법과 마찬가지로 권한 변경 객체를 구독하세요. 자세한 사항은 알림에서 볼 수 있습니다.

다음 예제는 애플리케이션에서 권한 변경 작업의 상태를 파악하는 방법을 보여줍니다.

SyncUser user = SyncUser.currentUser();
Realm realm = user.getManagementRealm();
final String permissionId = "permission-id";
PermissionChange change = realm.where(PermissionChange.class).equalTo("id", permissionId).findFirst();
change.addChangeListener(new RealmChangeListener<PermissionChange>() {
    @Override
    public void onChange(PermissionChange change) {
        if (change.getId().equals(permissionId)) {
            Integer status = change.getStatusCode();
            if (status == 0) {
                // 성공했을 때를 다룹니다
                realm.close();
            } else if (status > 0) {
                // 실패했을 때를 다룹니다
            }
        }
    }
});
PermissionOffer/Response

PermissionOffer(권한 제안)와 PermissionOfferResponse(권한 제안 응답) 클래스는 사용자 사이에 Realm을 공유할 수 있게 하며, 클라이언트 API에서 모든 것을 할 수 있습니다. 서버 코드가 필요하지 않습니다. Realm을 공유하기 위해 아래의 절차대로 합니다.

  1. 사용자 관리 Realm을 공유하기 위해 PermissionOffer을 만듭니다.
  2. 제안이 동기화되고 Realm 오브젝트 서버에 처리되길 기다립니다. 서버는 제안 객체의 token 속성을 만듭니다.
  3. 어떤 메서드를 통해 다른 사용자에게 토큰을 보냅니다.
  4. 리시버는 PermissionOfferResponse(권한 제안 응답)을 관리 Realm에 생성합니다.
  5. 제안 응답이 동기화되고 서버에 처리되길 기다립니다. 서버는 응답 객체의 realmUrl 속성을 만듭니다.
  6. 이제 권한을 받은 사용자는 해당 URL으로 공유 Realm 접근이 가능합니다.
SyncUser user = getUser("user");
Realm managementRealm = user.getManagementRealm();
String sharedRealmUrl = "realm://my.server/~/my-realm";
boolean mayRead = true;
boolean mayWrite = true;
boolean mayManage = true;
Date expiresAt = null; // 이 제안은 만기가 없습니다
final PermissionOffer offer = new PermissionOffer(sharedRealmUrl, mayRead, mayWrite, mayManage, expiresAt);
String offerId = offer.getId();
managementRealm.executeTransaction(new Realm.Transaction() {
    @Override
    public void execute(Realm realm) {
        realm.insert(offer);
    }
});

// 서버로 부터 제안이 다루어지길 기다립니다
RealmResults<PermissionOffer> offers = managementRealm.where(PermissionOffer.class)
        .equalTo("id", offerId)
        .equalTo("statusCode", 0)
        .findAll();
offers.addChangeListener(new RealmChangeListener<RealmResults<PermissionOffer>>() {
    @Override
    public void onChange(RealmResults<PermissionOffer> offers) {
        PermissionOffer offer = offers.first();
        String token = offer.getToken();
        // 제안이 준비되면 다른 사용자에게 토큰을 보냅니다
        sendTokenToOtherUser(token);
    }
});

PermissionChange와 비슷하게 인자로 주어진 realmURL에 해당하는 Realm의 read, write, manage 권한을 제어합니다. expiresAt 인자는 토큰의 수명을 제어할 수 있습니다. expiresAt에 값을 전달하지 않거나 nil을 전달하면 제안은 만기가 없습니다. 제안 토큰이 처리된 사용자는 만기 후에도 접근을 잃지 않습니다.

한번 토큰을 받은 다른 사용자는 그것을 소비해 PermissionOfferResponse(권한 제안 응답) 객체를 만들 수 있습니다.

final SyncUser user2 = getUser("user2");
Realm managementRealm = user.getManagementRealm();

// 받은 토큰으로 응답을 만듭니다
String offerToken = getToken();
final PermissionOfferResponse offerResponse = new PermissionOfferResponse(offerToken);

// ROS에 동기화하기 위해 관리 Realm에 추가합니다
managementRealm.executeTransaction(new Realm.Transaction() {
    @Override
    public void execute(Realm realm) {
        realm.insert(offerResponse);
    }
});

// 처리 후, offerResponse의 realmUrl 속성은 공유 Realm의 URL을 가진다

SyncPermissionOffer에서 주어진 권한은 부가적입니다. 사용자가 이미 write 접근이 있고 제안에 주어진 read 권한이 수용하면 쓰기 권한은 여전히 남아있습니다.

권한 제안은 관리 Realm에서 PermissionOffer을 삭제하거나 expiresAt 속성의 값을 과거로 바꾸어 제거할 수 있습니다. 이는 새로운 유저가 제안을 받는 것을 막을 수 있지만 기존에 처리한 사용자의 권한은 제거되지 않습니다.

로그 남기기

동기화된 Realm을 디버깅하는 것은 다루기 힘들기 때문에 로그를 남기는 것은 문제에 대해 이해를 높이기 위해 중요합니다. 세밀한 로그를 활성화하면 안드로이드 로그캣을 통해 무엇이 일어나는지 더 자세히 볼 수 있습니다.

RealmLog.add(new AndroidLogger(Log.VERBOSE));

에러 보고

에러 헨들러를 등록하여 에러를 취급하는 것은 중요합니다.

SyncConfiguration configuration = new SyncConfigurtion.Builder(user, serverURL)
  .errorHandler(new Session.Errorhandler() {
    void onError(Session session, ObjectServerError error) {
        // do some error handling
    }
  })
  .build();

모든 SyncConfigurations에 적용할 수 있게 기본 글로벌 에러 헨들러를 등록하는 것도 가능합니다.

SyncManager.setDefaultSessionErrorHandler(myErrorHandler);

마이그레이션

동기화된 Realm을 위해 자동 마이그레이션이 제공됩니다. 현재 기존 클래스에 필드를 추가하거나 클래스를 추가하는 추가적인 변경만 지원됩니다. 어떤 비추가적인 변경도 예외 발생으로 이어집니다.

마이그레이션은 필요에 따라 자동으로 발생합니다. 커스텀 마이그레이션 블록이나 스키마 버전 숫자는 필요하지 않습니다.

충돌 해결

충돌 해결은 Realm 오브젝트 서버 문서에 기술되어 있습니다.

제약사항

현재 final, transient, volatile 필드를 지원하지 못합니다. 이것은 Realm에 의해 관리되는 객체와 비관리 객체 간의 불일치를 막기 위해서입니다.

Realm 모델 클래스는 RealmObject 외의 다른 객체의 상속을 허용하지 않습니다. 한번 정의되면 기본 생성자(패러미터가 없는 생성자)는 비어있어야 합니다. 이유는 기본 생성자는 Realm 인스턴스가 주어져 있다고 가정하는 메서드를 호출 하기 때문입니다. 하지만 인스턴스는 생성자가 반환하기 전까지 만들어지지 않습니다. 편의를 위해서라면 다른 생성자를 추가하세요.

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를 분리할 수 있습니다.

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

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

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

만약 여러 개의 APK를 원하지 않는다면 단일 APK에서 지원할 아키텍처를 제한할 수 있습니다. 이는 abiFiltersbuild.gradle에 추가해서 할 수 있습니다.

android {
    defaultConfig {
        ndk {
            abiFilters 'armeabi-v7a', 'arm64-v8a', 'mips', 'x86', 'x86_64'
        }
    }
}

Realm은 오픈소스입니까?

Realm 내부의 C++ 스토리지 엔진과 그 위의 언어의 SDK들은 전부 오픈소스이고 아파치 2.0 라이선스입니다. Realm은 Realm 플랫폼 확장 컴포넌트를 부가적으로 가지고 있습니다만 Realm을 임베디드 데이터베이스로 사용하는데 필수는 아닙니다.

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

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

Realm 객체들의 인스턴스들은 관리(Managed)되거나 비관리(Unmanaged)입니다.

  • 관리(Managed) 객체는 Realm에 영속적이고 항상 최신이며 스레드에 연결되어 있습니다. 일반적으로는 자비 힙에서 더 적은 용량을 쓰기 때문에 비관리 버전보다 경량화되어 있습니다.

  • 비관리(Unmanaged) 객체는 일반적인 자바 객체와 비슷합니다. 영속적이지 않으며 자동으로 갱신되지 않습니다. 이들은 스레드 사이에 자유롭게 이동할 수 있습니다.

두 상태는 Realm.copyToRealm()Realm.copyFromRealm()로 전환할 수 있습니다.

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

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

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

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

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

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

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

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

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

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

커다란 Realm 파일 사이즈

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

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

앱을 실행하면 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 이상의 버전을 쓰면 문제를 해결할 수 있습니다.

여러 제3자 라이브러리, 프레임워크, 관리 앱에서 아직 64비트 지원을 하지 못합니다.

어떻게 Realm을 백업하고 복원합니까?

Realm은 파일 시스템의 파일로 저장합니다. getPath()를 호출하여 Realm 파일의 전체 경로를 알 수 있습니다. 이 방법으로 Realm 파일을 백업하거나 복원하려면 모든 Realm 인스턴스들이 닫혀있어야 합니다.

열려있는 Realm 파일을 realm.writeCopyTo(File)을 이용하여 백업하는 것도 가능합니다.

파일을 구글 드라이브와 같은 외부 장소로 백업하길 원한다면 이 튜토리얼을 참고할 수 있습니다. 파트 1, 파트 2, 파트 3.

블랙베리 장비

몇몇 블랙베리 장비들은 안드로이드 앱을 실행할 수 있습니다. 불행하게도 블랙베리와 제공되는 실시간 환경은 완전하지 못하기에 호환성이 보장되지 않습니다. 알려진 에러 메시지에는 다음과 같은 것이 있습니다.

io.realm.exceptions.RealmFileException: Function not implemented in io_realm_internal_SharedRealm.cpp line 81 Kind: ACCESS_ERROR.

블랙베리 이슈에 대해 관심이 있다면 Realm 코어와 Realm 자바가 모두 오픈소스이기 때문에 개선에 참여해 주세요.

Realm의 암호화된 키 저장하고 읽기

Realm의 암호화 키를 저장하기 위한 가장 안전한 방법은 안드로이드 키 스토어를 사용하는 것입니다.

다음과 같은 사용법을 추천합니다.

  1. 안드로이드의 키 스토어를 사용하여 비대칭 RSA 키를 만들고 이를 안드로이드에 안전하게 저장합니다. M 버전 이상에서는 키 스토어를 열 때 시스템이 사용자 PIN (혹은 지문)을 요구합니다. 심지어 루팅 된 단말기에서도 보안을 위한 추가적인 계층을 가집니다.

  2. 비대칭 키(AES)를 만들고 Realm의 암호화에 이용합니다.

  3. 비대칭 AES 키를 여러분의 비공개 RSA 키로 암호화합니다.

  4. 이제 암호화된 AES 키를 (예를 들면 SharedPreferences와 같은) 파일 시스템에 안전 하게 저장할 수 있습니다.

  5. 암호화된 Realm을 사용해야 할 때 암호화된 AES 키를 가져오고 공개 RSA 키를 사용하여 복호화하여 암호화된 Realm을 열 RealmConfiguration에 사용합니다.

우리는 양단의 예제를 유사한 절차로 이 라이브러리에 구현하였습니다. https://github.com/realm/realm-android-user-store

상세한 API를 사용할 수 있도록 여기에 예제가 있습니다.