평범한 개발자 행복한 가족, 패밀리그램

개발/안드로이드

Android MVVM 패턴을 위한 Architecture Components library - ROOM #1

패밀리그램 2017. 11. 19. 22:00

구글에서 안드로이드개발을 위한 아키텍처 컴포넌트 라이브러리 1.0 이 나왔다. 5월 말 쯤에 나왔는데 다른 것들에 관심을 갖다 연말이되어서야 확인하고 있다. ( 너무 알아야 할 것이 많아지는 것 같아 슬프다. )


간단하게 아키텍처 컴포넌트는 기존 안드로이드 개발에서  MVVM 패턴을 구현할 수 있도록 도와주는 라이브러리로 판단된다. 지금 까지 RxJava를 이용하여 Lifecycle을 Observe 하고 이를 이용해 MVVM 패턴을 구현했는데, 구글 안드로이드에서 직접 나서서 아키텍처 콤포넌트 라이브라리를 만들었다. 


For Room RxJava support, add:

  • implementation "android.arch.persistence.room:rxjava2:1.0.0"

아직 사용은 안해봤지만, RxJava 프로젝트를 배려해주는 마음이 듬뿍 담긴 Dependency이다.


프로젝트에 적용해 볼 생각이 있는 개발자라면 아래 링크를 확인해 보면 될 것 같다.


https://developer.android.com/topic/libraries/architecture/adding-components.html


https://developer.android.com/training/data-storage/room/defining-data.html


https://developer.android.com/training/data-storage/room/accessing-data.html





우선 아키텍처 콤포넌트 소개 영상 부터


Architecture Components에서 제공하는 Components는 4가지이다. 해당 라이브러리를 공부하기 위해 특정 개인 프로젝트를 아키텍처 콤포넌트를 적용하여 MVVM 패턴을 적용하는 대 수술을 진행해 보려고한다. 우선 기본적인 내용은 알아야 진행이 가능하니 한번 공부해보자.


ROOM에 대해 알아보자 - Spring JPA에서 봤던 듯한 물건이 Android에 똭

ROOM 은 Android SQL Mapping 라이브러리라고 소개하고있다. 기존에 Database의 데이터를 Object로 만들기 위해서 특정 Java class를 만들어 사용하였는데 ROOM을 이용하면 손쉽게 데이터의 Class와 Database access interface를 만들어 사용 할 수 있다. 해당 프로젝트가 Realm을 사용하고 있는데, 스터디를 위해 과감하게 MySQL로 마이그레이션도 진행해 볼 계획이다



기존 Java class를 POJO 라고 설명 하고 있다. - Plan Old Java Object

1
2
3
4
5
6
7
8
9
10
11
public class Product{
 
    public String name;
    public String barcode;
    public long price;
    public String storeName;
    public String imageUrl;
    public long checkInDate;
    public long checkOutDate;
..
}

cs


위 코드가 전통적인 자바의 데이터 클래스이다. 해당 코드를 ROOM에서 제공하는 Annotation을 달아 클래스의 특성과 속성들을 명시해준다. 

기존 Java class에 ROOM의 특성을 넣었으니 RPOJO 라고 해야하는가. - Room Plan Old Java Object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Entity
public class ProductEntity implements Product{
 
    @NonNull
    private @PrimaryKey String id;
    private String name;
    private long price;
    private String storeName;
    private String imageUrl;
    private long checkInDate;
    private long checkOutDate;
...
}
 
cs

Realm과 다르게 PrimaryKey Annotation이 데이터 타입과 접근제어자 사이에 붙어 있다. 여기까지만 보면 DB 마이그레이션은 엄청 쉬울 것 같다는 생각이 머리속에서 춤추고있다.
ROOM을 사용하는 Product와 Firebase DB를 사용하는 Product 2가지가 있기 때문에 확장성을 위해 Product interface를 정의하여 Product Entity Room 객체를 구현하였다.


DB 테이블의 기본 데이터 Class를 정의했으면 이제  DAO 를 만들어 보자 - Database Access Object

예전에 Spring boot Java 프로젝트를 할 때 JPA를 사용했었는데, DAO를 보니 JPA에 눈에 아른거린다 ( JPA와 해당 DAO는 다른다 )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Dao
public interface ProductDao{
 
    @Insert(onConflict = IGNORE)
    void insert(ProductEntity data);
 
    @Insert(onConflict = IGNORE)
    void insertAll(List<ProductEntity> data);
 
 
    @Query("SELECT * From ProductEntity")
    List<ProductEntity> findAll();
 
    @Update(onConflict = REPLACE)
    void update(ProductEntity data);
 
    @Query("DELETE FROM ProductEntity")
    void deleteAll();
}
cs

Spring의 JPA는 Repository 를 상속받아 구현하여야 하지만 Room의 Dao는 단순하게 Annotation하나면 해결된다. 그리고 Query 또한 SQLite에서 사용하는 Query와 흡사하기 때문에 이해하기가 쉽다.

Insert와 Update 할경우에는 예외에 대한 정의를 Annotation parameter로 정의해 주는데 크게 이해하기 어렵지는 않다.



ROOM Database - Dao를 정의했다고 하여 바로 사용 할 수 있는 것은 아니다.

