Droidknights realm

Realm 내부 구조와 동작 원리 자세히 살펴보기

안드로이드 개발자들을 위한 수준 있는 독립 컨퍼런스인 Droid Knights에서 “Anatomy of Realm”이라는 주제로 많은 호응을 얻은 Realm 개발자 직강입니다.


소개

Realm에서 일하고 있는 김용욱입니다. Realm을 한 마디로 소개하면 SQLite 다음으로 많이 사용되는 데이터베이스입니다. 얼마 전 Droid Kaigi라는 일본의 행사에서도 많은 호응을 얻었습니다.

3분 만에 살펴보는 Realm 사용법


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

Dog dog = new Dog();
dog.name = "Rex";
dog.age = 1;

Realm realm = Realm.getDefaultInstance();
realm.beginTransaction();
realm.copyToRealm(dog);
realm.commitTransaction()

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

RealmObject 객체를 상속받아서 객체를 만들면 모델이 만들어지고, 모델의 값을 변경하고 Realm 인스턴스를 열고 beginTransaction을 시작해서 copyToRealm을 하고 commitTransaction를 하면 데이터베이스에 넣을 수 있습니다. 객체를 가져올 때는 where를 사용합니다.

Realm 모바일 플랫폼 소개

anatomy-realm-rmp

Realm 모바일 플랫폼은 두 가지 요소로 이뤄집니다. 첫 번째는 앞서 말씀드린 Realm 모바일 데이터베이스입니다. 다음으로는 Realm 오브젝트 서버인데, 서버 단으로 동기화를 할 수 있습니다. 이 두 가지 요소를 합쳐 Realm 모바일 플랫폼이라고 합니다.

anatomy-realm-rmp-example

Realm 모바일 플랫폼의 동작하는 예시를 말씀드리겠습니다. 클라이언트에서 팀 쿡의 사진을 찍어서 클라이언트 내장 데이터베이스에 넣으면 오브젝트 서버의 데이터베이스로 자동으로 동기화가 이뤄집니다. 서버 단에 올라간 사진은 이벤트 핸들러를 호출하고 사진을 처리하는 메서드를 부를 수 있습니다. 이렇게 사진이 처리되고 서버 단의 데이터베이스에서 사진 갱신이 이뤄지면 또 자동으로 로컬의 데이터베이스가 갱신되고, 클라이언트가 이 갱신 사실을 알고 UI를 갱신하게 됩니다. 전통적인 앱 개발 과정을 생각하면 REST API 호출과 시리얼라이제이션, 에러 핸들링 등 복잡한 과정을 생각할 필요 없이 간단하게 데이터 동기화를 할 수 있습니다. 네트워크 스택에 대한 이해와 충돌 해결에 대한 괴로움 없이 앱 개발을 쉽게 만들어 줍니다.

Realm 모바일 데이터베이스 소개

이제부터는 앱 개발에 도입할 때 더욱 잘 이해할 수 있게 돕고 나아가 오픈 소스인 Realm의 개발에 참여할 수 있도록 장려할 수 있도록 Realm 모바일 데이터베이스가 어떤 기술을 쓰고 있는지 소개하겠습니다.

Realm 모델 객체의 특성

Realm 모델 객체는 특정 객체를 상속받거나 구현해야 합니다. 즉, 상속은 RealmObject를 상속하거나, RealmModel을 구현해야 합니다. 이렇게 만든 모델 객체는 Realm에 관리되는 managed, 혹은 일종의 POJO 객체인 unmanaged 두 가지 상태 중 하나를 가집니다.

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

이런 두 가지 상태를 가지는 이유는 무 복제 메커니즘과 크로스 플랫폼 호환성, 자동 갱신이라는 이점을 갖기 위해서입니다.

무 복제 메커니즘

