room으로 데이터를 다뤄보자

두리두두·2024년 5월 15일

Android

목록 보기
16/25

  • 난 그저 intent를 사용한 화면 이동을 실습해보고 싶었을 뿐인데.. 그래서 구글링하며 예제를 찾았을 뿐인데.. 그 예제에서 ViewModel, room 등의 심화 기능을 쓰셔서 얼떨결에 같이 공부를 하고 있다.
  • room을 통해 DB에 접근하는 법을 공부해보자!

🍥 room이란? room의 장점

  • 우선 안드로이드에서 로컬 DB를 사용할 때에는 다음과 같은 3가지 방법이 있다.
    1) File : 파일 생성하여 파일 입출력으로 데이터 관리
    2) SharedPreference : key, value 쌍으로 데이터 관리
    3) SQLite : 응용프로그램에서 사용하는 MySql과 같은 DB 시스템
  • 여기서 3번 SQLite를 더 편하게 쓰게 해주는게 room이다.
  • SQLite의 단점으로는,, 컴파일 시간에 쿼리에서 나는 오류를 잡을 수 없고, SQL쿼리와 데이터 객체와의 변환이 자유롭지 못하다고 한다.(후자는 먼지 잘 모르겠다.)

room의 장점

  • SQL 쿼리의 컴파일 시간 확인
  • 어노테이션으로 반복되는 상용구 줄여서 에러 발생 가능성 감소
  • 간소화된 데이터베이스 이전 경로

🍥 room 구조 및 생성

1) Entities

  • DB 테이블.
  • class의 변수들이 각 컬럼이 되어 테이블 구성
  • @Entity, @PrimaryKey, @ColumnInfo 어노테이션으로 구성
@Entity(tableName="todoTable")
class Todo (
    @ColumnInfo(name="id") @PrimaryKey(autoGenerate = true) var id:Long = 0,
    @ColumnInfo(name="title") val title:String,
    @ColumnInfo(name="content") val content:String,
    @ColumnInfo(name="timestamp") val timestamp:String,
    @ColumnInfo(name="isChecked") var isChecked:Boolean
    ): Serializable {

    }

2) DAO (Data Access Objects)

  • DB에 접근하여 수행할 작업을 메소드 형태로 정의하는 부분
  • 서비스단과 데이터베이스 중간 다리역할이라고 이해하였다.
  • interface 사용 권장!
  • SQL 쿼리(쿼리 메소드)를 사용할 수도 있고, 어노테이션(편의 메소드)으로 사용할 수도 있다.
@Dao
interface TodoDao {
	// 편의 메소드
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insert(dto: Todo)
	
    // 쿼리 메소드
    @Query("select * from todoTable")
    fun list(): LiveData<MutableList<Todo>>

    @Query("select * from todoTable where id = (:id)")
    fun selectOne(id:Long):Todo

    @Update
    suspend fun update(dto:Todo)

    @Delete
    fun delete(dto: Todo)
}

3) Database

  • DB를 보유할 AppDatabase 클래스
  • RoomDatabase()클래스를 확장하는 추상클래스여야 한다.
  • '단일 프로세스로 구동하는 앱일 경우 데이터베이스를 인스턴스화 할때 싱글톤 패턴을 따르는것이 권장됨'이라고 하는데 아직은 이부분은 잘 모르겠음..
@Database(entities = arrayOf(Todo::class), version = 1)
abstract class TodoDatabase: RoomDatabase() {
    abstract fun todoDao() : TodoDao
}

🍥 room 사용하기

  • 위에서 셋팅해놓은 roomDB를 사용하려면 액티비티 등에서 데이터베이스 인스턴스를 만들어야한다.
  • Room.databaseBuilder().build()로 생성
val db = Room.databaseBuilder(
            applicationContext,
            AppDatabase::class.java, "database-name"
        ).build()

repository

  • repository에서 RoomDB를 생성자로 받아서 roomDao에서 작성한 메서드를 구현해보자.

🍡 why repository?

  • 사실 클론코딩한 예제에서 dao와 repository를 나누어서 사용해서 무작정 따라했다가 왜 나눴지?하고 찾아봤다.
  • DAO : Data Access Object, DB에 접근하여 데이터를 조회하고 저장하는 역할. 일반적으로 DB 테이블 당 1:1 DAO 맵핑되어 해당 테이블에 대한 DB 작업을 DAO에서 수행
  • Repository : 서비스 레이어에서 비즈니스 로직을 수행하는 필요한 데이터를 찾아서 반환하는 역할. 비즈니스 로직을 수행하는데 필요한 데이터를 여러개의 DAO에서 찾아올 수도 있고, 꼭 DAO를 사용하지 않고도 값을 셋팅해 보내야할 수도 있다. 서비스로 데이터를 반환할 때는 캡슐화가 되어야하고, 비즈니스 로직을 바로 수행할 수 있는 도메인 객체를 반환해야함.
    (그러나 dao와 repository의 하는 일이 거의 같을 때에는 dao에서 바로 데이터를 보내도 된다.)
  • 위와 같은 이유로 리포지토리를 떼어서 만들어보자.
