Idealim
article thumbnail
Published 2021. 8. 10. 13:20
[Android] Room Android/Android with Kotlin

/* 본 게시물은 '참고 자료' 의 내용을 토대로 작성되었습니다. */

참고 자료

[안드로이드 부트캠프 Room 사용법] : https://www.youtube.com/watch?v=_A2FDHPQX0E 

[안드로이드 Room의 사용법과 예제] : https://todaycode.tistory.com/39


#Room

1. Room이란?

Room은 스마트폰 내장 DB에 데이터를 저장하기 위해 사용하는 라이브러리이다. SQLite를 통해 스마트폰 내장 DB에 접근할 수 있지만 아래와 같은 단점 또한 존재한다. 

https://developer.android.com/training/data-storage/sqlite?hl=ko

우리가 직접 sql문을 써서 관리하기 때문에 오타로 인한 오류 가능성이 높다. 이러한 단점을 해결하기 위해 Room을 사용한다. Room은 기존 SQLite를 활용해서 객체를 매핑하는 역할을 한다. 


2. Room 의 구조

https://developer.android.com/training/data-storage/room?hl=ko

1. Entity

Entity는 데이터베이스의 테이블이라 생각하면된다. 한국말로는 '개체'로 여러 속성들이 모여 만든 하나의 정보 단위이다.

//테이블 생성
@Entity(tableName = "video")
data class VideoEntity(
    @PrimaryKey(autoGenerate = true)
    var id : Int?,
    var videoId: String,
    var title: String,
    var videoTime: String,
    var view: String
)

여기에서는 id, videoId, title, videoTime, view의 속성이 모여 만든 개체이다. 이 때 id의 값이 null이어도 autoGenerate = true기 때문에 자동으로 id값을 넣어준다.

 

2. DAO

Data Access Object의 줄임말로 데이터에 접근할 수 있는 메서드를 정의해놓은 인터페이스이다.

@Dao
interface VideoDAO {

    // onConflict : id 가 같은 것이 있으면 데이터 덮어쓰기
    @Insert(onConflict = REPLACE)
    fun insert(video: VideoEntity)

    @Query("SELECT * FROM video")
    fun getAll() : List<VideoEntity>

    @Delete
    fun delete(video: List<VideoEntity>)
}

@Insert, @Delete 어노테이션을 이용해 데이터를 넣고 삭제하는 기능을 자동으로 완성해준다. 

@Query 어노테이션을 이용하면 원하는 쿼리문 함수를 만들 수 있다. 

 

3. Room Database

@Database(entities = arrayOf(VideoEntity::class), version = 1)
abstract class VideoDatabase : RoomDatabase() {
    abstract fun videoDAO() : VideoDAO

    // 객체를 생성하는 비용을 줄이기 위해 싱글톤 패턴을 사용한다.
    companion object{
        var instance : VideoDatabase? = null

        fun getInstance(context: Context): VideoDatabase? {
            if (instance == null){
                synchronized(VideoDatabase::class){
                    instance = Room.databaseBuilder(context.applicationContext,
                    VideoDatabase::class.java, "video.db")
                        .fallbackToDestructiveMigration() // database 에서 스키마의 변화가 생길 때 모든 데이터를 drop 하고 새로 생성
                        .build()
                }
            }
            return instance
        }
    }
}

VideoDatabase : 데이터베이스를 생성하고 관리하는 데이터베이스 객체를 만들기 위해서 RoomDatabase()를 상속받는 추상 클래스이다. 여기서 싱글톤 패턴을 이용해 VideoDatabase 객체를 하나만 생성한다.(객체를 생성하는 비용을 줄일 수 있다.) 

getInstance 함수는 instance(VideoDatabase 객체)를 반환한다. database 객체를 생성하기 위해서는 Room.databaseBuilder를 사용한다. 이 때 fallbackToDestructiveMigration은 database에서 스키마의 변화가 생길 때 모든 데이터를 드랍하고 새로 생성하는 옵션이다.

 

4. 데이터 베이스 사용

        // db 가져오기
        db = VideoDatabase.getInstance(this)!!

    fun insertVideo(video : VideoEntity){
        val insertTask = object : AsyncTask<Unit, Unit, Unit>(){
            override fun doInBackground(vararg p0: Unit?) {
                db.videoDAO().insert(video)
            }

            override fun onPostExecute(result: Unit?) {
                super.onPostExecute(result)
                getAllVideo()
            }
        }
        insertTask.execute()
    }

    fun getAllVideo(){
        val getTask = object : AsyncTask<Unit,Unit,Unit>(){
            override fun doInBackground(vararg p0: Unit?) {
                videoList = db.videoDAO().getAll()
            }

            override fun onPostExecute(result: Unit?) {
                super.onPostExecute(result)
                setRecyclerView(videoList)
            }
        }
        getTask.execute()
    }

    fun deleteVideo(videoList: List<VideoEntity>){
        val deleteTask = object : AsyncTask<Unit,Unit,Unit>(){
            override fun doInBackground(vararg p0: Unit?) {
                db.videoDAO().delete(videoList)
            }
        }
        deleteTask.execute()
    }

