모바일 앱 개발시에 DB는 주로 Sqlite 또는 Core Data를 많이 사용한다. 그런데 Sqlite 등을 사용하는 것이 조금 불편한 부분들이 많아서 지난 앱 개발시 Realm을 사용했다. Realm을 사용하면 개발이 용이해지기는 한데 간혹 다루기가 까다로워지는 경우가 있다. 사실 그런 문제 상황은 내가 사용시 뭔가를 잘못했을 확률이 높지만 개인적으로 다소 예민한 라이브러리라고 생각한다. 어찌보면 당연할 수도 있는 것이 아직 1.0 버전이 되지도 못한 라이브러리니 그럴만도 하다. 실제로 아직은 버전업이 자주 발생하고 버전업시에 변경 사항이 다소 있는 편이다. 중요 변경이 있을 때 마이그레이션을 잘못하거나 하면 앱 크래시의 원인이 되기도 하니 버전업시에는 유의해야 한다. 그렇지만 다행인 것은 문서화가 잘되어 있고 한글본도 번역도 빠르게 올라온다.
얼마전 기존 프로젝트의 Realm의 0.82.2에서 0.86.0으로 버전업을 했다(버전을 확인한지 그리 오래되지 않았는데 그 사이 많이도 버전업되었다). 그랬더니 아래와 같은 오류가 발생했다.
Field 'date' is required. Either set @Required to field 'date' or migrate using io.realm.internal.Table.convertColumnToNullable(). : io.realm.exceptions.RealmMigrationNeededException: Field 'date' is required. Either set @Required to field 'date' or migrate using io.realm.internal.Table.convertColumnToNullable().
사용하는 테이블의 date
필드가 required
이니 @Required
어노테이션을 붙이거나 테이블을 마이그레이션하라는 안내다. 이미 Realm에서 해결 방법을 친절히도 안내해주고 있다. @Required
는 기존에 사용하지 않던 것이라 찾아봤다. Realm 자바 0.83 — Null 지원!를 보니 0.83 버전에서 데이터값으로 null을 사용할 수 있게 됨에 따라 추가된 어노테이션이다. 그러니 기존 0.82.2의 데이터는 null이 될 수 없었으므로 required 타입으로 인식하는 것이다. 그외에도 다른 변경 사항을 보고 싶다면 changelog를 참고하자.
기존에도 null이 들어올 수 없는 구조로 프로그램되어 있었으니 @Required
를 해당 필드(변수)에 붙이는 것으로 해결하는 것이 맞겠다. 나는 여기서 다른 문제를 겪었었는데 @Required
만 붙이면 될 것을 스키마 버전 정보까지 올리는 바람에 엉망이 되었었다. 어쨌든 결론은 이 문제에 대해서는 @Required
를 붙이는 방법으로 간단히 해결된다.
그런데 문제는 여기서 끝나지 않았다. RealmMigration 클래스도 방식이 변경되어 맞춰줬다.
그리고 또 하나. 기존에는 아래와 같이 인스턴스를 생성해서 사용했다.
RealmConfiguration.Builder realmBuilder = new RealmConfiguration.Builder(context).name(realmName) .schemaVersion(SCHEME_VERSION);
RealmConfiguration config = realmBuilder.build();
Realm realm = Realm.getInstance(config);
그런데 여기서 문제가 발생하기 시작했다. 이 부분은 realm 접속을 생성하는 과정으로 사용하는 시점에 처리하고 있었다. 기존에 사용처 자체가 워낙에 간단한 부분이라서 그랬는지는 몰라도 문제가 없었다. 그런데 버전업 이후 이상하게 앱 크래시가 발생했다.
E/AndroidRuntime: FATAL EXCEPTION: main
Process: ~~~, PID: 21532
java.lang.RuntimeException: Unable to start activity ComponentInfo{~~~/~~~}: java.lang.IllegalArgumentException: Configurations cannot be different if used to open the same file.
...
오류 내용은 이렇다. 동일 table 파일을 다른 Configurations으로 열 수 없다. 이 오류는 처음에 앱이 열릴 때는 발생하지 않다가 종료 후 다시 들어오면 발생했다. 이상했다. 분명이 앱을 종료 했음에도 불구하고 왜 다른 Configurations을 넣고 있다는 것인지.
이번에 알게 되었는데 root activity에서의 finish()는 앱의 종료를 의미하는 것이 아니었다. 마지막 activity가 finish()되면 화면에서는 즉시 종료된 것으로 보이나 프로세스는 살아 남는다. 아마도 시스템 자원이 부족해지면 프로세스가 OS에 의해 죽을 수는 있다. 이와 관련해서 앱을 완전히 종료하는 방법에 대해서는 조만간 별도로 글을 남기려고 한다. 안드로이드 앱 종료 방법에서 설명하고 있다.
어쨌든 이러한 상황이 발생하는 것으로 보아 기존 버전과 최신 버전의 Realm 생명 주기를 관리하는 방법이 변경된 것으로 보인다. 메뉴얼에 보면 모범 사용예 - Realm 인스턴스들의 생명주기 관리하기라는 부분이 있다. "모범 사용예"라는 항목 자체가 0.85.0 버전부터 있는 것으로 보아 그 시점에 변경이 된 것으로 생각한다. 그런데 여기서 사용하는 함수는 Realm Java 0.81.1에서 추가된 것으로 소개하고 있다. 기존에 내가 사용하던 것이 0.82.2 였지만 위 문제가 발생하지 않았던 것으로 보아 앞선 예상대로 그 후에 관리 방법이 변경된 것 같기는 하다. 어쨌든 이 부분에 관련된 정보가 있어 발췌한다.
Realm 인스턴스들의 생명주기 관리하기
RealmObjects과 RealmResults는 데이터 전체를 느긋하게 가져옵니다. 이런 이유로 Realm 오브젝트나 질의 결과를 접근할 때 가능한 오래 Realm 인스턴스를 유지하는 것이 중요합니다. Realm 데이터 커넥션을 열고 닫는 추가 비용을 줄이기 위해 레퍼런스 카운트화된 캐시를 가집니다. 이는 Realm.getDefaultInstance()를 같은 스레드에서 여러번 호출하는 것은 비용이 들지 않고 내부의 리소스는 자체적으로 모든 인스턴스가 닫히면 해제됨을 의미합니다.
모든 액티비티와 프래그먼트의 UI 스레드에서 Realm 인스턴스를 열고 Activity나 Fragment가 파괴될 때 닫는 것은 쉽고 안전한 접근 법입니다.
// 애플리케이션에서 Realm 설정하기
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
RealmConfiguration realmConfiguration = new RealmConfiguration.Builder(this).build();
Realm.setDefaultConfiguration(realmConfiguration);
}
}
// 액티비티들을 전환하며 onCreate()/onDestroy()가 중첩되면 Activity 2의 onCreate가
// Activity 1의 onDestroy()보다 먼저 호출 됩니다.
public class MyActivity extends Activity {
private Realm realm;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
realm = Realm.getDefaultInstance();
}
@Override
protected void onDestroy() {
super.onDestroy();
realm.close();
}
}
// 프래그먼트에서 onStart()/onStop()를 사용합니다.
// 프래그먼트의 onDestroy()는 호출되지 않을 수 있습니다.
public class MyFragment extends Fragment {
private Realm realm;
@Override
public void onStart() {
super.onStart();
realm = Realm.getDefaultInstance();
}
@Override
public void onStop() {
super.onStop();
realm.close();
}
}
그렇다. 이제는 앱 생성시 Realm.setDefaultConfiguration()
를 이용해서 Configuration을 지정하고 필요한 곳에서 Realm.getDefaultInstance()
으로 인스턴스를 받아 사용하는 것을 권장한다. 위에 이야기한 문제도 이 방식을 사용하면 발생하지 않는다.