GDSC HUFS 3기/Android with Kotlin Team 5

[5팀]코틀린 안드로이드 기초강의_41,42 | SQLite, Room

루이란 2021. 11. 23. 18:05

이 글은 이것이 안드로이드다 with 코틀린(개정판)를 참고하여 작성하였습니다.

작성자 : 임예람

개발환경은 Windows, Android Studio입니다.

1. Android와 SQLite 데이터 베이스

📌SQLite: 안드로이드에서 기본적으로 제공하는 데이터 베이스이다.

👉🏻SQLite를 사용해서 메모장을 만들어보자!

SQLiteHelper

👉🏻SQLiteHelper: SQLite를 사용할 수 있는 도구가 되는 클래스

👉🏻메모장 테이블을 생성하는 onCreate와 메모장 변경사항을 저장하는 onUpgrade를 만든다.

MainActivity

👉🏻Main에서 sqliteHelper를 정의해준다.

✍🏻db 이름과 버전을 sqliteHelper 생성자로 넘겨주어 파일이 있는디 디렉토리를 찾는다. 없다면 onCreate 함수가 실행되어 memo 테이블을 생성한다.

SQLiteHelper

👉🏻데이터 입력(메모 적는)함수

✍🏻wd: writableDatabase로 SQLiteOpenHelper에서 제공하는 데이터를 적는 기능이다.

SQLiteHelper

👉🏻데이터 조회(메모 확인하는)함수

✍🏻rd: readableDatabase로 wd와 마찬가지로 SQLiteOpenHelper에서 제공하는 데이터를 읽는 기능이다.

SQLiteHelper

👉🏻데이터 수정(메모 수정하는)함수와 데이터 삭제(메모 삭제하는)함수

✍🏻whereClause: 몇번째 데이터를 수정할지 위치를 정한다.

👉🏻SQLiteHelper에 메모 CRUD(메모 생성, 읽기, 수정, 삭제)를 모두 생성해둔다. 한 곳에 다 모아놨기 때문에 앞으로 Main에서는 데이터 처리할 때 helper만 바라보면 된다.

activity_main

👉🏻이런식으로 리사이클러뷰로 메모 리스트를 표시하고 메모를 입력하고 저장할 버튼을 만든다.

👉🏻높이를 100dp로 설정한 아이템 하나의 항목 레이아웃을 만든다.

RecyclerAdapter.kt

👉🏻아답터를 생성해 리사이클러에 데이터를 set하는 코드를 적는다.

✍🏻SimpleDateFormat: 어떠한 형식으로 날짜를 출력할지 지정한다.

RecyclerAdapter.kt

👉🏻Holder와 Helper를 이용해 데이터를 넣어주고 출력하는 코드를 쓴다.

MainActivity

👉🏻Main에 아답터를 생성하고 메모를 적고 저장하는 코드를 짠다.

👉🏻Main에 버튼을 누르면 메모를 저장하는 코드를 작성한다.

 

2. Room 데이터베이스

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

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

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-kapt'
}

dependencies {
    def roomVersion = "2.3.0"
    implementation("androidx.room:room-runtime:$roomVersion")
    annotationProcessor("androidx.room:room-compiler:$roomVersion")

    kapt("androidx.room:room-compiler:$roomVersion")
    implementation("androidx.room:room-ktx:$roomVersion")
    ...
}

👉🏻build.gradle에 다음과 같이 설정해준다.

//메모리스트를 받아와 각각 메서드를 실행
class RecyclerAdapter(val roomMemoList:List<RoomMemo>) : RecyclerView.Adapter<RecyclerAdapter.Holder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
        val binding = ItemRecyclerBinding.inflate(
                        //첫 인자는 context, 두번째는 부모 뷰
                        LayoutInflater.from(parent.context), parent, false)
        return Holder(binding) //홀더에 바인딩을 넘겨준다.
    }

    override fun onBindViewHolder(holder: Holder, position: Int) {
        //해당 메모 위치를 얻어와 set한다.
        holder.setMemo(roomMemoList.get(position))
    }

    override fun getItemCount() = roomMemoList.size

    //아답터는 항상 홀더를 가장 먼저 세팅
    class Holder(binding: ItemRecyclerBinding) : RecyclerView.ViewHolder(binding.root) {
        //메모 내용 set하는 함수
        fun setMemo(roomMemo:RoomMemo) {
            textNo.text = "${roomMemo.no}"
            textContent.text = roomMemo.content
            //Long 타입 숫자를 날짜로 변환
            val sdf = SimpleDateFormat("yyyy/MM/dd hh:mm")
            textDatetime.text = sdf.format(roomMemo.datetime)
        }
    }
}

👉🏻RecyclerAdapter에서 메모 내용을 set하는 코드를 짠다.

✍🏻항상 Holder를 먼저 세팅해준다.

//데이터는 테이블 형식으로 들어간다.
@Entity(tableName = "room_memo")
class RoomMemo {
    //null이 들어가면 숫자를 자동으로 생성해주는 키
    @PrimaryKey(autoGenerate = true)//no에 값이 없을 때 자동증가된 숫자값을 db에 입력해준다.
    //메모에 쓸 변수 생성
    @ColumnInfo //컬럼을 사용한다고 명시
    var no: Long? = null
    @ColumnInfo
    var content: String = ""
    @ColumnInfo(name = "date") //데이터베이스에는 date라는 이름으로 컬럼을 생성한다.
    var datetime: Long = 0

