다이어리 앱 2. 내부 DB 연결 - Room

woniwon·2024년 3월 18일
0

Android

목록 보기
12/19
post-thumbnail

Room 종속 항목 추가

  • 모듈 수준 gradle 파일에 Room 라이브러리의 종속 항목 추가
    plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("com.google.devtools.ksp") //ksp 관련 추가
    }
    dependencies{ // 다른 코드 생략
    //Room
    implementation("androidx.room:room-runtime:${rootProject.extra["room_version"]}")
    ksp("androidx.room:room-compiler:${rootProject.extra["room_version"]}")
    implementation("androidx.room:room-ktx:${rootProject.extra["room_version"]}")
    }
  • App 수준 gradle 파일에 추가
//Room 버전 - 문서 참고해서 업데이트 필요
buildscript {
    extra.apply {
        set("room_version", "2.6.1") 
    }
}

plugins {
    id("com.android.application") version "8.2.2" apply false
    id("org.jetbrains.kotlin.android") version "1.9.22" apply false // 버전 관리 필요 -> compose 때문에 필요함
    id("com.google.devtools.ksp") version "1.9.22-1.0.17" apply false //ksp 추가
}

KSP, Room 버전 관리 관련 문서

Compose와 Kotlin 버전 관리

Diary 모델 생성 (Entitiy 만들기)

@Entity(tableName = "diarys") // Table 이름 지정
data class Diary(
    @PrimaryKey (autoGenerate = true) // ID를 PK로 지정 후 자동 추가
    val id: Int=0, // 가장 최초의 값을 0으로 지정 
    val title: String,
    val content: String,
    val createdDate: LocalDateTime = LocalDateTime.now(), // 기본값 설정
    val modifiedDate: LocalDateTime? = null // Null 허용
)
  • ID : 각각의 일기를 구분할 수 있는 PK가 필요
  • createdDate : 일기를 쓰는 날짜
  • modifiedDate : 마지막 수정 날짜
⚠️ 여기서 Room은 LocalDateTime과 같은 복잡한 타입을 **직접적으로 저장할 수 없다**! → DB에 저장 가능한 기본 타입으로 변환해야함 → TypeConverter를 사용한다
  • LocalDateTimeConverter 코드
    package com.example.diary.model
    
    import androidx.room.TypeConverter
    import java.time.LocalDateTime
    import java.time.format.DateTimeFormatter
    
    object LocalDateTimeConverter {
        private val formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME
    
        @TypeConverter
        fun toLocalDateTime(value: String?): LocalDateTime? {
            return value?.let { formatter.parse(it, LocalDateTime::from) }
        }
    
        @TypeConverter
        fun fromLocalDateTime(date: LocalDateTime?): String? {
            return date?.format(formatter)
        }
    }
    • LocalDateTime 객체를 String 타입으로 그리고 그 반대로 변환하는데 사용

    • 이후 Room DB에 알려줘야 하기 때문에, Database 클래스에 @TypeConverters 어노테이션을 사용해 타입 컨버터를 등록한다

      @TypeConverters(LocalDateTimeConverter::class)
      abstract class DiaryDatabase : RoomDatabase() {
      ...
      }

DAO 만들기

  • DAO는 쿼리를 작성해 복잡한 DB 작업이 가능하도록 해주는 것( 데이터에 접근하는 객체)
  • 이때, 의존성을 낮춰주고 단일 책임 원칙을 따를 수 있도록 해준다. → 데이터를 사용하는 코드와 관계없이 데이터 레이어를 변경이 가능해지게 함!
@Dao
interface DiaryDao {
    // Suspend : 별도의 스레드에서 실행
    // onConflict~~ : 충돌이 발생할 경우 Room에 실행할 작업 알려줌 -> 여기선 IGNORE 즉 새 항목을 무시
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insert(diary: Diary)

    @Update
    suspend fun update(diary: Diary)

    @Delete
    suspend fun delete(diary: Diary)

    @Query("SELECT * from diarys WHERE id = :id")
    fun getDiary(id:Int): LiveData<Diary>

    @Query("SELECT * from diarys ORDER BY createdDate")
    fun getALLDiaries(): LiveData<List<Diary>>

    @Query("SELECT * FROM diarys WHERE id = :id")
    suspend fun getDiaryById(id: Int): Diary?
}