db.vdeoDAO().insert / getAll /delete 함수를 사용해 데이터베이스의 CRUD 작업을 수행할 수 있다. 데이터베이스 작업은 백그라운드에서 처리한다. 그리고 보통 onStop() 콜백함수에서 데이터베이스를 다룬다.


2. Room 예제

이번 예제는 recyclerView + RoomDatabase를 이용한 예제이다. 클라이언트가 데이터를 입력하면 데이터베이스에 추가하고 화면에 표시한다. 

 

1. RoomActivity

class RoomActivity : AppCompatActivity() {

    lateinit var db : VideoDatabase
    var videoList = listOf<VideoEntity>()
    lateinit var binding : ActivityRoomBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // db 가져오기
        db = VideoDatabase.getInstance(this)!!

        val _binding = ActivityRoomBinding.inflate(layoutInflater)
        binding = _binding
        setContentView(binding.root)

        binding.videoAddBtn.setOnClickListener {
            val title = binding.videoEditText.text.toString()
            var video = VideoEntity(null, "videoId", title, "8:00", "조회수 200만 회")
            insertVideo(video)
        }

        binding.removeAllBtn.setOnClickListener {
            deleteVideo(videoList)
            getAllVideo()
        }

    }

    fun insertVideo(video : VideoEntity){
        val insertTask = object : AsyncTask<Unit, Unit, Unit>(){
            override fun doInBackground(vararg p0: Unit?) {
                db.videoDAO().insert(video)
            }

            override fun onPostExecute(result: Unit?) {
                super.onPostExecute(result)
                getAllVideo()
            }
        }
        insertTask.execute()
    }

    fun getAllVideo(){
        val getTask = object : AsyncTask<Unit,Unit,Unit>(){
            override fun doInBackground(vararg p0: Unit?) {
                videoList = db.videoDAO().getAll()
            }

            override fun onPostExecute(result: Unit?) {
                super.onPostExecute(result)
                setRecyclerView(videoList)
            }
        }
        getTask.execute()
    }

    fun deleteVideo(videoList: List<VideoEntity>){
        val deleteTask = object : AsyncTask<Unit,Unit,Unit>(){
            override fun doInBackground(vararg p0: Unit?) {
                db.videoDAO().delete(videoList)
            }
        }
        deleteTask.execute()
    }

    fun setRecyclerView(videoList: List<VideoEntity>){
        binding.recyclerView.layoutManager = LinearLayoutManager(this)
        binding.recyclerView.adapter = CustomAdapter(videoList = videoList)
    }

}

2. CustomAdapter

class CustomAdapter(videoList: List<VideoEntity>) : RecyclerView.Adapter<CustomAdapter.Holder>(){

    var listData = videoList
    lateinit var binding: ItemRecyclerBinding
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder { 
        val view = ItemRecyclerBinding.inflate(LayoutInflater.from(parent.context),parent, false)
        binding = view
       
        return Holder(view.root)
    }

    override fun onBindViewHolder(holder: Holder, position: Int) { 
        val video = listData[position]
        holder.setVideo(video)
    }

    override fun getItemCount(): Int {
        return listData.size
    }

    inner class Holder(itemView: View) : RecyclerView.ViewHolder(itemView){
        init { // 목록클릭 이벤트 처리
            itemView.setOnClickListener{
                Toast.makeText(itemView?.context, "클릭된 아이템 = ${binding.videoTitle.text}", Toast.LENGTH_LONG).show()
            }
        }

        fun setVideo(video: VideoEntity){
            binding.textNo.text = "${video.id}"
            binding.videoTitle.text = video.title
            binding.videoView.text = video.view
            binding.textVideoId.text = video.videoId
        }
    }
}

실행 결과

느낀점
SQLite과 다르게 직접 sql문을 입력하고 관리하지 않아도 돼서 편했다. 또한, Entity / DAO / Database 로 나누다 보니 유지보수 관점에서 SQLite보다 이점이 크다 생각된다. 
반응형
profile

Idealim

@Idealim

읽어주셔서 감사합니다. 잘못된 내용이 있으면 언제든 댓글로 피드백 부탁드립니다.