private const val DATABASE_NAME = "todo-databse.db"
class TodoRepository private constructor(context: Context){
    private val database:TodoDatabase  = Room.databaseBuilder(
        context.applicationContext,
        TodoDatabase::class.java,
        DATABASE_NAME
    ).build()

	// Dao 객체
    private val todoDao = database.todoDao()
	
    // 서비스단에서 사용할 함수(구 메소드 현 함수)
    fun list(): LiveData<MutableList<Todo>> = todoDao.list()

    fun getTodo(id:Long):Todo = todoDao.selectOne(id)

    fun insert(dto: Todo) = todoDao.insert(dto)

    suspend fun update(dto : Todo) = todoDao.update(dto)

    fun delete(dto:Todo) = todoDao.delete(dto)
	
    // 초기화 및 생성
    companion object{
        private var INSTANCE: TodoRepository?=null

        fun initialize(context: Context){
            if (INSTANCE==null){
                INSTANCE = TodoRepository(context)
            }
        }

        fun get():TodoRepository{
            return INSTANCE ?:
            throw IllegalStateException("TodoRepository must be initialized!")
        }
    }
}
  • 이제 서비스단에서 repository 객체를 생성해서 원하는 정보를 쏙쏙 뽑아올 준비가 다 되었다^^

viewmodel

  • 그리고 뽑아온 데이터를 유실 없이 사용하도록 뷰모델에서 다루자!!
  • 결국은 찐 로컬 DB -> DataBase -> DAO -> repository -> ViewModel의 여정을 거쳐서 앱에 보이게 되는 것이다!!
class TodoViewModel:ViewModel() {
    val todoList: LiveData<MutableList<Todo>>
    private val todoRepository:TodoRepository = TodoRepository.get()
    init {
        todoList = todoRepository.list()
    }

    fun getOne(id : Long) = todoRepository.getTodo(id)
    fun insert(dto: Todo) = viewModelScope.launch(Dispatchers.IO){
        todoRepository.insert(dto)
    }
    fun update(dto:Todo) = viewModelScope.launch(Dispatchers.IO){
        todoRepository.update(dto)
    }
    fun delete(dto:Todo)=viewModelScope.launch(Dispatchers.IO){
        todoRepository.delete(dto)
    }
}

mainActivity

  • 뷰모델까지 준비가 되었다. 요 뷰모델의 메소드들을 메인화면에서 어떻게 쓰는지 봐보자.
    1) 데이터 삽입
    - 주석에도 있지만,, 투두 작성 화면에서 받아온 todo객체를 뷰모델에 insert함으로써 viewModel->todoRepository->todoDao->DB저장 까지 가게 된다.
    // registerForActivityResult에서 투두 입력한 결과를 받아오는 부분
    when(it.data?.getIntExtra("flag",-1)){
          0 -> {
               CoroutineScope(Dispatchers.IO).launch {
               // todoViewModel.insert(todo)를 통해 viewModel -> todoRepository -> todoDao 순으로 타고 들어가 데이터베이스에 저장
               todoViewModel.insert(todo)
                }
               Toast.makeText(this, "추가되었습니다.", Toast.LENGTH_SHORT).show()
                    }
                 ...
              }
    2) 데이터 수정
    • 수정될 때는 itemId를 key로 수정할 투두의 Todo객체를 가져와서 EditActivity로 넘긴다.
      // 수정할 투두를 클릭했을 때
      todoAdapter.setItemClickListener(object: TodoAdapter.ItemClickListener{
                override fun onClick(view: View, position:Int, itemId: Long){
                    CoroutineScope(Dispatchers.IO).launch {
                    	// itemId로 뷰모델에서부터 투두를 가져온다
                        val todo = todoViewModel.getOne(itemId)
                        val intent = Intent(this@MainActivity, EditTodoActivity::class.java).apply{
                            putExtra("type", "EDIT")
                            putExtra("item", todo )
                        }
                        requestActivity.launch(intent)
                    }
                }
            })
    • EditActivity에서 수정을 하고 다시 todo 객체로 만들어서 넘기면,
      registerForActivityResult에서 업데이트 쳐준다.
      // registerForActivityResult
      ...
      1 -> {
            CoroutineScope(Dispatchers.IO).launch {
            todoViewModel.update(todo)
          }
           Toast.makeText(this, "수정되었습니다.", Toast.LENGTH_SHORT).show()
          }

  • 이상으로 room을 사용한 로컬DB에서의 CRUD를 공부해보았다.
  • 무작정 따라한 예제라서 이해가 안됐었는데 하나하나 알아보니 이해가 되어 기뿌당~~
  • 그럼 20000.

(추가) 클린 아키텍쳐란..

  • 알고보니 위에 해봤던 구조가 구글이 권장하는.. UI와 데이터가 분리된 아키텍쳐였다
  • 생각없이 Activity나 Fragment에 코드를 다 때려박지 말자!
  • 진짜 20000.

(참고한 갓 블로그들)

https://jminie.tistory.com/171#2.%20%F0%9F%93%8C%C2%A0%20Room
https://gogumac.github.io/android/room/android-jetpack-room/
https://makemepositive.tistory.com/33
https://pekahblog.tistory.com/169

(추가로 공부해야할 것들)

  • 코루틴
  • suspend 키워드
  • companion object 키워드
profile
야금야금 앱 개발자

0개의 댓글