본문 바로가기

3.구현/Java or Kotlin

[java] Collections like SQL

들어가기

Java에 배열, 맵, 셋들 사용해서 자료를 처리하는 중에 여러 조건으로 값을 찾을 경우가 있었다. 그 조건에 중간에 조금씩 계속 바뀌는 상황으로 자료 구조를 생각하면서 수정하기에 번거롭기 시작했다. 혹시 가벼우면서도 SQL 비슷한게 없나 찾아보았다. 물론 메모리 DB를 사용할 수 있지만, 너무 덩치가 커서 배보다 배꼽이 커지는 상황이다. 그러던 중에 Java Collection 라이브러리 중에서 SQL 쿼리와 비슷한 기능을 제공하는 라이브러리가 있었다.

Java에는 간단한 자료 구조로 단순한 기능만 가진다. RDBMS는 기능은 풍부하지만, 간단한 처리에는 부담이 크다. 간단하지만 쉽게 memory db같은 형태의 자료 처리가 필요해서 찾은게 CQEngine(Collection Query Engine)과 Boon이다.

이 둘에 대해 간단한 성능 확인과 결론을 내보려고 한다.

작성자: ospace114@empal.com, http://ospace.tistory.com/

컬렉션 라이브러리

CQEngine

CQEngine(Collection Query Engine)은 SQL 쿼리와 비슷한 조회를 제공한다. DB 테이블 비슷한 형태의 데이터 처리가 가능하다. 그리고 초당 1백만 번 쿼리 처리가 가능하다. 이는 Java 내장 iteration보다 더 빠르다. ㅡ.ㅡ.;

Boon

Java에서 데이터 처리 유틸리티들을 모은 라이브러리라고 할 수 있다. CQEngine에서 영감을 받았다고 한다. DSL 쿼리를 차용했고 CQEngine과 비슷하지만 프레임워크를 목표로 가졌다고 한다. SQL 쿼리 비슷한 컬랙션은 DataRepo에서 제공된다.

데이터 클래스

사용할 데이터 클래스이다 별다른게 없이 단순 데이터 저장용 클래스이다. 문자열 속성이 1개와 정수 속성이 2개로 구성되어 있다.

public class Foo {
    private String name;
    private int num;
    private int idx;
  // getter/setter
}

CQEngine

CQEngine 테스트 코드이다.

final Attribute<Foo, Integer> FOO_NUM = new SimpleAttribute<Foo, Integer>("num") {
    public Integer getValue(Foo foo, QueryOptions qo) { return foo.getNum(); }
};
final Attribute<Foo, String> FOO_NAME = new SimpleNullableAttribute<Foo, String>("name") {
    public String getValue(Foo foo, QueryOptions qo) { return foo.getName(); }
};

IndexedCollection<Foo> fooRepo = new ConcurrentIndexedCollection<Foo>();
fooRepo.addIndex(HashIndex.onAttribute(FOO_NAME));
fooRepo.addIndex(HashIndex.onAttribute(FOO_NUM));
long runtime = Benchmark.runtime(()->{
    for(int i=0; i<1000000; ++i) {
        fooRepo.add(Benchmark.makeFoo());
     }
});
System.out.println("runtime(msec): "+runtime);
int cnt[] = {0};
runtime = Benchmark.runtime(()->{
    Query<Foo> q1 = and(in(FOO_NAME, "MON", "SUN"), equal(FOO_NUM, 0));
    ResultSet<Foo> res = fooRepo.retrieve(q1);
    cnt[0] += res.size();
});
System.out.println("runtime(msec): "+runtime+", cnt:"+cnt[0]);

어트리뷰트 객체를 두 개 생성했곡 각각 num 필드와 name 필드에 대한 정보를 갖는 어트리뷰트 객체이다. 그리고 ConcurrentIndexedCollection으로 스레드에서도 사용가능한 컬랙션 객체를 생성했다. 이때 앞에서 생성한 두개 어트리뷰트 객체들을 인덱싱으로 사용한다.

다음으로 백 만개 데이터 객체를 생성한 컬랙션에 추가하고 실행시간을 측정했다. 그리고, 쿼리 객체를 생성해서 캘랙션 객체에 조회요청을 보낸 실행시간을 측정했다.

Boon 예제

Boon 테스트 코드이다. Boon 실행 중에 ExceptionInInitializerError 예외가 발생할 경우 아래 Troubleshooting을 참고하시기 바란다.

Repo<Integer, Foo> fooRepo = Repos.builder().primaryKey("idx")
        .searchIndex("name").searchIndex("num")
        .build(int.class, Foo.class);
