Realm과 RxJava 사용하기

이 글은 우리의 사용자 Kirill Boyarshinov의 글입니다. 이 글은 원래 그의 블로그에 올라갔습니다. Kirill은 Live Typing의 리드 프로그래머입니다. Live Typing은 웹과 iOS, 안드로이드, 윈도 폰용 모바일 앱을 만드는 러시아 회사에요. 깃헙트위터에서 Kirill을 만나보세요.


이 블로그 글에서 Realm과 RxJava를 같이 사용하는 법을 설명합니다. Realm은 안드로이드용으로 나온 새로운 모바일 우선 NoSQL 데이타베이스입니다. RxJava는 비동기와 순차 옵저버블를 이용한 이벤트 기반의 프로그램을 조합하는 라이브러리입니다. 저는 1년 이상 안드로이드 개발에서 RxJava를 핵심 라이브러리 스택에 포함하고 있습니다. RxJava에 익숙하지 않다면 Grokking RxJava 시리즈를 참고하세요.

Realm 모델 정의하기

일반적으로 RealmRealmObject 인스턴스를 사용하여 Realm을 사용합니다. Realm들은 각각 데이타베이스며 하나의 파일로 디스크에 매핑됩니다. Realm 객체들은 Realm으로부터 얻을 수 있고 파일의 어떤 데이터에 매핑됩니다. 모든 RealmObject는 모든 게터와 세터가 오버라이드된 생성된 프록시 클래스에 백업됩니다.

예를 들어 깃헙 이슈의 단순한 모델은 이렇습니다.

public class RealmIssue extends RealmObject {
    private String title;
    private String body;
    private RealmUser user;
    private RealmList<RealmLabel> labels;

    // standard getters and setters..
}

public class RealmUser extends RealmObject {
    private String login;

    // standard getters and setters..
}

public class RealmLabel extends RealmObject {
    private String name;
    private String color;

    // standard getters and setters..
}

비동기 접근

RxJava는 대단한 기능이 있습니다. 객체가 옵저버와 서브스크라이브의 여러 스레드를 넘어갑니다. 비동기 데이타 베이스 쿼리는 observeOnsubscribeOn를 사용하면 매우 간단해집니다. 하지만 Realm은 강력한 제한이 있습니다. Realm, RealmObject, RealmResults 인스턴스는 스레드를 넘을 수 없습니다. 이 제약 때문에 우리는 아래의 규칙을 지키며 Realm과 RxJava를 써야 합니다.

  • RealmObject는 UI 스레드 밖에서만 사용해야 합니다.
  • RealmObject가 Observable.map(...)를 통해 옵저빙되기 전에 불변 UI 객체에 매핑되어야 합니다.

RxJava의 내부적인 지원 전에는 우리는 이 규칙을 지켜야 합니다. UI 오브젝트는 Realm 객체와 같은 필드 구성을 가지는 불변 POJO입니다.


public class Issue {
    private final String title;
    private final String body;
    private final User user;
    private final List<Label> labels;

    public Issue(String title, String body, User user, List<Label> labels) {
        this.title = title;
        this.body = body;
        this.user = user;
        this.labels = labels;
    }

    // getters..
}

public class User {
    private final String login;

    public User(String login) {
        this.login = login;
    }

    // getters..
}

public class Label {
    private final String name;
    private final String color;

    public Label(String name, String color) {
        this.name = name;
        this.color = color;
    }

    // getters..
}

Realm 옵저빙하기

RealmObservable을 아래와 같이 상상해 봅시다.

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

  • 원자적 연산을 실행합니다. 읽기, 쓰기, 삭제, 결합
  • 단일 Realm 인스턴스를 제어합니다. 생성과 닫기
  • 사용자 정의 함수에 Realm 인스턴스를 인자로 전달하여 설정.

Realm은 이런 트랜잭션을 공통적인 begin, commit, cancel 흐름으로 지원합니다. 그들은 우리가 원하는 원자성에 딱 맞습니다.

Observable 조건에 따르는 RealmObject 서브클래스를 위한 Observable.OnSubscribe를 구현해 봅시다.