Database 인스턴스 만들기

  • Entity 및 DAO를 사용하는 RoomDataBase를 생성 → Entity 및 DAO 목록을 정의한다
  • DataBase 인스턴스를 가져오는 일반적인 방법
    1. RoomDatabase를 확장하는 public abstract 클래스를 만든다. (추상 클래스)
      → 추상 클래스는 상속(Extends)을 통해서 자식 클래스(Room) 에 의해 완성이 된다. 그래서 추상클래스 자체로는 제 기능을 다하지는 못하지만, 새로운 기능을 정의하는데 있어서 바탕(틀)이 될 수 있다!
    2. 클래스에 @Database 주석을 단다. 인수에서 데이터베이스의 항목을 나열하고 버전 번호를 설정
    3. ItemDao 인스턴스를 반환하는 추상 메서드나 속성을 정의하면 Room에서 구현을 생성
    4. 전체 앱에 RoomDatabase 인스턴스 하나만 있으면 되므로 RoomDatabase를 싱글톤으로 만듭니다.
    5. Room의 [Room.databaseBuilder](https://developer.android.com/reference/androidx/room/Room?hl=ko#databaseBuilder(android.content.Context,java.lang.Class,kotlin.String))를 사용하여 (diary_database) 데이터베이스를 만든다(없는 경우에만). 있다면 기존 데이터베이스를 반환
/**
 * @Database 주석에는 Room이 데이터베이스를 빌드할 수 있도록 인수가 여러 개 필요하다
 *
 * Diary를 entities 목록이 있는 유일한 클래스로 지정한다.
 * version을 1로 설정한다. 데이터베이스 테이블의 스키마를 변경할 때마다 버전 번호를 높여야 함
 * 스키마 버전 기록 백업을 유지하지 않도록 exportSchema를 false로 설정
 */
@Database(entities = [Diary::class], version=1, exportSchema = false)
@TypeConverters(LocalDateTimeConverter::class)
abstract class DiaryDatabase : RoomDatabase() {
    abstract fun diaryDao(): DiaryDao //데이터베이스가 DAO에 관해 알 수 있도록 diaryDao를 반환하는 추상 함수를 클래스 본문 내에서 선언

    companion object{ //데이터베이스를 만들거나 가져오는 메서드에 대한 액세스를 허용하고 클래스 이름을 한정자로 사용하는 companion object를 정의
        @Volatile // Instance값이 항상 최신으로 유지되고 모든 실행 스레드에 동일하게 유지 -> 한 스레드 인스턴스 변경시 모든 스레드에 즉시 반영
        private var instance: DiaryDatabase? = null // 데이터베이스에 관한 null을 허용하는 비공개 변수 Instance를 선언하고 null로 초기화 -> 열린 DB의 단일 인스턴스 유지 가능

        fun getDatabase(context: Context): DiaryDatabase {

            // if the Instance is not null, return it, otherwise create a new database instance.
            return instance ?: synchronized(this) { // 코드를 래핑해 Synchronized 블록 내에 DB를 가져오면 한 번에 한 실행 스레드만 코드블록에 들어가짐 -> 1번만 초기화 -> 경합상태 방지
                Room.databaseBuilder(context, DiaryDatabase::class.java, "diary_database") //동기화된 블록 내에서 데이터베이스 빌더를 사용하여 데이터베이스를 가져옴
                    .build()
                    .also { instance = it } // Instance = it을 할당하여 최근에 만들어진 db 인스턴스에 대한 참조를 유지
            }
        }
    }
}

Repository 구현 (저장소)

// dao 구현에 매핑되는 함수를 인터페이스에 추가
interface DiaryRepository {
    fun getALLDiariesStream(): LiveData<List<Diary>>

    fun getDiaryStream(id:Int) : LiveData<Diary>

    suspend fun insertDiary(diary: Diary)

    suspend fun deleteDiary(diary: Diary)

    suspend fun updateDiary(diary: Diary)

    suspend fun getDiaryById(id: Int): Diary?
}

suspend 함수

  • suspend fun는 일시 중단 가능한 함수로, 해당 함수 내에 일시 중단이 가능한 작업이 있다는 것을 뜻한다.
  • 따라서 suspend fun은 코루틴 내부에서 또는 suspend fun 내부에서만 사용할 수 있다.
  • 비동기 코드의 간소화 : 복잡한 비동기 코드를 간단하게 동기코드 처럼 사용할 수 있음 ( repo에서 쓴 이유)
  • 컨텍스트 전환 용이: 코루틴을 사용할 때, withContext 같은 함수와 함께 suspend 함수를 사용하면 쉽게 스레드 컨텍스트를 전환할 수 있다. 예를 들어, 백그라운드 작업을 위해 IO 디스패처로 전환한 뒤 다시 메인 스레드로 결과를 반환하는 과정을 간단히 구현할 수 있다.
  • 자원의 효율적 사용: suspend 함수는 필요할 때에만 실행되며, 작업이 완료될 때까지 현재 코루틴의 실행을 중단시킵니다. 이는 코루틴을 사용하는 동안 스레드를 불필요하게 점유하지 않으므로, 자원을 효율적으로 사용할 수 있게 한다.

OfflineDiaryRepository 구현

  • 기존의 DiaryRepository 인터페이스에 정의된 함수를 재정의 하고 DiaryDao에 상응하는 함수를 호출

class OfflineDiariesRepository(private val diaryDao: DiaryDao) : DiaryRepository{

    override fun getALLDiariesStream(): LiveData<List<Diary>> = diaryDao.getALLDiaries()

    override fun getDiaryStream(id:Int): LiveData<Diary> = diaryDao.getDiary(id)
    override suspend fun getDiaryById(id: Int): Diary? = diaryDao.getDiaryById(id)

    override suspend fun deleteDiary(diary: Diary) = diaryDao.delete(diary)

    override suspend fun insertDiary(diary: Diary) = diaryDao.insert(diary)

    override suspend fun updateDiary(diary: Diary) = diaryDao.update(diary)
}
  • 인터페이스와 추상 클래스

    주요 차이점

    • 다중 상속: 인터페이스는 다중 구현이 가능하지만, 추상 클래스는 단일 상속만 가능
    • 멤버 변수: 인터페이스는 상수만을 가질 수 있으며, 추상 클래스는 인스턴스 변수를 가질 수 있다
    • 메서드 구현: 인터페이스는 Java 8 이전에는 구현 메서드를 가질 수 없었지만, 추상 클래스는 구현된 메서드를 포함할 수 있다
  • DAO와 Repository

    DAO (Data Access Object)

    • 목적: 데이터베이스 또는 다른 저장소에 대한 접근을 추상화하고 캡슐화 한다. DAO는 저장소에 저장된 데이터에 대한 접근을 제공하는 특정 메서드(예: CRUD - 생성, 읽기, 업데이트, 삭제)를 정의한다.

    • 특징:

      • DAO는 보통 특정 엔티티(데이터베이스 테이블 또는 문서 등)에 초점을 맞춘다.
      • DAO는 데이터베이스 특정 쿼리 언어(SQL 등)를 사용하여 데이터를 조작한다.
      • DAO는 데이터베이스나 다른 저장소의 세부 구현에 밀접하게 연관되어 있다.
    • 사용 시나리오: 데이터베이스와 같은 특정 유형의 데이터 소스에 접근하고, 해당 데이터 소스의 세부 구현에 의존하는 애플리케이션에서 사용된다.

      Repository

    • 목적: 도메인 모델과 데이터 저장소 사이의 메디에이터 혹은 컬렉션처럼 동작하는 계층

      • Repository는 도메인 로직이 데이터베이스의 구조나 쿼리 언어에 의존하지 않도록 하며, 도메인 객체의 컬렉션에 대해 더 추상화된 접근을 제공한다.
    • 특징:

      • Repository는 도메인 모델의 관점에서 데이터 컬렉션에 대한 접근을 추상화한다.
      • Repository는 일반적으로 데이터를 어떻게 저장하고 검색하는지에 대한 세부 사항을 숨긴다
      • Repository는 도메인 로직을 데이터 저장소 로직으로부터 분리하고, 애플리케이션의 비즈니스 로직에 초점을 맞춘다.
    • 사용 시나리오: 애플리케이션에서 도메인 중심의 접근 방식을 사용하고, 데이터베이스 구조나 저장소의 변경으로부터 도메인 로직을 보호하고자 할 때 사용

      차이점

    • 추상화 수준: DAO는 데이터 저장소의 접근을 추상화하는 반면, Repository는 도메인 모델과 애플리케이션 로직을 데이터 저장소로부터 분리하는 더 높은 수준의 추상화를 제공한다

    • 목적의 차이: DAO는 데이터 접근에 집중하는 반면, Repository는 도메인 로직의 구현과 데이터 저장소 사이의 결합도를 낮추는 데 더 초점을 맞춘다

    • 사용하는 관점: DAO는 데이터베이스 또는 특정 저장소에 가까운 반면, Repository는 도메인 모델과 비즈니스 로직에 더 가깝다.

profile
단순 기록용 Velog 입니다.

0개의 댓글