Realm Blog

Realm Java 3.0을 소개합니다: 컬렉션 알림과 스냅샷 및 관계간 정렬이 가능합니다!

2017년 2월 28일 Realm Java 3.0이 발표됐습니다. 이번 릴리즈부터 관계를 기준으로 정렬할 수 있게 됐으며, Realm의 실시간 컬렉션인 RealmResultsRealmList에 정밀한 컬렉션 알림 기능을 추가해서 앱의 원소가 추가, 삭제, 변경되는 것을 알 수 있습니다.

정밀한 컬렉션 알림

지금까지는 Realm에서 제공하는 단일 RealmChangeListener 인터페이스를 사용해서 Realm, RealmObject, RealmResults에 클래스에 등록했습니다. 하지만 어떤 것이 변경됐는지에 대한 정보를 줄 수 없고 단지 변화가 있었던 것만을 알릴 수 있다는 인터페이스상의 한계가 있었습니다. 많은 경우 이것만으로도 충분하지만, 더 많은 정보가 필요한 경우도 있습니다. 일반적으로 RecyclerView의 변화를 애니메이션으로 표시하려는 경우입니다. Realm의 객체는 실시간으로 업데이트되므로 여태까지는 주로 realm.copyFromRealm()과 Android Support Library의 DiffUtil 클래스를 조합해서 이를 해결할 수 있지만 이상적인 방법은 아니었습니다.

이번 릴리즈에서 저희는 정밀한 컬렉션 알림을 추가했습니다. 이는 RealmResults에 등록할 수 있는 새로운 인터페이스로 정확히 어떤 객체가 추가, 삭제, 변경됐는지에 대한 정보를 줍니다. 따라서 실제로 변경된 원소만을 새로 고쳐서 보다 빠르고 반응이 빠른 UI를 구성할 수 있습니다.

또한 Android Adapters 라이브러리도 업데이트해서 RealmRecyclerViewAdapter가 매끄러운 애니메이션을 위해 자동으로 정밀한 알림을 사용하도록 했습니다. 컬렉션 알림 없이 RecyclerView를 사용하는 것과 비교해서 실제 상황을 영상으로 보여드리겠습니다.

private final OrderedRealmCollectionChangeListener<RealmResults<Person>> changeListener = new OrderedRealmCollectionChangeListener() {
    @Override
    public void onChange(RealmResults<Person> collection, OrderedCollectionChangeSet changeSet) {
        // `null`  means the async query returns the first time.
        if (changeSet == null) {
            notifyDataSetChanged();
            return;
        }
        // For deletions, the adapter has to be notified in reverse order.
        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);
        }
    }
};

정밀한 컬렉션 알림은 다른 Realm 리스너와 동일하게 작동하므로, findAllAsync()를 사용하는 것처럼 쿼리가 비동기적으로 실행되면 정밀한 체인지셋이 백그라운드 스레드에서 계산돼서 결과를 UI 스레드로 보내며, 이 때 DiffUtil을 사용해서 변경사항을 계산하는 메모리 오버헤드가 발생하지 않습니다.

컬렉션 스냅샷