특히 무 복제 메커니즘은 실제로 사용되기 전에는 Java에 데이터가 복제되지 않는 작동 원리입니다. Realm은 C++로 된 코어 코드가 있는데 여기서 할당된 데이터는 실제 사용 전에는 Java Heap으로 복사되지 않습니다. 과거 Facebook 앱은 JSON 인코딩과 디코딩에 많은 시간을 허비했기 때문에 FlatBuffers를 사용해서 사용할 때만 메모리 맵에서 Java로 데이터를 가져올 수 있도록 했습니다. 이런 형태로 Facebook은 많은 성능 향상을 가져올 수 있었습니다.

특히 전통적으로 수화(Hydrate)라는 현상은 성능에 큰 영향을 미칩니다. 수화란 즉 일반적인 데이터베이스를 안드로이드로 가져올 때 문자열이나 기타 인코딩된 형태로 가져오면 파싱 혹은 디코딩을 해서 가져온 데이터를 모든 필드에 대해 몽땅 Java 객체에 담아주는 과정입니다. 이렇게 하면 쓰지도 않은 데이터도 다 가져오고 변환을 거쳐야 하며, 또한 다시 Java 객체로 변환하는 과정에서 쓰지 않을 데이터 역시 전부 다시 변환하고 모두 필드에 담아야 하므로 CPU와 메모리가 낭비됩니다.

anatomy-realm-zero-copy

기존 ORM은 이런 문제에서 본질적으로 자유로울 수 없었지만, Realm은 사용하기 전까지는 복제하지 않으므로 성능이 크게 향상됩니다. Realm 객체를 만들면 이런 코드가 자동으로 생성됩니다.

anatomy-realm-apt

또한, 사용자가 만든 객체에 대해 RealmProxyObject 객체가 자동으로 생성되는데, APT(Annotation Processing Tool)이 이 작업을 대신 해줍니다. 잠깐 APT에 관해 설명하자면 ProcessorAbstractProcessor 두 객체로 구성되는데 보통 AbstractProcessor를 상속받아서 Processor를 구현해서 반복적인 작업을 하도록 할 수 있습니다.

Realm의 APT는 RealmProcessor라는 APT 객체가 있고, 어떤 어노테이션 프로세서를 실행할지 결정하는 메타데이터가 있습니다. 이를 통해 필요한 Java 객체를 만들어냅니다.

anatomy-realm-annotation

그런 다음 처리할 어노테이션을 @SupportedAnnotationTypes 어노테이션으로 지정합니다. Realm에서 사용할 어노테이션을 등록해두면 Proxy 인터페이스와 Proxy 클래스를 만들어 줄 수 있습니다.

anatomy-realm-processing

다음으로 프로세스 메서드를 설명하겠습니다. 프로세스 메서드는 어노테이션 프로세싱의 진입점으로, 어노테이션 프로세싱 툴의 재귀적 처리 때문에 최소한 2라운드까지 진입됩니다.

하지만 기존의 객체를 상속받아 확장할 수만 있고 객체 자체를 변경할 수 없는 APT의 한계 때문에 APT만으로 부족합니다. 따라서 Realm은 객체의 게터/세터에서 lazy하게 데이터를 다룰 수 있는 코드를 넣어야 하기 위해 특별한 조치가 필요했습니다. 버전 1.x 시절에는 게터와 세터를 표준 이름으로만 생성하도록 강제했고, 바이트 코드를 변경해서 사용자가 필드를 읽으면 대신 Realm의 게터를, 필드에 뭔가 쓰면 Realm의 세터를 호출하게 했습니다.

anatomy-realm-transformer

바이트 코드 수정은 Gradle 빌드 과정에 트랜스폼을 빌드 과정에 포함할 수 있는 트랜스포머 API를 사용했습니다. 트랜스포머 API를 사용하려면 Transformer 객체를 만들어야 하므로 이를 상속받아 RealmTransformer 객체를 만들고, Javassist 라이브러리로 바이트 코드를 변경했습니다. 만약 직접 바이트 코드를 바꿀 필요가 있다면 Transformer API와 Javassist를 사용해 보세요.