public abstract class OnSubscribeRealm<T extends RealmObject> implements Observable.OnSubscribe<T> {
    private Context context;
    private String fileName;

    public OnSubscribeRealm(Context context) {
        this.context = context;
        fileName = null;
    }

    public OnSubscribeRealm(Context context, String fileName) {
        this.context = context;
        this.fileName = fileName;
    }

    @Override
    public void call(final Subscriber<? super T> subscriber) {
        final Realm realm = fileName != null ? Realm.getInstance(context, fileName) : Realm.getInstance(context);
        subscriber.add(Subscriptions.create(new Action0() {
            @Override
            public void call() {
                try {
                    realm.close();
                } catch (RealmException ex) {
                    subscriber.onError(ex);
                }
            }
        }));

        T object;
        realm.beginTransaction();
        try {
            object = get(realm);
            realm.commitTransaction();
        } catch (RuntimeException e) {
            realm.cancelTransaction();
            subscriber.onError(new RealmException("Error during transaction.", e));
            return;
        } catch (Error e) {
            realm.cancelTransaction();
            subscriber.onError(e);
            return;
        }
        if (object != null) {
            subscriber.onNext(object);
        }
        subscriber.onCompleted();
    }

    public abstract T get(Realm realm);
}

RealmResultsRealmList의 구현은 같습니다. Func1<Realm, T>를 인자로 받는 정적 메서드를 제공하는 함수들을 간편히 사용하기 위해 헬퍼 객체를 만듭시다.

public final class RealmObservable {
    private RealmObservable() {
    }

    public static <T extends RealmObject> Observable<T> object(Context context, final Func1<Realm, T> function) {
        return Observable.create(new OnSubscribeRealm<T>(context) {
            @Override
            public T get(Realm realm) {
                return function.call(realm);
            }
        });
    }

    public static <T extends RealmObject> Observable<T> object(Context context, String fileName, final Func1<Realm, T> function) {
        return Observable.create(new OnSubscribeRealm<T>(context, fileName) {
            @Override
            public T get(Realm realm) {
                return function.call(realm);
            }
        });
    }
}

데이터 서비스

데이터베이스에서 이슈를 담당할 간단한 DataService 인터페이스를 정의해봅시다.

public interface DataService {
    public Observable<List<Issue>> issues();
    public Observable<Issue> newIssue(String title, String body, User user, List<Label> labels);
}

RealmObsevables을 이용해서 구현해 봅시다.

public class RealmDataService implements DataService {
    private final Context context;

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

    @Override
    public Observable<Issue> newIssue(final String title, final String body, final User user, List<Label> labels) {
        // map internal UI objects to Realm objects
        final RealmUser realmUser = new RealmUser();
        realmUser.setLogin(user.getLogin());
        final RealmList<RealmLabel> realmLabels = new RealmList<RealmLabel>();
        for (Label label : labels) {
            RealmLabel realmLabel = new RealmLabel();
            realmLabel.setName(label.getName());
            realmLabel.setColor(label.getColor());
            realmLabels.add(realmLabel);
        }
        return RealmObservable.object(context, new Func1<Realm, RealmIssue>() {
            @Override
            public RealmIssue call(Realm realm) {
                // internal object instances are not created by realm
                // saving them using copyToRealm returning instance associated with realm
                RealmUser user = realm.copyToRealm(realmUser);
                RealmList<RealmLabel> labels = new RealmList<RealmLabel>();
                for (RealmLabel realmLabel : realmLabels) {
                    labels.add(realm.copyToRealm(realmLabel));
                }
                // create RealmIssue instance and save it
                RealmIssue issue = new RealmIssue();
                issue.setTitle(title);
                issue.setBody(body);
                issue.setUser(user);
                issue.setLabels(labels);
                return realm.copyToRealm(issue);
            }
        }).map(new Func1<RealmIssue, Issue>() {
            @Override
            public Issue call(RealmIssue realmIssue) {
                // map to UI object
                return issueFromRealm(realmIssue);
            }
        });
    }

