ObjectBox Converter 베이스 모음집

도입

최근 진행하고 있는 개인 프로젝트인 ‘どこでもゆかりん’ 프로젝트에서 ObjectBox 를 중심으로 로직을 구성하는 기능이 있다.

ObjectBox에는 Converter란 개념이 있는데, ObjectBox가 지원하지 않는 타입을 사용할 경우에 지원하는 형태로 map할 수 있는 기능이다.

이 글에서는 해당 프로젝트에서 사용한 ObjectBox Converter의 베이스 형태를 작성해둔 것을 정리하려 한다.

Enum

import io.objectbox.converter.PropertyConverter

open class PropertyEnumConverter<T, R>(private val values: Array<T>, private val default: T,
                                       private val predicate: T.(R) -> Boolean,
                                       private val supplier: (T) -> R?) : PropertyConverter<T, R> {

    override fun convertToEntityProperty(databaseValue: R?): T? {
        if (databaseValue == null) return null
        for (state in values) {
            if (state.predicate(databaseValue)) {
                return state
            }
        }
        return default
    }

    override fun convertToDatabaseValue(entityProperty: T?): R? {
        if (entityProperty == null) return null
        return supplier(entityProperty)
    }
}
 class VoiceEngineConverter : PropertyEnumConverter<VoiceEngine, String>(
        VoiceEngine.values(),
            NONE, { this.id == it }, { it.id })

첫 번째 파라미터에는 해당 Enum의 전체값, 두 번쨰 파라미터에는 값이 없을 때의 기본값, 세번째는 Enum과 db에 있는 값을 map 할 때 사용할 조건, 마지막 네번째 필드는 map 할 값이다.

Json

보통 커스텀 클래스를 다른 엔터티의 값으로 사용하려 할 때 발생한다.

import com.google.gson.Gson
import io.objectbox.converter.PropertyConverter

open class PropertyJsonConverter<T>(private val cls: Class<T>) : PropertyConverter<T, String> {
    override fun convertToEntityProperty(databaseValue: String?): T? {
        if (databaseValue == null) return null
        return Gson().fromJson(databaseValue, cls) as T
    }

    override fun convertToDatabaseValue(entityProperty: T?): String? {
        if (entityProperty == null) return null
        return Gson().toJson(entityProperty)
    }
}
class PresetItemConverter : PropertyJsonConverter<PresetItem>(PresetItem::class.java)

Local JUnit Test on Objectbox

도입

최근 앱 내부에 모델을 저장해야 되는 일이 있을 때에 Eventbus로 유명한 Greenrobot가 제작하는 ObjectBox 를 많이 활용한다.

Entity, 즉 ObjectBox와 프로젝트를 연결하는 Manager 라는 클래스를 별도로 제작하여 이 클래스 안에서 ObjectBox에 관련된 작업 (CRUD, 정렬, 검색 등)을 진행하고 하위 코드(ViewModel) 단에서 이 Manager 클래스를 Inject받아 RxJava로 활용하는 방식이다.

이 방식에 약간의 문제점이 있다면 하위 코드를 작성하기 전까지는 해당 Manager 클래스를 테스트하기 어렵다는 점인데, 다행히 ObjectBox는 JUnit를 통한 단위 테스트를 제공한다.

따라서 이 글에서는 간단히 JUnit로 ObjectBox 와 RxJava2를 테스트하는 방법을 살펴보려 한다.

테스트 코드의 언어는 Java를 사용했다.

ObjectBox 테스트 환경 제작

ObjectBox에서는 JUnit로 테스트할 때에 OS(Windows / macOS / Linux)에 맞는 바이너리 파일을 받아와 mdb 파일을 생성하고 데이터를 실제로 넣는 작업을 진행한다.

