File: 자바의 file과 동일한 방식으로 사용DataStore: key-value 저장, Proto 저장Room: 로컬 데이터베이스 SQLiteMedia: ContentResolver 이용Documents: DocumentsProvider를 통해 Intent를 사용하여 접근Datasets: (BolbStoreManager를 통해 대형 파일에 접근)
UI Layer
UI 담당
Data Layer
비즈니스 로직, 데이터 저장/읽기 담당
UI와 Data Layer를 분리하여
fileDir 속성(getFilesDir())cacheDir 속성(getCacheDir()), 임시 파일용getExternalFilesDir(null)externalCacheDir 속성(getExternalCacheDir()), 임시 파일용권한(permission)
안드로이드 보안 기능으로, 앱이 시스템의 민감한 리소스를 사용하려고 할 때, 권한이 필요하며 이 권한은 사용자가 명시적으로 승인해야 함
key-value 형태로 저장val Context.dataStore: DataStore<Preferences>
by preferencesDataStore(name = "settings")
DataStore는 하나의 인스턴스만 생성해야 함
Repository
데이터를 저장하고 읽는 것, View와 무관
ViewModel
데이터를 View에 표시하기 위한 것
class MyViewModel(private val myRepository: MyRepository) : ViewModel() {
val myPref = myRepository.myPref
fun setPref(key: Preferences.Key<String>, value: String) {
viewModelScope.launch { // viewmodel라이프사이클에 맞춘 coroutine scope
myRepository.setPref(key, value)
}
}
}