    private static Issue issueFromRealm(RealmIssue realmIssue) {
        final String title = realmIssue.getTitle();
        final String body = realmIssue.getBody();
        final User user = userFromRealm(realmIssue.getUser());
        final RealmList<RealmLabel> realmLabels = realmIssue.getLabels();
        final List<Label> labels = new ArrayList<>(realmLabels.size());
        for (RealmLabel realmLabel : realmLabels) {
            labels.add(labelFromRealm(realmLabel));
        }
        return new Issue(title, body, user, labels);
    }

    private static User userFromRealm(RealmUser realmUser) {
        return new User(realmUser.getLogin());
    }

    private static Label labelFromRealm(RealmLabel realmLabel) {
        return new Label(realmLabel.getName(), realmLabel.getColor());
    }
}

사용

RealmDataService를 액티비티나 프래그먼트에서 사용할 수 있습니다.

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    dataService = new RealmDataService(this);
}

private void requestAllIssues() {
    Subscription subscription = dataService.issues().
        subscribeOn(Schedulers.io()).
        observeOn(AndroidSchedulers.mainThread()).
        subscribe(
            new Action1<List<Issue>>() {
                @Override
                public void call(List<Issue> issues) {
                    Log.d(TAG, "Issues received with size " + issues.size());
                }
            },
            new Action1<Throwable>() {
                @Override
                public void call(Throwable throwable) {
                    Log.e(TAG, "Request all issues error", throwable);
                }
            }
        );
    if (compositeSubscription != null) {
        compositeSubscription.add(subscription);
    }
}

private void addNewIssue() {
    String title = "Feature request: removing issues";
    String body = "Add function to remove issues";
    User user = new User("kboyarshinov");
    List<Label> labels = new ArrayList<>();
    labels.add(new Label("feature", "FF5722"));
    Subscription subscription = dataService.newIssue(title, body, user, labels).
        subscribeOn(Schedulers.io()).
        observeOn(AndroidSchedulers.mainThread()).
        subscribe(
            new Action1<Issue>() {
                @Override
                public void call(Issue issue) {
                    Log.d(TAG, "Issue with title " + issue.getTitle() + " successfully saved");
                }
            },
            new Action1<Throwable>() {
                @Override
                public void call(Throwable throwable) {
                    Log.e(TAG, "Add new issue error", throwable);
                }
            }
        );
    if (compositeSubscription != null) {
        compositeSubscription.add(subscription);
    }
}

전체 코드는 깃헙에서 받을 수 있습니다.

결론

Realm은 좋은 모바일 데이터베이스입니다. SQL을 쓰지 않고 빠르고 단순하게 사용할 수 있습니다. 하지만 RxJava와 함께 쓰려면 보일러 플레이트 코드가 필요합니다. 명시적으로 UI 객체들을 만들고 Realm 객체들과 연결해야합니다. 이 과정은 AutoValue를 이용하여 단순화할 수 있습니다. 나는 내부 지원이 스레드 이슈들을 해결했으면 좋겠습니다. Realm을 사용하고자 한다면 전체 Realm 문서를 살펴보세요. 다음 블로그 글에서는 복잡한 Realm 객체들을 다루는 좋은 예를 작성하고, 어떻게 데이터 통합하는지, 중복과 불필요한 것을 어떻게 막는지를 다루려고 합니다.


블로그의 글을 사용가능하게 해준 Kirill Boyarshinov에게 깊은 감사를 드립니다. 그의 블로그의 튜토리얼이나 Realm 관련 글을 공유하고 싶다면 깃헙이나 이메일 통해 연락해보세요.

다음: Realm Mobile Platform으로 실시간 협업 기능과 확장이 가능한 리액티브 앱을 만들어 보세요.

General link arrow white

컨텐츠에 대하여

이 컨텐츠는 저자의 허가 하에 이곳에서 공유합니다.


Kirill Boyarshinov

Hi! My name is Kirill Boyarshinov and I’m software engineer. Now working as Lead Android Developer at Touché in sunny/rainy Singapore.

Things I do: hacking, contributing to open-source projects, giving tech talks at conferences and webinars, supporting developers’ communities, working out and skateboarding :)

4 design patterns for a RESTless mobile integration »

close