long runtime = Benchmark.runtime(() -> {
    for (int i = 0; i < 1000000; ++i) {
        fooRepo.add(Benchmark.makeFoo());
    }
});
System.out.println("runtime(msec): " + runtime);
int cnt[] = {0};
runtime = Benchmark.runtime(() -> {
    List<Foo> res = fooRepo.query(in("name", "MON", "SUN"),
            eq("num", 0));
    cnt[0] += res.size();
});
System.out.println("runtime(msec): " + runtime + ", cnt:" + cnt[0]);

Boon은 상대적으로 CQEngine보다 사용이 단순한다. 내부적으로 리플랙션을 사용해 객체 정보를 추출하는듯 하다. Repos의 builder()을 사용해 빌더 객체 생성해서 이를 통해 기본 키와 인덱스를 설정한다. 기본키와 인덱스에 사용하는 이름은 Foo 클래스의 필드명이다. 그리고 마지막으로 build()로 Repo 객체 생성할 때 기본키 자료형과 사용할 클래스의 클래스 정보를 전달한다.

CQEngine과 마찬가지로 백만개 데이터 생성해서 추가하고 쿼리을 생성해서 조회한 각각 실행 시간을 측정했다.

성능 측정

가상 객체(Foo)를 생성하는 시간과 조회 쿼리 실행 시간을 측정하였다. 객체를 백만 개 생성하는 시간과 백만 개 객체에 대해 조회 쿼리 실행시간을 측정했고 추가로 테스트하는 동안 증가한 메모리 사용량도 측정했다.

구분 초기화 시간 쿼리실행시간 메모리사용량
CQEngine 0.345 초 0.039초 17.72MB
Boon 0.131 초 0.113초 12.68MB

CQEngine이 초기화 시간은 크지만 쿼리 실행시간은 작았고 Boon은 초기화 시간은 작지만 쿼리 실행이 CQEngine보다 크다. 메모리 사용량은 CQEngine이 조금 컸다.

결과

무엇을 선택해야 할까?

CQEngine과 Boon은 측정 데이터 을 가지고 쿼리 실행 회수에 대한 식을 세워보았다.

  • 0.345 + 0.039n = 0.131 + 0.113n

이때 n을 계산하면 약 2.9이다. 즉, 3회 쿼리 실행이 넘어가면 CQEngine이 전체 시간이 덜 소모됨을 알 수 있다. 조회가 3회 전까지는 Boon이 더 작다.

한번 데이터를 저장했다가 3회 이상이 조회 쿼리를 실행하면 CQEngine이 유리하다는 의미이고 만약 1~2회 정보만 조회하고 다시 새로 데이터를 리셋한다면 Boon이 유리하다는 의미이다.

여기서 수치는 위의 측정한 결과이므로 본인의 환경에 따라 달라질 수 있다. 그렇기 때문에 단순 참조용을 사용하시기 바란다.

메모리 사용을 보면 더미 객체를 백만개를 생성할 경우 단순 계산상 최소 10MB 정도 필요한데 CQEngine은 메모리를 다소(?) 좀 많이 소모하지만 생각보다는 크지 않고, 한번 가져온 데이터에서 쿼리가 여러 번 수행될 경우 이런 부분은 크게 문제가되지 않는다고 생각한다. 물론 메모리가 중요해서 조금이라더 덜 사용하는게 중요하다면 Boon을 선택하는게 좋다.

CQEngine 기타등등

CQEngine에서 어트리뷰트 객체 생성하는게 코드가 길다. 이를 조금 줄일 수 있는 방법이 attribute()와 nullableAttribute()을 사용하면 된다. 또한 CQEngine은 SQL 구문을 제한적이지만 사용할 수 있다.

static final Attribute<Foo, Integer> FOO_NUM = attribute(it->it.num);
static final Attribute<Foo, String> FOO_NAME = nullableAttribute(it->it.name);
SQLParser<Foo> parser = SQLParser.forPojoWithAttributes(Foo.class, createAttributes(Foo.class));
ResultSet<Foo> results = parser.retrieve(fooRepo, "SELECT * FROM fooRepo WHERE (" +
                "name IN ('MON', 'SUN') " +
                "AND num = 0 " +
                "ORDER BY name DESC");

Troubleshooting

Boon을 실행할 때 ExceptionInInitializerError 예외 발생

  • 원인: Java9+를 사용할 경우 발생한다. boon에서 dependency을 이후 버전으로 업데이트 안되었다. FastStringUtils이 Srping 클래스내 필드을 접근하는데 Java9+ 이후에 String이 변경되면서 발생했다.
  • 해결: Boon 실행 전에 org.boon.faststringutils.disable환경 변수를 true로 설정한다.
System.setProperty("org.boon.faststringutils.disable", "true");

참조

[1] https://github.com/npgall/cqengine

[2] https://github.com/boonproject/boon

[3] https://github.com/boonproject/boon/wiki/Background-information-on-Boon-datarepo

[4] https://stackoverflow.com/questions/64611717/qbit-boon-reflect-classcastexception-b-incompatible-with-c

반응형