크로스 플랫폼 호환성

Realm 모바일 데이터베이스 모델 객체가 두 가지 상태를 가지는 두 번째 이유는 Java뿐만이 아니라 iOS, Xamarin, React Native 등 여러 플랫폼 간의 호환을 위해서입니다. 성능과 호환성을 위해 Realm은 많은 노력을 기울이고 있습니다.

자동 갱신

또한, 자료가 다른 곳에서 갱신되면 자동으로 다른 곳에도 갱신되게 하려고 Realm 모델 객체는 현재의 특성을 가지게 됩니다. 즉, 데이터를 추가하면 리스트 뷰가 자동으로 갱신되고, 자동으로 갱신된 데이터를 파악하기 위해 알림이 전달되며, 알림이 전달될 때 UI를 새로 그릴 수 있도록 합니다.

Realm 객체 생성

이런 unmanaged 객체, 즉 POJO 인스턴스를 복사해서 managed 인스턴스를 생성하는 copyToRealm 메서드가 있습니다. 이 경우 자동으로 Proxy 객체를 만들어서 넘겨줍니다. 한편 새로운 managed 인스턴스를 생성하는 Realm.createObject 메서드도 있는데, 처음부터 Proxy 객체로 만드는 개념입니다.

anatomy-realm-create-object

이런 객체가 호출되면 내부적으로 테이블 객체를 가져와서 비어있는 row를 만들고 전달하는 과정을 거칩니다. Realm에서 사용하는 Schema는 일종의 메타데이터이자 모델과 연관된 RealmObjectSchema와 Table을 관리하는데, 내부적으로는 addColumn / removeColumn / renameColumn, addEmptyRow / addEmptyRows / add, getPrimaryKey / hasPrimaryKey / isPrimaryKey, getLong(column, row) / getBoolean(column, row), findFirstLong(column, long) / findFirstBoolean(column, boolean) 등의 메서드가 포함됩니다. 테이블 객체를 직접 호출해서 이런 메서드로 데이터를 만들 수도 있지만 실제로는 직접 호출하지 않고 Proxy 객체에서 만듭니다.

anatomy-realm-table

Realm Java 객체는 Proxy 객체가 Table 객체를 거치고 JNI를 호출해서 마지막으로 Table.cpp라는 C++로 만들어진 코어 객체 안의 객체를 호출하게 됩니다.

public long addColumn(RealmFieldType type, String name, boolean isNullable) {
  verifyColumnName(name);
  return nativeAddColumn(nativePtr, type.getNativeValue(), name, isNullable);
}

Table.java의 실제 코드를 보면 addColumn 이라는 메서드 내에서 JNI 코드를 호출하는 nativeAddColumn 메서드를 호출합니다. 이를 호출하면 C++로 된 JNI가 호출되고, 실제로 Table.cpp를 호출해서 add_column 메서드가 호출됩니다.

anatomy-realm-blg-plus-tree

빅+ 트리와 비슷한 구조이지만 차이점은 테이블 아래 컬럼부터 또 다른 빅+ 트리라는 것이 차이점입니다. 루트에서부터 타고 내려가면 해당 데이터베이스 테이블로 연결이 되고, 테이블에서는 column이 연결됩니다.

SQLite에서는 일반적으로 column이 아니라 row로 연결되는데 column으로 연결되는 이유는 전통적인 데이터베이스는 row 단위로 한 줄씩 저장하므로 이름으로 검색하면 메모리상에서 캐시 라인을 벗어나므로 구조적으로 캐시 미스가 발생할 확률이 높습니다. 또한, 이렇게 한 줄씩 저장하면 용량이 맞지 않아서 패딩 바이트도 넣어야 할 필요도 생깁니다. 예를 들어 8바이트씩 저장하고 있는 데이터에 boolean 필드를 저장하면 7바이트가 낭비되겠죠. 그래서 최근 데이터베이스는 한 column 씩 저장해서 캐시 히트율을 높이고 패딩이 필요 없도록 하고 있습니다.

