Protocol Buffer DB 저장 / 로드하기

읽기 전

  • 불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다.
  • 개인적으로 사용해보면서 배운 점을 정리한 글입니다.
  • 전체 프로젝트는 깃헙에 올려두었습니다. 우측 링크 확인해주세요. MyProtobuf 프로젝트 링크

protobuf 구조 SQLite DB에 저장 후 로드

protobuf를 채택하여 프로젝트 제작 시 입맛에 맞게 조합한 구조를 다루다 보면 DB에 저장, 관리하고 싶을 수 있다. 물론 protobuf는 별도의 커스터마이징된 자료형이지만 저번 포스팅에서 intent에 protobuf 객체를 넣고 전달했을 떄와 같이 String으로 잘 변환하여 Text 타입을 DB에 저장하면 된다. 관련 용어로 말하자면 Serialize(직렬화), Deserialize(역직렬화)라고 표현한다.

Sample SQLite DB 생성하기

Android에서 DB를 사용하는 방법은 여러 가지가 있지만 이번 포스팅에서는 SQLite DB를 외부에서 생성한 뒤 assets 폴더에 넣어 앱에서 접근하는 방식을 채택하였다. 우선 myporobuf.db라는 db file을 생성 후 stored_proto 테이블을 만들고 idx, proto_string 컬럼을 갖게끔 설정하였다.

app\src\main\assets\databases 경로에 생성한 db파일을 저장한 뒤 이후 Helper클래스에 해당 파일을 인식하게 만들어준다. 경로를 잘 지켜줘야 SQLAssetHeleper 클래스가 인식한다.

SQLite DB 접근하는 class 생성하기

가장 기본적인 SQLiteOpenHelper보다는 assets 폴더의 DB 파일을 사용하기로 결정했으므로 SQLiteAssetHelper 모듈을 사용하였다. (해당 모듈의 사용법은 추후에 기회가 닿는다면 정리하겠습니다.) 우선 build.gradle(Module)에 SQLAssetHelper 모듈을 더하는 코드 한줄을 추가한다.

build.gradle(Moduel)

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
    implementation 'com.readystatesoftware.sqliteasset:sqliteassethelper:2.0.1' // add this line
}

GlobalDBHelper.class

assets 폴더 내의 db 파일 접근, 레코드 읽기, 삭제하는 메소드들을 정의하였다.

public class GlobalDBHelper extends SQLiteAssetHelper {
    private static final String DB_NAME = "myprotobuf.db";
    private static final String PROTO_TABLE = "stored_proto";
    private static final String[] PROTO_SELECT = {"proto_string"};
    private static final int DB_VERSION = 1;
    private static GlobalDBHelper mInstance = null;

    public GlobalDBHelper(Context context) {
        super(context, DB_NAME, null, DB_VERSION);
        setForcedUpgrade();
    }

    public static GlobalDBHelper getInstance(Context context) {
        if (mInstance == null) {
            mInstance = new GlobalDBHelper(context.getApplicationContext());
        }
        return mInstance;
    }

    private Cursor getCursor(String table, String[] sqlProjectionIn, String sqlSelect, String[] whereArgs) {
        SQLiteDatabase db = getReadableDatabase();
        SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
        queryBuilder.setTables(table);
        Cursor mCursor = queryBuilder.query(db, sqlProjectionIn, sqlSelect, whereArgs, null, null, "idx desc");
        return mCursor;
    }

    public Cursor getProto() {
        return getCursor(PROTO_TABLE, PROTO_SELECT, null, null);
    }

    public void insertProto(ContentValues contentValues) {
        SQLiteDatabase db = getWritableDatabase();
        db.insert(PROTO_TABLE, null, contentValues);
        db.close();
    }

    public void cleanProtoTable() {
        SQLiteDatabase db = getWritableDatabase();
        db.delete(PROTO_TABLE, null, null);
        db.close();
    }
}

전달받은 protobuf 객체 저장 후 다시 로드하기

Intent로 전달했던 지난 포스팅처럼 DB에 저장할 때도 주의해야 한다. 제대로 변환하지 않고 단순히 toString()으로 변환하면 자체적으로 encoding 과정을 거치면서 Bit Corruption이 발생한다. 따라서 저장 후 다시 로드하여 encoding을 하면 정상적으로 protobuf 객체로의 변환이 이뤄지지 않는다. Intent로 전달했을 때와 같이 base64로 변환하는 방법과 string으로 변환하는 2가지 방법을 사용하여 확인해봤다.

protobuf - Base64 - protobuf

Android_Protocol_Buffer_003_02

protobuf - String(object) - protobuf

Android_Protocol_Buffer_003_03

protobuf - object.toString() - protobuf

Android_Protocol_Buffer_003_04

세 가지 방법을 시도한 결과 toString()을 제외한 두 가지는 제대로 DB에서 문자열 조회 후 protobuf로 변환됨을 확인할 수 있다. 이렇게 앱에서 수집한 정보들을 담은 protobuf 객체를 DB에 저장함으로써 좀 더 복합적인 정보 관리가 가능하다. 2번째와 3번째 방법에서의 차이가 발생하는 이유로 추측되는 건 해당 코드를 추적한 결과 서로 반환하는 값이 달랐기 때문이다. new String(byte[])는 byte[] 안의 값을 String으로 새롭게 반환하였다면 byte[].toString()은 클래스이름 + @ + 해시코드를 반환하는 것으로 보아 참조값을 보여주는 듯하다. (그냥 얌전히 BASE64 인코딩했으면 이런 고민 할 필요도 없었겠지만.. 덕분에 추석동안 이거 하나 매달렸습니다 ㅠ)

