이준범
데이터 계층 구축하기
목차
01
02
03
데이터 레이어란?
데이터 레이어 구축 코드랩 살펴보기
실전에서는?
시작하기에 앞서 …
발표자료
데이터 레이어란?
도메인 레이어와 UI 레이어가 의존하는 레이어인 데이터 레이어를 보여주는 다이어그램
애플리케이션에 필요한 데이터를 관리하는 계층
데이터 레이어 구축하기 코드랩 살펴보기
데이터 요청
로컬로
데이터 요청
데이터 저장
로컬에
데이터 저장
네트워크에 데이터 저장
sync
TODO
data class Task(
val title: String = "",
val description: String = "",
val isCompleted: Boolean = false,
val id: String,
)
Immutable
TaskNetwork
NetworkTask
TaskRepository
Business logic
TaskDao
LocalTask
updates tasks
exposes tasks
reads/writes
reads/writes
TODO
@Entity(
tableName = "task"
)
data class LocalTask(
@PrimaryKey val id: String,
var title: String,
var description: String,
var isCompleted: Boolean,
)
@Dao
interface TaskDao {
@Query("SELECT * FROM task")
fun observeAll(): Flow<List<LocalTask>>
@Upsert
suspend fun upsert(task: LocalTask)
...
}
@Database(entities = [LocalTask::class], version = 1, exportSchema = false)
abstract class ToDoDatabase : RoomDatabase() {
abstract fun taskDao(): TaskDao
}
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Singleton
@Provides
fun provideDataBase(@ApplicationContext context: Context): ToDoDatabase {
return Room.databaseBuilder(
context.applicationContext,
ToDoDatabase::class.java,
"Tasks.db"
).build()
}
@Provides
fun provideTaskDao(database: ToDoDatabase) : TaskDao = database.taskDao()
}
테스트
class TaskDaoTest {
private lateinit var database: ToDoDatabase
@Before
fun initDb() {
database = Room.inMemoryDatabaseBuilder(
getApplicationContext(),
ToDoDatabase::class.java
).allowMainThreadQueries().build()
}
@Test
fun insertTaskAndGetTasks() = runTest {
val task = LocalTask(
title = "title",
description = "description",
id = "id",
isCompleted = false,
)
database.taskDao().upsert(task)
val tasks = database.taskDao().observeAll().first()
assertEquals(1, tasks.size)
assertEquals(task, tasks[0])
}
}
TaskNetwork
NetworkTask
TaskRepository
Business logic
TaskDao
LocalTask
updates tasks
exposes tasks
reads/writes
reads/writes
TODO
data class NetworkTask(
val id: String,
val title: String,
val shortDescription: String,
val priority: Int? = null,
val status: TaskStatus = TaskStatus.ACTIVE
) {
enum class TaskStatus {
ACTIVE,
COMPLETE
}
}
class TaskNetworkDataSource @Inject constructor() {
// A mutex is used to ensure that reads and writes are thread-safe.
private val accessMutex = Mutex()
private var tasks = listOf(NetworkTask(...))
suspend fun loadTasks(): List<NetworkTask> = accessMutex.withLock {
delay(SERVICE_LATENCY_IN_MILLIS)
return tasks
}
suspend fun saveTasks(newTasks: List<NetworkTask>) = accessMutex.withLock {
delay(SERVICE_LATENCY_IN_MILLIS)
tasks = newTasks
}
}
TaskNetwork
NetworkTask
TaskRepository
Business logic
TaskDao
LocalTask
updates tasks
exposes tasks
reads/writes
reads/writes
TODO
class DefaultTaskRepository @Inject constructor(
private val localDataSource: TaskDao,
private val networkDataSource: TaskNetworkDataSource,
) {
fun observeAll() : Flow<List<Task>> { … }
suspend fun complete(taskId: String) { … }
suspend fun refresh() { … }
suspend fun create(title: String, description: String)
…
}
suspend fun create(title: String, description: String): String {
val taskId = createTaskId() // 복잡한 작업
val task = Task(
title = title,
description = description,
id = taskId,
)
localDataSource.upsert(task.toLocal())
return taskId
}
class DefaultTaskRepository @Inject constructor(
private val localDataSource: TaskDao,
private val networkDataSource: TaskNetworkDataSource,
@DefaultDispatcher private val dispatcher: CoroutineDispatcher,
) {
…
}
suspend fun create(title: String, description: String): String {
val taskId = withContext(dispatcher) {
createTaskId()
}
val task = Task(
title = title,
description = description,
id = taskId,
)
localDataSource.upsert(task.toLocal())
return taskId
}
TaskNetwork
NetworkTask
TaskRepository
Business logic
TaskDao
LocalTask
updates tasks
exposes tasks
reads/writes
reads/writes
TODO
Data Synchronization
Load
TaskRepogitory
Local
Data Synchronization
Save
TaskRepogitory
Local
Network
1
2
Data Synchronization
Refresh
TaskRepogitory
Local
Network
2
1
TODO Save
TaskRepogitory
Local
Network
1
2
private suspend fun saveTasksToNetwork() {
val localTasks = localDataSource.observeAll().first()
val networkTasks = withContext(dispatcher) {
localTasks.toNetwork()
}
networkDataSource.saveTasks(networkTasks)
}
suspend fun create(title: String, description: String): String {
val taskId = withContext(dispatcher) {
createTaskId()
}
...
localDataSource.upsert(task.toLocal())
saveTasksToNetwork()
return taskId
}
//Blocking!
class DefaultTaskRepository @Inject constructor(
private val localDataSource: TaskDao,
private val networkDataSource: TaskNetworkDataSource,
@DefaultDispatcher private val dispatcher: CoroutineDispatcher,
@ApplicationScope private val scope: CoroutineScope
) {
…
}
private fun saveTasksToNetwork() {
scope.launch {
val localTasks = localDataSource.observeAll().first()
val networkTasks = withContext(dispatcher) {
localTasks.toNetwork()
}
networkDataSource.saveTasks(networkTasks)
}
}
suspend fun create(title: String, description: String): String {
val taskId = withContext(dispatcher) {
createTaskId()
}
...
localDataSource.upsert(task.toLocal())
saveTasksToNetwork()
return taskId
}
TODO
테스트
class FakeTaskDao(initialTasks: List<LocalTask>) : TaskDao {
private val _tasks = initialTasks.toMutableList()
private val tasksStream = MutableStateFlow(_tasks.toList())
override suspend fun upsert(task: LocalTask) {
_tasks.removeIf { it.id == task.id }
_tasks.add(task)
tasksStream.emit(_tasks)
}
override suspend fun upsertAll(tasks: List<LocalTask>) {
val newTaskIds = tasks.map { it.id }
_tasks.removeIf { newTaskIds.contains(it.id) }
_tasks.addAll(tasks)
}
...
}
class DefaultTaskRepositoryTest {
private var testDispatcher = UnconfinedTestDispatcher()
private var testScope = TestScope(testDispatcher)
private val localTasks = listOf(
LocalTask(id = "1", title = "title1", description = "description1", isCompleted = false),
LocalTask(id = "2", title = "title2", description = "description2", isCompleted = true),
)
private val localDataSource = FakeTaskDao(localTasks)
private val networkDataSource = TaskNetworkDataSource()
private val sut = DefaultTaskRepository(
localDataSource = localDataSource,
networkDataSource = networkDataSource,
dispatcher = testDispatcher,
scope = testScope
)
}
TODO 를 작성한다
새로운
TODO를 추가한다.
Given
When
TODO 가 저장된다.
Then
01
02
03
private val sut = DefaultTaskRepository(…)
@Test
fun onTaskCreation_localAndNetworkAreUpdated() = testScope.runTest {
// When
val newTaskId = taskRepository.create(
localTasks[0].title,
localTasks[0].description
)
// Then
val localTasks = localDataSource.observeAll().first()
assertEquals(true, localTasks.map { it.id }.contains(newTaskId))
val networkTasks = networkDataSource.loadTasks()
assertEquals(true, networkTasks.map { it.id }
.contains(newTaskId)) }
데이터 계층 구축하기👏
실전에서는?
Data Layer 필요할까?
왜 사용할까?
Repository 구성은 어떻게 할까?
Test는 어떻게 작성할까?
Thank You
이준범
Android Developer
Building a Data Layer
Code Lab