Overview

브랜디 AOS 파트에서는 MVVM + 캐싱 지원을 목표로 작업을 하고 있습니다. 방향성은 정해져 있지만 해당 기술 적용을 위한 변경 작업, 업무 시간 등을 고려했을 때 빠른 시간 내에 적용하고 테스트하기에는 무리가 있습니다. 이러한 문제를 피하고 안정적인 서비스를 유지하면서 적용할 수 있도록 많은 검증 단계와 테스트를 거치고 있습니다. 캐싱을 지원하기 위한 기반을 다지는 시간이라 생각하시면 될 것 같습니다.

Room이란

Room은 SQLite에 대한 추상화 레이어를 제공하여 원활한 데이터베이스 액세스를 지원하는 동시에 SQLite를 완벽히 활용합니다.

상당한 양의 구조화된 데이터를 처리하는 앱은, 데이터를 로컬로 유지하여 대단한 이점을 얻을 수 있습니다. 가장 일반적인 사용 사례는 관련 데이터를 캐싱하는 것입니다. 이런 방식으로 기기가 네트워크에 액세스할 수 없을 때 오프라인 상태인 동안에도 사용자가 여전히 콘텐츠를 탐색할 수 있습니다. 나중에 기기가 다시 온라인 상태가 되면 사용자가 시작한 콘텐츠 변경사항이 서버에 동기화됩니다.

Room은 이러한 문제를 자동으로 처리하므로 SQLite 대신 Room 을 사용할 것을 적극적으로 권장합니다.

Room 의 구성 요소

Entity, Database, Dao 이렇게 3가지로 구성되어 있습니다.

  • Entity
    • Database의 테이블을 나타냅니다.
  • Dao
    • 데이터베이스에 액세스하는 데 사용되는 메서드가 포함되어 있습니다.
  • Database
    • 데이터베이스 홀더를 포함하며 앱의 지속적인 관계형 데이터의 기본 연결을 위한 기본 액세스 포인트 역할을 합니다.
    • RoomDatabase를 상속받는 추상 클래스여야 합니다.
    • 주석 내에 데이터베이스와 연결된 항목의 목록을 포함해야 합니다.
    • 인수가 0개이며 @Dao로 주석이 지정된 클래스를 반환하는 추상 메서드를 포함해야 합니다.

room


Entity

  • @Entity(tableName = “table_name”)
    • Table 정의 어노테이션
    • 기본적으로 Room은 클래스 이름을 Dabatabse 테이블 이름으로 사용합니다.
    • 다르게 지정하기 위해서는 tableName 을 선언해야 합니다.
    • SQLite의 테이블 이름은 대소문자를 구분하지 않습니다.
  • @PrimaryKey
    • 각각의 Entity에서는 최소 한 개의 필드에 PrimaryKey 어노테이션을 선언해야 한다.
  • @ColumnInfo
    • 테이블을 구성하는 필드
  • @Ignore
    • 유효하지 않는 필드일 경우 무시할 수 있습니다.
      @Entity(tableName = "users")
      class User {
      		@PrimaryKey(autoGenerate = true)  val id: Int,
          @ColumnInfo(name = "first_name") val firstName: String?,
          @ColumnInfo(name = "last_name") val lastName: String?,
      		@Ignore val picture: Bitmap?
      }
    


Dao

  • @Dao
    • Database를 접근하는 interface 선언 어노테이션
  • @Query(“SELECT * from tableName ORDER BY id ASC”)
    • 기본적으로 읽기, 쓰기 작업을 실행할 수 있습니다.
  • @Insert(onConflict = OnConflictStrategy.REPLACE)
    • 단일 트랜잭션으로 데이터베이스에 삽입하는 구현을 생성합니다.
    • onConflict 중복 데이터 추가 시 REPLACE, ROLLBACK, ABORT, FAIL, IGNORE 정의할 수 있습니다.
  • @Update
    • 주어진 매개 변수로 부터 Database의 Entity를 수정할 수 있습니다.
  • @Delete
    • 주어진 매개 변수로부터 Database의 Entity를 삭제할 수 있습니다.
@Dao
interface UserDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertUsers(vararg users: User)

    @Update
    suspend fun updateUsers(vararg users: User)

    @Delete
    suspend fun deleteUsers(vararg users: User)

    @Query("SELECT * FROM user")
    suspend fun loadAllUsers(): Array<User>

    @Query("SELECT * FROM user")
    fun lvLoadAllUsers(): LiveData<Array<User>>

}


DataBase

  • @Database(entities = [EntityClass::class], version = 1, exportSchema = false)
    • RoomDatabase 상속받는 추상 클래스 정의
    • entities에는 정의된 Entity Data class 여러 개를 등록할 수 있습니다.
    • version 관리가 가능합니다.
@Database(entities = [User::class], version = 1, exportSchema = false)
abstract class UserDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao

    companion object {
        @Volatile
        var INSTANCE: UserDatabase? = null

        fun getDatabase(context: Context): UserDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context,
                    UserDatabase::class.java, "database-name"
                ).build()

                INSTANCE = instance

                instance
            }
        }

    }
}


Koin + ViewModel + Repository 적용하기

기본적인 구성요소를 확인했으니 마지막으로 MVVM에서 Room을 사용하여 DI 적용하고 Ropository Layer로 나누는 코드로 확인해 보겠습니다.

MyApplication.kt

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        // start Koin
        startKoin() {
            // Android context
            androidContext(this@MyApplication)
            // modules
            modules(myModule)
        }
    }
}

val myModule = module {
    single { MyViewModel(get()) }
    single { UserDatabase.getDatabase(get())}
		single { UserRepository(get()) }
}

UserRepository.kt

class UserRepository(userDatabase: UserDatabase) {

    private val userDao: UserDao

    val lvUserList: LiveData<Array<User>>

    init {
        userDao = userDatabase.userDao()
        lvUserList = userDao.lvLoadAllUsers()
    }

    suspend fun insertUsers(users: User) {
        userDao.insertUsers(users)
    }
}

MyViewModel.kt

class MyViewModel(val repository: UserRepository) : ViewModel() {

    val lvUser: LiveData<Array<User>> =  repository.lvUserList


    fun insertUser(user: User) {
        viewModelScope.launch {
            repository.insertUsers(user)
        }
    }
}


Conclusion

기존에 사용하던 ORM에서 나아가 Room 기반으로 변경을 준비하고 있습니다. 기본적으로 Room의 Dao 메서드는 LiveData와 코루틴을 지원하기 때문에 비동기에 대한 처리와 관리가 간편합니다. 지금 가장 가까운 목표는 로컬 데이터를 Room으로 마이그레이션을 하는 것이 될 것 같습니다. 이러한 결정은 언제나 브랜디에서 안정적인 서비스를 제공하는 것이 가장 큰 목적이기 때문에 다양한 상황을 고려한 후 반영될 것입니다. 겉에서 보이지는 않지만 항상 내부적인 질적 향상을 위해 오늘도 열심히 개발하는 MA팀이 되도록 하겠습니다. 감사합니다.

참고사이트:
https://developer.android.com/training/data-storage/room?hl=ko
https://codelabs.developers.google.com/codelabs/android-room-with-a-view/#0


고재성 | MA팀
gojs@brandi.co.kr
브랜디, 오직 예쁜 옷만