먼저, JUnit 클래스를 만들고 작업이 시작되기 전과 후에 Objectbox에 대한 작업을 진행한다.

    private static final File TEST_DIRECTORY = new File("objectbox-example/test-db");
    private BoxStore mBoxStore;

    @Before
    public void setUp() throws Exception {
        // delete database files before each test to start with a clean database
        BoxStore.deleteAllFiles(TEST_DIRECTORY);
        mBoxStore = MyObjectBox.builder()
                // add directory flag to change where ObjectBox puts its database files
                .directory(TEST_DIRECTORY)
                // optional: add debug flags for more detailed ObjectBox log output
                .debugFlags(DebugFlags.LOG_QUERIES | DebugFlags.LOG_QUERY_PARAMETERS)
                .build();
    }

    @After
    public void tearDown() throws Exception {
        if (mBoxStore != null) {
            mBoxStore.close();
            mBoxStore = null;
        }
        BoxStore.deleteAllFiles(TEST_DIRECTORY);
    }

@Before 가 붙은 setUp() 의 메서드 실행 결과로 BoxStore 객체가 생성되므로, Box 객체를 받아올 때에는mBoxStore.boxFor(Class) 를 실행하면 된다.

그 다음 @After 가 붙은 tearDown 에서는 테스트가 종료될 때 생성된 DB 파일을 지워 이전의 테스트 결과가 현재의 테스트 결과에 영향을 주지 않게 변경한다.

ObjectBox 작업 테스트하기

본 예제에서 테스트할 작업은 DB에서 시작 날짜와 종료 날짜 사이의 데이터를 날짜 기준으로 정렬하여 리스트를 가져오는 작업이다.

public Observable<List<CalendarData>> getCalendarDataList(Calendar startCalendar, Calendar endCalendar) {
    return Observable.create(emitter -> {
        int startDay = CalendarUtils.getFormatDay(startCalendar);
        int endDay = CalendarUils.getFormatDay(endCalendar);

        List<CalendarData> sorted = Stream.of(mBox.query()
                .between(CalendarData_.formatDay, startDay, endDay)
                .build()
                .find())
                .sortBy(value -> value.formatDay)
                .toList();

        emitter.onNext(sorted);
    });
}

그리고 위 메서드에 대해 테스트 케이스를 작성하면, RxJava 구독을 성공했고 에러 없이 작업이 될 경우일 것이다.

@Test
public void getCalendarDataWeakList() {
    TestObserver<List<CalendarData>> dataTestObserver = new TestObserver<>();

    Calendar startCalendar = Calendar.getInstance();
    Calendar endCalendar = Calendar.getInstance();

    startCalendar.set(Calendar.DAY_OF_WEEK, 1);
    endCalendar.set(Calendar.DAY_OF_WEEK, endCalendar.getActualMaximum(Calendar.DAY_OF_WEEK));

    mCalendarDataManager.getCalendarDataList(startCalendar, endCalendar)
            .subscribe(dataTestObserver);

    dataTestObserver.assertSubscribed()
            .assertNoErrors();
}

여기서 TestObserver 라는 클래스가 있는데, TestObserver 는 RxJava의 다양한 행동을 검증할 수 있는 메서드를 제공한다.

상기한 ‘구독을 성공했다’ 에 대한 검증은 assertSubscribed()를, 에러 없이 작업이 될 경우는 assertNoErrors() 를 사용했다.

위 테스트 케이스를 구동하면 아래와 같은 로그가 나온다.

[INFO ] Creating query #1 for CalendarData with 1 condition(s)
[INFO ] Finding using query #1
[INFO ] Parameters for query #1:
formatDay between 20181104 and 20181110
[INFO ] Creating query #2 for CalendarData with 1 condition(s)
[INFO ] Finding scalars using query #2
[INFO ] Creating query #3 for CalendarData with 1 condition(s)
[INFO ] Finding using query #3
[INFO ] Parameters for query #3:
formatDay between 20181104 and 20181110

Process finished with exit code 0

setUp 메서드에서 설정한 대로 ObjectBox가 표시하는 디버그 로그가 나오면서, 작업이 성공한다.

정리

이처럼 ObjectBox와 RxJava가 실제 환경과 거의 같은 환경을 제공하는 덕분에, 하위 코드가 작성되기 전 해당 클래스에 대한 검증이 가능했다.