Room Dao는 따로 사용되는 것은 아닌걸로 샘플코드에서 확인했다. RoomDatabase Class를 상속받는 Application의 Database 를 정의하여, 해당 DB 객체를 통해서 사용되어야 한다. ( Android SQLite DB create, update 등 version 관련 이유인 것 같다 ) 


아래 코드는 Sample 코드의 Database class의 이름만 변경하여 프로젝트에 적용한 Class이다. 관심있게 본 부분은 Database 내에서 사용하는 Executor 를 정의하여 Network I/O와 Disk I/O 그리고 Main Thread ( UI 업데이트 ) 를 Executor별로 따로 나눠 실행하는 부분이었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
@Database(entities = {ProductEntity.class}, version = 1)
@TypeConverters(DateConverter.class)
public abstract class MemoDatabase extends RoomDatabase{
 
    private static final String TAG = "MemoDatabase";
 
    private static MemoDatabase sInstance;
 
    @VisibleForTesting
    public static final String DATABASE_NAME = "market-memo-db";
 
    public abstract ProductDao productDao();
 
    private final MutableLiveData<Boolean> mIsDatabaseCreated = new MutableLiveData<>();
 
 
    public static MemoDatabase getInstance(final Context context){
        return getInstance(context, new AppExecutors());
    }
 
    public static MemoDatabase getInstance(final Context context, final AppExecutors executors) {
        if (sInstance == null) {
            synchronized (MemoDatabase.class) {
                if (sInstance == null) {
                    sInstance = buildDatabase(context.getApplicationContext(), executors);
                    sInstance.updateDatabaseCreated(context.getApplicationContext());
                }
            }
        }
        return sInstance;
    }
 
    /**
     * Build the database. {@link Builder#build()} only sets up the database configuration and
     * creates a new instance of the database.
     * The SQLite database is only created when it's accessed for the first time.
     */
    private static MemoDatabase buildDatabase(final Context appContext,
                                             final AppExecutors executors) {
        return Room.databaseBuilder(appContext, MemoDatabase.class, DATABASE_NAME)
                .addCallback(new Callback() {
                    @Override
                    public void onCreate(@NonNull SupportSQLiteDatabase db) {
                        super.onCreate(db);
                        executors.diskIO().execute(() -> {
                            // Add a delay to simulate a long-running operation
                            addDelay();
                            // Generate the data for pre-population
                            MemoDatabase database = MemoDatabase.getInstance(appContext, executors);
//                            insertData(database, products, comments);
                            // notify that the database was created and it's ready to be used
                            database.setDatabaseCreated();
                        });
                    }
                }).build();
    }
 
    /**
     * Check whether the database already exists and expose it via {@link #getDatabaseCreated()}
     */
    private void updateDatabaseCreated(final Context context) {
        if (context.getDatabasePath(DATABASE_NAME).exists()) {
            setDatabaseCreated();
        }
    }
 
    private void setDatabaseCreated(){
        mIsDatabaseCreated.postValue(true);
    }
 
    private static void insertData(final MemoDatabase database, final List<ProductEntity> products) {
        database.runInTransaction(() -> {
            database.productDao().insertAll(products);
        });
    }
 
    private static void addDelay() {
        try {
            Thread.sleep(4000);
        } catch (InterruptedException ignored) {
        }
    }
 
    public LiveData<Boolean> getDatabaseCreated() {
        return mIsDatabaseCreated;
    }
 
}
cs

코드 중간에  LiveData라는 Class가 보이는데 이 부분은 다음 포스팅에 정리해볼 계획이다. 단순하게 생각하면 LiveData에 Observable을 붙여 Data가 변경되었을 때 관찰중인 곳으로 업데이트 되었다고 알려주는 Data인것 같았다.

Android Test

아직 프로젝트에서 사용하는 Realm DB를 교체하진 못 하였고, 적용된 Room DB를 Test하는 코드를 동작시켜 보았다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@RunWith(AndroidJUnit4.class)
public class ProductDaoTest {
 
    private static final String TAG = "ProductDaoTest";
 
    Context context;
    ProductDao mProductDao;
 
 
    @Before
    public void createDb() {
        context = InstrumentationRegistry.getTargetContext();
        mProductDao = MemoDatabase.getInstance(context).productDao();
    }
 
 
    @After
    public void closeDb(){
        MemoDatabase.getInstance(context).close();
    }
 
    @Test
    public void insertAndRead(){
        ProductEntity product = new ProductEntity();
        product.setId(UUID.randomUUID().toString());
        product.setCheckInDate(0);
        product.setCheckOutDate(0);
        product.setName("Apple");
        product.setStoreName("DrugStore");
        Log.d(TAG, "INSERT PRODUCT : " + product);
        mProductDao.insert(product);
        Log.d(TAG, "INSERT DONE");
        Log.d(TAG, "FIND ALL PRODUCT");
        List<ProductEntity> list = mProductDao.findAll();
        Log.d(TAG, "ALL PRODUCT" + list);
        assertNotNull(list);
    }
}
cs


정상적으로 동작하여 Success 되는 것을 확인하였다. 프로젝트 더 깔끔할 수있다는 기대감에 빨리 Android 아키텍처 컴포넌트를 적용해보고 싶다.

기존 프로젝트는 Realm DB Change이벤트를 받아 각 UI이 Rx Observable을 붙여 다른 곳에서 DB가 변경될 때 마다 UI를 업데이트 했었는데, 다음은 LiveData를 이용하여  대해 이 부분을 수정해봐야겠다.

반응형