전체 코드

GlobalDBHelper.class

package com.example.myprotobuf;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;

import com.readystatesoftware.sqliteasset.SQLiteAssetHelper;

public class GlobalDBHelper extends SQLiteAssetHelper {
    private static final String DB_NAME = "myprotobuf.db";
    private static final String PROTO_TABLE = "stored_proto";
    private static final String[] PROTO_SELECT = {"proto_string"};
    private static final int DB_VERSION = 1;
    private static GlobalDBHelper mInstance = null;

    public GlobalDBHelper(Context context) {
        super(context, DB_NAME, null, DB_VERSION);
        setForcedUpgrade();
    }

    public static GlobalDBHelper getInstance(Context context) {
        if (mInstance == null) {
            mInstance = new GlobalDBHelper(context.getApplicationContext());
        }
        return mInstance;
    }

    private Cursor getCursor(String table, String[] sqlProjectionIn, String sqlSelect, String[] whereArgs) {
        SQLiteDatabase db = getReadableDatabase();
        SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
        queryBuilder.setTables(table);
        Cursor mCursor = queryBuilder.query(db, sqlProjectionIn, sqlSelect, whereArgs, null, null, "idx desc");
        return mCursor;
    }

    public Cursor getProto() {
        return getCursor(PROTO_TABLE, PROTO_SELECT, null, null);
    }

    public void insertProto(ContentValues contentValues) {
        SQLiteDatabase db = getWritableDatabase();
        db.insert(PROTO_TABLE, null, contentValues);
        db.close();
    }

    public void cleanProtoTable() {
        SQLiteDatabase db = getWritableDatabase();
        db.delete(PROTO_TABLE, null, null);
        db.close();
    }
}

SecondActivity

package com.example.myprotobuf;

import androidx.appcompat.app.AppCompatActivity;

import android.content.ContentValues;
import android.database.Cursor;
import android.os.Bundle;
import android.util.Base64;
import android.util.Log;

import com.example.myprotobuf.UserInfo.Person;
import com.google.protobuf.InvalidProtocolBufferException;

public class SecondActivity extends AppCompatActivity {
    private Person mPerson, mLoadedPerson;
    private String mConvertedProto, mLoadedProto;
    private GlobalDBHelper mDBHelper;
    private static final String COL_PROTO_STRING = "proto_string";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        try {
            mPerson = Person.parseFrom(Base64.decode(getIntent().getStringExtra("person"), Base64.DEFAULT));
        } catch (Exception e) {
            e.printStackTrace();
        }
        Log.e("before converting", mPerson.toString());
        mDBHelper = GlobalDBHelper.getInstance(getApplicationContext());
        mDBHelper.cleanProtoTable();

        mConvertedProto = Base64.encodeToString(mPerson.toByteArray(), Base64.DEFAULT);
        Log.e("before DB save", mConvertedProto);
        mLoadedProto = processSaveLoadProto(mConvertedProto);
        try {
            mLoadedPerson = Person.parseFrom(Base64.decode(mLoadedProto, Base64.DEFAULT));
            Log.e("converted", mLoadedPerson.toString());
        } catch (InvalidProtocolBufferException e) {
            e.printStackTrace();
            Log.e("error string", "got error by Base64");
        }

        mConvertedProto = new String(mPerson.toByteArray());
        Log.e("before DB save", mConvertedProto);
        mLoadedProto = processSaveLoadProto(mConvertedProto);
        try {
            mLoadedPerson = Person.parseFrom(mLoadedProto.getBytes());
            Log.e("converted", mLoadedPerson.toString());
        } catch (InvalidProtocolBufferException e) {
            e.printStackTrace();
            Log.e("error string", "got error by newString");
        }

        mConvertedProto = mPerson.toByteArray().toString();
        Log.e("before DB save", mConvertedProto);
        mLoadedProto = processSaveLoadProto(mConvertedProto);
        try {
            mLoadedPerson = Person.parseFrom(mLoadedProto.getBytes());
            Log.e("converted", mLoadedPerson.toString());
        } catch (InvalidProtocolBufferException e) {
            e.printStackTrace();
            Log.e("error string", "got error by toString");
        }
    }

    private String processSaveLoadProto(String convertedString) {
        mDBHelper.cleanProtoTable(); // 1개만 저장한다고 하자.
        ContentValues contentValues = new ContentValues();
        contentValues.put(COL_PROTO_STRING, convertedString);
        mDBHelper.insertProto(contentValues);
        Cursor protoCursor = mDBHelper.getProto();
        protoCursor.moveToFirst();
        String loadedProtoString = protoCursor.getString(protoCursor.getColumnIndex(COL_PROTO_STRING));
        Log.e("Load", loadedProtoString);
        return loadedProtoString;
    }
}

참고자료

+ Recent posts