class MyRepository(private val context: Context) {
val myPref : Flow<Preferences> = context.dataStore.data
Consumer는 Flow에서 데이터를 가져다 사용
Consumer는 데이터를 언제 받을지 모르기 때문에 코루틴 내에서 수행되야 함
앞의 예에서 collect를 호출한 것이 Consumer
Flow에서 데이터를 가져와 State로 나타냄
val myPref by viewModel.myPref.collectAsStateWithLifecycle(initialValue = null)

SQLite보다 Room 사용을 권장
클래스를 데이터베이스로 지정하는 annotation,
RoomDatabase를 상속받은 클래스이어야 함
클래스를 테이블 스키마로 지정하는 annotation
클래스를 DAO(Data Access Object)로 지정해주는 annotation
기본적인 insert, delete, update SQL은 자동으로 생성,
복잡한 SQL은 직접 만들 수 있음
annotaion: 코드에 메타데이터(추가 정보)를 붙이는 문법
CREATE TABLE student_table (
student_id INTEGER PRIMARY KEY,
name TEXT NOT NULL
)
@Entity(tableName = "student_table")
data class Student (
@PrimaryKey @ColumnInfo(name = "student_id") val id: Int,
val name: String
)
@ColumnInfo: 실제 테이블 스키마에서 사용할 이름 지정@Entity(tableName = "class_table")
data class ClassInfo (
@PrimaryKey val id: Int,
val name: String,
val day_time: String,
val room: String,
val teacher_id: Int
)
@Entity(tableName = ""): 실제 테이블 이름을 지정
@PrimaryKey(autoGenerate = true): 키 자동으로 생성
@Entity(tableName = "enrollment",
primaryKeys = ["sid", "cid"],
foreignKeys = [
ForeignKey(entity = Student::class, parentColumns = ["student_id"], childColumns = ["sid"]),
// sid는 Student 테이블의 student_id를 참조
ForeignKey(entity = ClassInfo::class, parentColumns = ["id"], childColumns = ["cid"])
// cid는 ClassInfo 테이블의 id를 참조
]
)
data class Enrollment (
val sid: Int,
val cid: Int,
val grade: String? = null
)
interface나 abstract class로 정의되어야 함@Query ("SELECT * from table") fun getAllData() : List<Data>
@Insert, @Update, @Delete는 SQL 쿼리를 작성하지 않아도
컴파일러가 자동으로 생성
@Insert, @Update는 key가 중복되는 경우 처리를 위해 onConflict를 지정
OnConflictStrategy.ABORT: key 충돌 시 종료
OnConflictStrategy.IGNORE: key 충돌 시 무시
OnConflictStrategy.REPLACE: key 충돌 시 새로운 데이터로 변경
@Query로 리턴되는 데이터의 타입을 Flow<>로 하면,
데이터 업데이트 시 Collect/State를 통해 알 수 있음
@Query("SELECT * from table") fun getAllDate() : Flow<List<Data>>
@Query에 SQL을 정의할 때, 메소드의 인자를 사용할 수 있음@Query("SELECT * from student_table WHERE name= :sname")
// 인자 sname을 SQL에서 :sname으로 사용
suspend fun getStudentByName(sname: String) : List<Student>
// 해당 메소드는 block될 수 있으므로 코루틴 내에서 호출
Flow<>를 리턴할 때에는 비동기적으로 동작하기 때문에@Dao
interface MyDAO {
@Insert(onConflict = OnConflictStrategy.REPLACE) // INSERT, key 충돌이 나면 새 데이터로 교체
suspend fun insertStudent(student: Student)
@Query("SELECT * FROM student_table")
fun getAllStudentsFlow(): Flow<List<Student>> // Flow<>사용
@Query("SELECT * FROM student_table WHERE name = :sname")
suspend fun getStudentByName(sname: String): List<Student>
@Delete
suspend fun deleteStudent(student: Student); // primary key is used to find the student
@Transaction
@Query("SELECT * FROM student_table WHERE student_id = :id")
suspend fun getStudentsWithEnrollment(id: Int): List<StudentWithEnrollments>
// ...
}
RoomDatabase를 상속하여 자신의 Room 클래스를 만들어야 함
포함되는 Entity들과 데이터베이스 버전(version)을
@Database annotation에 지정
version이 기존에 저장되어 있는 데이터베이스보다 높으면,
데이터베이스를 open할 때 migration을 수행하게 됨
Migration 수행은 RoomDatabase 객체의 addMigration() 메소드로 지정
DAO를 실제 가져올 수 있는getter 메소드를 만듦
실제 메소드 정의는 자동으로 생성됨
Room 클래스의 인스턴스는 하나만 있으면 되므로 Singleton 패턴 사용
Room 클래스의 객체 생성은 Room.databaseBuilder()를 이용
@Database(entities=[Student::class, ClassInfo::class, Enrollment::class, Teacher::class],exportSchema=false, version=1)
abstract class MyDatabase : RoomDatabase() {
abstract fun getMyDao() : MyDAO
companion object {
private var INSTANCE: MyDatabase? = null
private val MIGRATION_1_2 = object : Migration(1, 2) {
// 버전 1에서 버전 2로 마이그레이션을 의미
override fun migrate(database: SupportSQLiteDatabase) {생략 }
}
private val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {생략 }
}
fun getDatabase(context: Context) : MyDatabase {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(
context, MyDatabase::class.java, "school_database")
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.build()
}
return INSTANCE as MyDatabase
}
}
}
private val MIGRATION_1_2 = object : Migration(1, 2) { // version 1 -> 2
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE student_table ADD COLUMN last_update INTEGER")
}
}
private val MIGRATION_2_3 = object : Migration(2, 3) { // version 2 -> 3
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE student_table ADD COLUMN last_update INTEGER")
}
}
@Entity
data class Item(
@PrimaryKey val id: Int,
val name: String // 컬럼 1개
)
@Entity
data class Item(
@PrimaryKey val id: Int,
val name: String,
val description: String // 컬럼 추가 = 스키마 변경
)
// 문제는 앱이 이미 설치된 사용자의 폰에는 기존 DB가 남아있는데,
// 새 코드는 새로운 구조를 기대하기 때문에 충돌이 나게 됨
// @Database(version = 2) → 버전을 올리고
// Migration(1, 2) → "버전 1짜리 DB를 버전 2로 어떻게 바꿀지" 를 알려줘야 함
In-memory database로 만들어서 테스트 하는 방법도 가능
단, 앱을 다시 시작할 때 마다 저장된 데이터가 초기화됨
Room.inMemoryDatabaseBuilder()
Room 2.4.0-alpha 버전부터 Auto Migration을 지원함
Repository와 ViewModel의 사용 권장
RoomDatabase 객체에서 DAO 객체를 받아오고,
이 DAO객체의 메소드 호출하여 데이터베이스 접근
class MyRepository(private val context: Context) {
private val myDao = MyDatabase.getDatabase(context).getMyDao()
val allStudents = myDao.getAllStudentsFlow()
suspend fun insertStudent(student: Student) = myDao.insertStudent(student)
class MyViewModel(private val myRepository: MyRepository) : ViewModel() {
val allStudents = myRepository.allStudents
fun insertStudent(id: Int, name: String) = viewModelScope.launch {
myRepository.insertStudent(Student(id, name))
}
Flow<> 타입으로 리턴되는 DAO 메소드 경우
@Entity
data class Enrollment(
val sid: Int, // 학생 ID (Student 테이블의 student_id 참조)
val cid: Int // 수업 ID (ClassInfo 테이블의 id 참조)
)
data class StudentWithEnrollments( // 1:N 관계
@Embedded val student: Student,
// 다른 클래스의 속성을 포함시키고 싶을 때 사용
@Relation(parentColumn = "student_id", entityColumn = "sid")
// 연관된 속성값 탐색
val enrollments: List<Enrollment>
)
@Dao
interface MyDAO {
// 생략
@Transaction
@Query("SELECT * FROM student_table WHERE student_id = :id")
suspend fun getStudentsWithEnrollment(id: Int): List<StudentWithEnrollments>
// ...
}
SELECT * from student_table WHERE student_id = :idSELECT * from enrollment WHERE sid = :id@Transaction annotation이 필요함@Transaction
DB 작업을 하나의 단위로 묶는 것