    //생성자를 사용해 내용과 날짜를 받는다. 편리하게 사용
    constructor(content: String, datetime: Long) {
        this.content = content
        this.datetime = datetime
    }
}

👉🏻RoomMemo 파일에서 메모 데이터 형식을 정해준다.

✍🏻@Entity: 기본적으로 Room은 클래스 이름을 데이터베이스 테이블 이름으로 사용한다. 테이블의 이름을 다르게 지정하려면 @Entity 주석의 tableName 속성을 설정해준다.

✍🏻@PrimaryKey: null 값이 들어오면 숫자를 자동으로 생성해준다. (Room에서 항목에 자동 ID를 할당한다.)

✍🏻@ColumnInfo: 컬럼을 사용한다고 명시해준다.

@Dao // dao임을 알려준다.
interface RoomMemoDAO {
    //쿼리를 직접 짜준다.
    @Query("select * from room_memo")
    fun getAll() : List<RoomMemo>
    
    //충돌이 나면 교체해준다.
    @insert(onConflict = onConflictStrategy.REPLACE)
    fun insert(memo:RoomMemo)
    
    @delete
    fun insert(memo:RoomMemo)
}

👉🏻RoomMemoDAO에서 세개의 메서드를 만들어 준다.

//데이터베이스임을 명시하고 내가 사용하는 데이터 개수와 버전을 넘겨준다.
@Database(entities = arrayOf(RoomMemo::class), version = 1, exportSchema = false)
//helper는 직접 사용할 수 있는 class가 아니므로 abstract이다. 
abstract class RoomHelper : RoomDatabase() {
    //룸 헬퍼를 통해 이 메서드를 호출하면 RoomMemoDAO 인터페이스가 반환된다.
    //Dao에 있는 메서드를 꺼내 쓸 수 있다.
    //room이 코드를 알아서 생성해주므로 간결하다!
    abstract fun roomMemoDao(): RoomMemoDAO
}

👉🏻실제로 room을 사용할 Helper클래스를 만들어준다.

✍🏻helper는 직접 사용할 수 있는 class가 아니므로 abstract으로 선언한다.

✍🏻roomMemoDao를 지정하고 불러주기만 하면 room이 코드를 알아서 생성해준다. (굉장히 편리한 도구)

class MainActivity : AppCompatActivity() {

    val binding by lazy{ ActivityMainBinding.inflate(layoutInflater)}
    
    //room을 사용하기 위해 helper 선언
    lateinit var helper:RoomHelper
    //아답터 생성
    lateinit var memoAdapter:RecyclerAdapter
    //메모리스트 갱신
    val memoList = mutableListOf<RoomMemo>()
    //내가 쓰는 dao 꺼내놔서 사용
    lateinit var memoDAO:RoomMemoDAO

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        //빌더가 호출되는 순간 룸 헬퍼를 사용할 수 있게 헬퍼 코드를 다 채우고 헬퍼 클래스를 만들어 전달
        helper = Room.databaseBuilder(this, RoomHelper::class.java, "room_db")
            .allowMainThreadQueries() //공부할 때만 쓴다.
            .build()
        
        memoDAO = helper.roomMemoDao()

        //아답터를 이용해 목록을 보여준다.
        memoAdapter = RecyclerAdapter(memoList)
        //아래에 정의된 함수 - 변경 내용을 보여준다.
        refreshAdapter()
        
        with(binding) {
            //아답터와 연결
            recyclerMemo.adapter = memoAdapter
            //with 스코프 내이기 때문에 어디 this인지 명시 필요
            recyclerMemo.layoutManager = LinearLayoutManager(this@MainActivity) 

            buttonSave.setOnClickListener {
                //메모 내용을 가져와서
                val content = editMemo.text.toString()
                //비어있지 않으면 내용을 헬퍼에 담아 넘겨준다.
                if(content.isNotEmpty()) {
                    val datetime = System.currentTimeMillis()
                    val memo = RoomMemo(content, datetime)
                    memoDAO.insert(memo)
                    refreshAdapter()
                }
            }
        }
    }

    //새로운 메모를 집어넣기
    //기존 내용을 비우고 모든 데이터를 불러와 마지막에 바뀐 부분만 표시해 맨 마지막에 추가된 메모가 보임 
    //데이터가 많을 때는 사용하기 힘드니까 데이터가 적을 때 사용
    fun refreshAdapter() {
        memoList.clear()
        memoList.addAll(memoDAO.getAll())
        memoAdapter.notifyDataSetChanged()
    }
}

👉🏻MainActivity에서 이전까지 만들었던 helper, adapter 등을 모두 정의해두고 코드를 적는다. 

👉🏻메모를 업데이트 하는 내용의 코드는 자주 사용하므로 refreshAdapter 함수로 만들어 따로 빼둔다.

✍🏻refreshAdapter 함수의 내용을 보면 데이터 수정 내용을 반영하는 방식이 이전 데이터를 모두 지우고 모든 데이터를 불러와 바뀐 부분만 출력한다. 이 방법은 현재 데이터가 적기 때문에 사용할 수 있는 방식이고 이후 데이터 양이 많아지거나 정식으로 프로젝트 할 때는 사용하지 않는 것이 좋다.