Realm의 핵심 설계 원칙은 “라이브” 객체와 컬렉션입니다. 즉, 쿼리에서 RealmResults를 생성하면 기본 데이터가 변경될 때마다 최신 상태로 유지됩니다. 단지 RealmChangeListener`를 등록해서 쿼리의 결과가 업데이트될 때 알림을 받을 수 있으므로 매우 효과적인 반응형 구조를 만들 수 있습니다.

하지만 어떤 경우 이 “라이브” 특성으로 라이브 컬렉션을 순회할 때 예기치 않은 문제가 발생할 수도 있습니다. 예제와 함께 설명하겠습니다.

RealmResults<Guest> uninvitedGuests = realm.where(Guests.class)
  .equalTo("inviteSent", false)
  .findAll();

for (i = 0; i < uninvitedGuests.size(); i++) {
  realm.beginTransaction()
  uninvitedGuests.get(i).sendInvite();
  realm.commitTransaction();
}

여기서 초대받지 못한 모든 손님에 대한 초대를 보내기 위해 쿼리를 했습니다. 만약 uninvitedGuests가 일반적인 ArrayList라면 예상한 대로 동작해서 모든 손님이 초대를 받을 수 있을 겁니다.

하지만 RealmResults는 라이브로 업데이트되고, for 루프 안에서 트랜잭션을 실행하기 때문에 Realm은 자동으로 순회할 때마다 RealmResults를 업데이트합니다. 이 경우 uninvitedGuests 리스트에서 손님이 삭제돼서 전체 배열에서 변동이 일어나지만, for 루프가 현재 인덱스를 추적하기 때문에 인덱스가 잘못된 위치에 있게 됩니다. 결과적으로 절반의 손님만 초청을 받게 되므로 잘못된 결과로 이어집니다.

이를 해결하기 위해서는 두 가지 해결 방법이 있습니다.

// 1. 전체 루프를 트랜젝션 안에 감싸기
realm.beginTransaction()
for (i = 0; i < uninvitedGuests.size(); i++) {
  uninvitedGuests.get(i).sendInvite();
}
realm.commitTransaction();

// 2. 리스트를 역방행으로 순회하기
for (int i = uninvitedGuests.size() - 1; i >= 0; i--) {
  realm.beginTransaction()
  uninvitedGuests.get(i).sendInvite();
  realm.commitTransaction();
}

두 가지 해결 방안 모두 명확하지 않으므로, Realm Java 0.89에서는 RealmResults의 모든 업데이트를 Handler.postAtFrontOfQueue를 사용해서 다음 루퍼 이벤트로 연기하도록 했습니다.

이렇게 해서 간단한 for 루프가 예상대로 작동할 수 있게 됐지만, RealmResults의 동기화가 약간 지연된다는 반대 급부가 있었습니다. 예를 들어 guest 객체에 boolean 값을 설정하는 대신 객체 자체를 삭제하는 경우 쿼리 결과에는 여전히 존재해서 잘못된 객체가 될 수 있습니다.

realm.beginTransaction();
guests.get(0).deleteFromRealm(); // Delete the object from Realm
realm.commitTransaction();
guests.get(0).isValid() == false; // You could now get a reference to a deleted object.

이런 경우가 실제 경우에 거의 발생하지는 않으므로 Realm의 핵심 설계 결정을 위반하지 않고 표준 반복 동작을 구현했다고 할 수 있습니다.

3.0 릴리즈에서 우리는 RealmResults가 다시 온전히 라이브가 되도록 해서 이런 결정을 재검토했습니다. 하지만 라이브 컬렉션이 여전히 간단한 for 루프에서 사용하기 쉽도록 createSnapshot() 메서드를 통해 현재 안정적인 목록을 제공합니다. 간단한 for 루프 안에서 데이터가 변경되는 경우 이 스냅샷을 사용하면 됩니다. 단, 대부분의 경우 루프 안에서 트랜잭션을 발생시키는 것이 여전히 추천하지 않는 패턴임을 기억하세요.

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

Iterator가 신 뒤에서 이 스냅샷을 사용하므로 항상 사용하던 방식에서 기대하는 대로 동작합니다.

realm.beginTransaction();
RealmResults<Person> uninvitedGuests = realm.where(Person.class).equalTo("inviteSent", false).findAll();
for (Person guest : guests) {
    realm.beginTransaction();
    uninvitedGuests.setInvited(true);
    realm.commitTransaction();
}

여러 가지 이유로 이 기능을 추가했는데, 그중 하나는 정밀한 컬렉션 알림을 지원하는데 내부 리팩토링이 필요했기 때문입니다. 또한 RealmResultsRealmList를 온전히 라이브로 만들어서 모든 Realm 클래스가 동일한 의미를 가지게 되면 더 쉽게 이들에 대해 추론하거나 사용법을 문서로 만들 수 있기 위해서입니다. 따라서 과거에는 다소 혼란스러울 수 있었던 Realm 클래스의 라이브 특성을 완벽하게 지원하고 문서로 만들어서 더욱 강력한 API를 사용자에게 제공할 수 있게 되었습니다.

이런 변경 사항이 기존의 코드 베이스에 문제가 될 수 있지만, 간단한 for 루프 내에서 쓰기 트랜잭션을 수행하는 경우에만 영향을 미치며 다른 경우에는 코드가 정상적으로 계속 작동합니다. 이런 변화 방향이 장기적으로 보다 나은 결과로 이어질 수 있을 것으로 기대합니다.

관계 기준 정렬

지금까지는 RealmResults를 직접적인 속성으로만 정렬할 수 있었습니다. 하지만 이번 Realm Java 3.0 릴리즈부터는 다 대 일 관계를 맺고 있는 객체의 속성으로 정렬하도록 쿼리할 수 있습니다.

예를 들어 주인이 키우고 있는 개의 나이로 Person 객체를 정렬하려면 다음과 같이 하면 됩니다.

RealmResults<Person> persons = realm.where(Person.class).findAllSorted(dog.age, Sort.ASCENDING);

또한 지난 릴리즈의 몇몇 버그를 수정했습니다. 전체 변경 사항에 대해서는 changelog를 확인하세요.


3.0 릴리즈 소식을 함께 해주셔서 감사합니다. 이제 Realm을 사용해서 멋진 앱을 만들어 보세요! 언제나 Stack Overflow, GitHub, Facebook page에서 저희를 만날 수 있습니다!


Realm Team

Realm의 미션은 더 나은 앱을 빠르게 개발할 수 있도록 돕는 것입니다. 이를 위해 저희는 개발자들이 실시간 협업, 가상 현실, 라이브 데이터 동기화, 오프라인 경험, 메시징 등 정교하고 강력한 기능을 쉽게 개발할 수 있도록 하는 개발 도구와 플랫폼을 제공하고 있습니다.

저희는 모바일 인터넷이 수많은 사용자와 보다 많은 디바이스가 속한 개방형 네트워크와 이들 간의 실시간 상호 작용으로 진화할 것이라고 믿으며, 개발자가 이같은 방향으로 발전할 수 있도록 돕기 위해 저희 제품들을 개발하고 있습니다.

이런 개발 뉴스를 더 만나보세요