1 of 46

이준범

데이터 계층 구축하기

2 of 46

3 of 46

목차

01

02

03

데이터 레이어란?

데이터 레이어 구축 코드랩 살펴보기

실전에서는?

4 of 46

시작하기에 앞서 …

발표자료

5 of 46

6 of 46

데이터 레이어란?

7 of 46

8 of 46

도메인 레이어와 UI 레이어가 의존하는 레이어인 데이터 레이어를 보여주는 다이어그램

9 of 46

애플리케이션에 필요한 데이터를 관리하는 계층

10 of 46

데이터 레이어 구축하기 코드랩 살펴보기

데이터 요청

로컬로

데이터 요청

데이터 저장

로컬에

데이터 저장

네트워크에 데이터 저장

sync

11 of 46

TODO

12 of 46

data class Task(

val title: String = "",

val description: String = "",

val isCompleted: Boolean = false,

val id: String,

)

Immutable

13 of 46

TaskNetwork

NetworkTask

TaskRepository

Business logic

TaskDao

LocalTask

updates tasks

exposes tasks

reads/writes

reads/writes

TODO

14 of 46

@Entity(

tableName = "task"

)

data class LocalTask(

@PrimaryKey val id: String,

var title: String,

var description: String,

var isCompleted: Boolean,

)

15 of 46

@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

}

16 of 46

@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()

}

17 of 46

테스트

18 of 46

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])

}

}

19 of 46

TaskNetwork

NetworkTask

TaskRepository

Business logic

TaskDao

LocalTask

updates tasks

exposes tasks

reads/writes

reads/writes

TODO

20 of 46

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

}

}

21 of 46

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

}

}

22 of 46

23 of 46

TaskNetwork

NetworkTask

TaskRepository

Business logic

TaskDao

LocalTask

updates tasks

exposes tasks

reads/writes

reads/writes

TODO

24 of 46

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)

}

25 of 46

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

}

26 of 46

class DefaultTaskRepository @Inject constructor(

private val localDataSource: TaskDao,

private val networkDataSource: TaskNetworkDataSource,

@DefaultDispatcher private val dispatcher: CoroutineDispatcher,

) {

}

27 of 46

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

}

28 of 46

TaskNetwork

NetworkTask

TaskRepository

Business logic

TaskDao

LocalTask

updates tasks

exposes tasks

reads/writes

reads/writes

TODO

29 of 46

Data Synchronization

Load

TaskRepogitory

Local

30 of 46

Data Synchronization

Save

TaskRepogitory

Local

Network

1

2

31 of 46

Data Synchronization

Refresh

TaskRepogitory

Local

Network

2

1

32 of 46

33 of 46

TODO Save

TaskRepogitory

Local

Network

1

2

34 of 46

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!

35 of 46

class DefaultTaskRepository @Inject constructor(

private val localDataSource: TaskDao,

private val networkDataSource: TaskNetworkDataSource,

@DefaultDispatcher private val dispatcher: CoroutineDispatcher,

@ApplicationScope private val scope: CoroutineScope

) {

}

36 of 46

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

}

37 of 46

38 of 46

TODO

테스트

39 of 46

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)

}

...

}

40 of 46

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

)

}

41 of 46

TODO 를 작성한다

새로운

TODO를 추가한다.

Given

When

TODO 가 저장된다.

Then

01

02

03

42 of 46

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)) }

43 of 46

데이터 계층 구축하기👏

44 of 46

실전에서는?

Data Layer 필요할까?

왜 사용할까?

Repository 구성은 어떻게 할까?

Test는 어떻게 작성할까?

45 of 46

Thank You

이준범

Android Developer

46 of 46

Building a Data Layer

Code Lab