티스토리 뷰

1. 안드로이드 Room 으로 로컬  데이터베이스에 데이터 저장하기

Android 개발자 라이브러리 Room은 SQLite를 추상화 하여 제공하기 때문에 보다 쉬운 데이터 베이스 사용이 가능합니다. 또 물론 SQLite에서 지원하는 것을 완벽하게 활용할 수 있다고하네요.

그래서 이번에 계산기 예제를 만들면서 계산 히스토리를 저장할때 Room을 사용해서 로컬 데이터베이스에 기록을 해주는 방식으로 구현을 해보았답니다.

1.1. Room 사용에 좋은 예시

공식 문서에 있는 바를 간단하게 요약하자면, 유저가 앱을 실행하는 환경이 오프라인이었을 경우에는 이전에 온라인 상태에 있을 때 컨텐츠를 Room을 통해 저장해둔 컨텐츠를 불러와서 보여주며 유저와 상호작용을 하다가 이후에 다시 온라인 상태가되면 유저가 동작한 내용을 서버와 동기화하도록 하여 서버에 반영 되도록 설계하면 이득이라고 합니다.

이걸 Room으로 구현하면 자동으로 처리가 가능하기 때문에 SQLite를 바로 쓰는 대신 Room을 거쳐서 사용할 것을 공식적으로 권장하고 있습니다.

2. Room 사용 하기

그럼 바로 안드로이드 스튜디오 에서 Room을 사용해보도록 하겠습니다. 코드는 코틀린 코드 기준으로 작성하였습니다. Java 코드는 공식 문서를 참고하시면 되겠습니다.

2.1. Gradle 파일 수정

먼저 Room을 사용하기 위해서 build.gradle 파일에 아래 항목을 추가해야합니다.

dependencies {
  def room_version = "2.2.6"

  implementation "androidx.room:room-runtime:$room_version"
  kapt "androidx.room:room-compiler:$room_version"

  // optional - Kotlin Extensions and Coroutines support for Room
  implementation "androidx.room:room-ktx:$room_version"

  // optional - Test helpers
  testImplementation "androidx.room:room-testing:$room_version"
}

2.2. Room의 구성요소 3가지

Room은 3가지 구성요소를 가지며 아래와 같습니다.

  • 데이터베이스: 데이터베이스 홀더를 포함하고, 앱의 지속적인 관계형 데이터의 기본 연결을 위한 기본적인 엑세스 포인트 역할을 하게됩니다.
    • @Database로 주석이 지정되게 되며 아래를 만족시켜야합니다.
      • RoomDatabase를 확장하는 추상 클래스로 작성해야합니다.
      • 주석 내에 데이터베이스와 연결된 항목의 목록을 포함해야합니다.
      • 인수가 0개이며 @Dao로 주석이 지정된 클래스를 반환하는 추상 메서드를 포함하고 있어야합니다. (@Dao로 주석된 클래스를 우리가 구현해주어야 된다는 것을 알 수 있습니다.)
    • 항목(Entitiy): 데이터베이스 내의 테이블을 나타내게됩니다.
    • DAO: 데이터베이스에 액세스하는 데 사용되는 메서드가 포함됩니다.

앱이 Room DB를 사용해서 데이터베이스와 연결된 데이터 액세스 개체 또는 DAO를 가져오게됩니다. 그리고 나서는 앱은 각 DAO를 사용해서 데이터베이스에서 Entitiy를 가져오고 Entitiy의 변경사항을 다시 DB에 저장합니다. 마지막으로 앱은 Entitiy를 사용해서 데이터베이스 내에 테이블 열에 해당하는 값을 가져오고 설정하게됩니다.

사실 위 내용이 한번에 와닿지 않을 수 있습니다. 직접 아래에서 코드화해보면 보다 이해하기 쉬울실겁니다.

그림 1. Room 구조 다이어그램 (출처 : developer.android.com)

2.3. Entity 클래스 

Entity 클래스를 정의합니다. 이때 data 클래스로 생성해서 기본적인 기능을 포함하여 인자로 정의할 수 있게 합니다.

@Entity Annotation을 지정해주고 저는 tableName을 따로 지정해주었습니다. @PrimaryKey로 기본키를 지정할 수 있고 이 기본키는 null을 할당해도 내부적으로는 +1씩해서 추가할 때마다 고유값으로 설정되게되겠죠.

@ColumnInfo로는 열값으로 들어갈 이름과 자료형을 설정해주시면되겠습니다.

// 데이터 클래스로 선언
@Entity(tableName = "history_table") // room 사용(room의 데이터 클래스) db 테이블
data class History(
    @PrimaryKey val uid: Int?, // 유니크한 아이디 (기본키로 사용)
    @ColumnInfo(name = "expression") val expression: String?,
    @ColumnInfo(name = "result") val result: String?
)

2.4. Dao 인터페이스

다음은 Dao 인터페이스 구현 코드입니다. 예제로 작성된 코드와 공식문서에 있는 코드 2개를 가져왔으니 참고해서 사용하시면되겠습니다. @Query 어노테이션으로 직접 쿼리문을 작성할 수 있다는 점이 신박했습니다.

@Dao
interface HistoryDao {

    @Query("SELECT * FROM history_table") // 쿼리문 작성
    fun getAll(): List<History>

