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"]}")
}
//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 추가
}
@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 허용
)
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
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?
}
RoomDatabase
를 확장하는 public abstract
클래스를 만든다. (추상 클래스)(Room)
에 의해 완성이 된다. 그래서 추상클래스 자체로는 제 기능을 다하지는 못하지만, 새로운 기능을 정의하는데 있어서 바탕(틀)이 될 수 있다! @Database
주석을 단다. 인수에서 데이터베이스의 항목을 나열하고 버전 번호를 설정ItemDao
인스턴스를 반환하는 추상 메서드나 속성을 정의하면 Room
에서 구현을 생성RoomDatabase
인스턴스 하나만 있으면 되므로 RoomDatabase
를 싱글톤으로 만듭니다.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 인스턴스에 대한 참조를 유지
}
}
}
}
// 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?
}
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)
}
목적: 데이터베이스 또는 다른 저장소에 대한 접근을 추상화하고 캡슐화 한다. DAO는 저장소에 저장된 데이터에 대한 접근을 제공하는 특정 메서드(예: CRUD - 생성, 읽기, 업데이트, 삭제)를 정의한다.
특징:
사용 시나리오: 데이터베이스와 같은 특정 유형의 데이터 소스에 접근하고, 해당 데이터 소스의 세부 구현에 의존하는 애플리케이션에서 사용된다.
목적: 도메인 모델과 데이터 저장소 사이의 메디에이터 혹은 컬렉션처럼 동작하는 계층
특징:
사용 시나리오: 애플리케이션에서 도메인 중심의 접근 방식을 사용하고, 데이터베이스 구조나 저장소의 변경으로부터 도메인 로직을 보호하고자 할 때 사용
추상화 수준: DAO는 데이터 저장소의 접근을 추상화하는 반면, Repository는 도메인 모델과 애플리케이션 로직을 데이터 저장소로부터 분리하는 더 높은 수준의 추상화를 제공한다
목적의 차이: DAO는 데이터 접근에 집중하는 반면, Repository는 도메인 로직의 구현과 데이터 저장소 사이의 결합도를 낮추는 데 더 초점을 맞춘다
사용하는 관점: DAO는 데이터베이스 또는 특정 저장소에 가까운 반면, Repository는 도메인 모델과 비즈니스 로직에 더 가깝다.