anatomy-realm-mvcc

또한 MVCC(다중 버전 동시성 제어)를 사용한다는 것이 Realm의 특이 사항입니다. 읽기 트랜잭션이 필요 없이 쓰기 트랜잭션 중에도 읽기가 가능하다는 장점이 있습니다. 또한, 안전하기 때문에 많은 이점이 있습니다. 이런 방식으로 사용자에게는 모든 객체가 스레드 로컬 형태로 된다는 영향이 미칩니다. 개별 스레드는 다른 메타 데이터를 가지기 때문에 배타적이지 않게 읽을 수 있고 다른 스레드에서 쓰고 있을 때도 읽을 수가 있습니다.

배타적이지 않고 효율적으로 읽고 쓰기 위해 메타 데이터를 스레드 단위로 저장하는 스레드 로컬이 다소 혼란스러울 수 있지만 성능 상의 이슈로 채택하고 있으며, 개별 스레드가 다른 메타 데이터를 가지지만 데이터는 메모리 맵으로 연결돼서 아무런 속도 제한 없이 멀티 스레드로 공유됩니다. 즉, 다른 스레드에서 쿼리를 하면 멀티 스레드로 자료가 공유되므로 바로 읽을 수 있습니다. 단, 스레드 로컬로 열린 Realm, RealmList, RealmResults, RealmObjects 객체를 다른 스레드로 전달할 수는 없습니다.

아직 Realm의 스레드 개념이 혼란스럽다면 다른 스레드로 정보를 전달하지 말고 해당 스레드에서 바로 질의하세요. 아무런 부담 없이 사용할 수 있습니다.

Gradle Plugin

환경별로 어노테이션 설치가 어렵고, 동기화 기능이나 어노테이션 전용 코드를 앱 빌드에서 빼기 어려우므로 Gradle Plugin을 따로 만들었습니다.

anatomy-realm-gradle

메타 데이터가 있어서 어떤 객체가 플러그인을 위한 것인지 명시하며, Plugin<Project>를 상속받아서 apply 메서드를 구현하면 됩니다. 메타 데이터 이름을 보고 객체를 찾아서 apply를 호출하는 구조입니다.

Realm의 경우 Groovy로 작성했지만, Java로 작성해도 됩니다. plugin 객체를 상속받아서 apply 메서드를 만드는 것이 동일합니다. Realm Core와 Realm Java가 모두 오픈 소스이므로 관심 있는 분은 많이 참조하고 만드는 직접 제작하는 라이브러리에 적용해보면 좋을 것 같습니다.


본 영상과 글은 Droid Knights의 비디오 스폰서인 Realm에서 제공합니다. 모바일 개발자가 더 나은 앱을 더 빠르게 만들도록 돕는 Realm 모바일 데이터베이스Realm 모바일 플랫폼을 통해 핵심 로직에 집중하고 개발 효율을 높여 보세요! 공식 문서에서 단 몇 분 만에 시작할 수 있습니다. 또한 Realm 홈페이지에서는 모바일 개발자를 위한 다양한 최신 기술 뉴스와 튜토리얼을 제공하고 있으니 즐겨찾기하고 자주 들러 주세요!

다음: Realm 이해하기 #5: Realm의 서버리스 로직: Realm Functions를 소개합니다.

General link arrow white

컨텐츠에 대하여

2017년 3월에 진행한 Droid Knights 행사의 강연입니다. 영상 녹화와 제작, 정리 글은 Realm에서 제공하며, 주최 측의 허가 하에 이곳에서 공유합니다.

Leonardo YongUk Kim

Leonardo YongUk Kim is a software developer with extensive experience in mobile and embedded projects, including: several WIPI modules (Korean mobile platform based on Nucleus RTOS), iOS projects, a scene graph engine for Android, an Android tablet, a client utility for black boxes, and some mini games using Cocos2d-x.

4 design patterns for a RESTless mobile integration »

close