    @Insert
    fun insertHistory(history: History)

    @Query("DELETE FROM history_table") // 테이블 전체 삭제
    fun deleteAll()

    @Delete // 해당 히스트로리만 제거
    fun delete(history: History)

    // 조건 가진 결과만 가져오기 (1개만)
    @Query("SELECT * FROM history_table WHERE result LIKE :result LIMIT 1")
    fun findByResult(result: String): History
}
    @Dao
    interface UserDao {
        @Query("SELECT * FROM user")
        fun getAll(): List<User>

        @Query("SELECT * FROM user WHERE uid IN (:userIds)")
        fun loadAllByIds(userIds: IntArray): List<User>

        @Query("SELECT * FROM user WHERE first_name LIKE :first AND " +
               "last_name LIKE :last LIMIT 1")
        fun findByName(first: String, last: String): User

        @Insert
        fun insertAll(vararg users: User)

        @Delete
        fun delete(user: User)
    }
    

2.5. AppDatabase 추상 클래스 생성

다음으로 database 추상 클래스를 정의하는 코드입니다. @Database 어노테이션으로 어떠한 entity를 연결하여 사용할 것인지와 @Dao로 설정된 클래스를 반환하는 추상 메서드를 정의해주어야합니다.

이때 version 코드를 설정하는 것을 알수있는데 이는 우리가 이후 버전에서 데이터 베이스(정의 등)을 수정하게 된다면, 해당 버전 코드를 업데이트하고 마이그레이션 하는 등의 작업을 해줄 수 있게됩니다. 한 번 정의한 데이터베이스가 완벽할 수는 없으니 이후 수정에 있어서 이러한 기능은 매우 유용할 것 같네요.

// 데이터 베이스가 만들어질때 히스토리 테이블을 사용하겠다고 등록
// 그리고 버전을 작성해주어야함. 앱 업데이트 할 경우 디비가 바뀔 수 있는데 변경이 되었을 때
// 최신 버전 디비로 마이그레이션을 해주어셔 데이터가 날라가지 않게.
@Database(entities = [History::class], version = 1)
abstract class AppDatabase : RoomDatabase() { // 추상 클래스
    abstract fun historyDao(): HistoryDao // AppDatabase 생성시 HistoryDao를 가져가서 사용할 수 있게
}

3. Room Database 사용하기

이제 아래 코드로 부터 생성한 데이터베이스 인스턴스를 가져와서 실제로 사용하는 예시를 확인할 수 있습니다. 먼저 onCreate 부분에서 Room의 databaseBuilder를 사용해서 데이터 베이스를 빌드하게되며 (이때 우리가 작성한 AppDatabase 클래스가 사용되는 것을 확인할 수 있습니다.)

그리고 전역으로 선언된 db를 실 사용하는 부분을 보면 각 함수에서 Thread를 통해서 사용하게 하였고 historyDao를 통해 우리가 정의했던 삽입, 삭제 등의 기능을 사용하는 것을 볼 수 있습니다.

 이때 메인 스레드가 아닌 Thread를 추가해서 사용한 이유는 DB 또는 네으트워크 등의 작업은 비용이 크게 발생할 수 있고 메인스레드에서 직접 사용할 경우 성능면에서 떨어질 수 있기 때문에 직접 UI 자원을 업데이트 시키지 않는 이상 별도의 Thread에서 작업할 수 있도록 설계하였습니다.

완성본 계산기 어플리케이션 코드 보러가기 (깃허브 저장소)

    lateinit var db: AppDatabase // 전역

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        // onCrate 시 db 변수에 앱데이터베이스 빌드해서 할당
        db = Room.databaseBuilder(
            applicationContext,
            AppDatabase::class.java,
            "historyDB"
        ).build()
    }
    
    fun resultButtonClicked(v: View) {
        ...

        // 디비에 넣어주는 부분
        // DB 입출력 과정은 메인스레드 외 추가 스레드에서 해야함
        // Thread 에는 Runnable 구현체가 들어감
        Thread(Runnable {
            // uid: null 로주어도 기본키라 자동으로 +1되서 들어감
            db.historyDao().insertHistory(History(null, expressionText, resultText))
        }).start()

        ...
    }
    
        fun historyButtonClicked(v: View) {

        ...
        
        Thread(Runnable {
            db.historyDao().getAll().reversed().forEach {
                // 뷰 생성하여 넣어주기
                // 레이아웃 인플레이터 기능 사용 해보기
                // ui 스레드 열기
                runOnUiThread {
                    // 핸들러에 포스팅될 내용 작성

                    // R.layout.history_row 에서 인플레이트를 시킴., root랑 attachToRoot. 나중에 addview를 통해 붙일거라 null, false
                    val historyView =
                        LayoutInflater.from(this).inflate(R.layout.history_row, null, false)
                    historyView.findViewById<TextView>(R.id.expressionTextView).text = it.expression
                    historyView.findViewById<TextView>(R.id.resultTextView).text = "= ${it.result}"

                    historyLinearLayout.addView(historyView) // 뷰 추가
                }
            } // 리스트 뒤집어서 가져오기
        }).start()

    }

댓글
최근에 올라온 글
최근에 달린 댓글
네이버 이웃추가
«   2024/04   »
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
글 보관함