일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 |
- sharedFlow
- Compose
- 파이어베이스
- 코틀린
- 플레이스토어
- Android
- 커스텀뷰
- cleanarchitecture
- 뷰
- 회원가입
- 리사이클러뷰
- 로그인
- NavHost
- UiState
- Build variants
- 알고리즘
- DiffUtil
- XML
- Authentication
- coroutine
- NavController
- Flow
- 안드로이드
- 클린아키텍처
- 컴포즈
- Jetpack
- ListAdapter
- MVVM
- Kotlin
- 코딩테스트
- Today
- Total
Grusie 안드로이드 개발 기술 블로그
[Android] 클린아키텍처에서 Room DB 사용해, 임시저장 기능 구현하기 본문
사이드 프로젝트를 진행하면서, 임시저장 기능을 구현해야 했다.
클린아키텍처 구조를 사용중이며, Room DB를 통해 내부저장소에 저장 할 생각을 하였다.
거두절미하고 코드를 보며 이해해보자
build.gradle.kts(Module:data)
// Room DB
kapt("androidx.room:room-compiler:2.6.1")
implementation ("androidx.room:room-ktx:2.6.1")
Room DB를 사용하기 위한 의존성을 선언해준다.
kapt는 hilt를 사용하기 위해 이미 선언 해뒀을 것이라고 생각한다.
Database
@Database(
entities = [LocalSlowMailInfo::class],
version = 1,
exportSchema = false
)
abstract class SlowMailBoxDatabase : RoomDatabase() {
abstract fun getSlowMailBoxDao() : SlowMailBoxDao
}
Database를 선언해주는 파일이다.
Di Module에서 의존성을 주입해서 사용할 예정이다.
Dao를 생성해서 반환해주는 abstract 함수가 있다.
Dao
@Dao
interface SlowMailBoxDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertSlowMail(slowMailInfo: LocalSlowMailInfo): Long
@Query("SELECT * FROM slowMail WHERE spaceId = :spaceId ORDER BY createAt DESC")
suspend fun getAllSlowMails(spaceId: Long): List<LocalSlowMailInfo>
@Delete
suspend fun deleteSlowMail(slowMailInfo: LocalSlowMailInfo)
}
실질적인 데이터베이스 로직을 수행하기 위한 DAO를 작성하였다.
추 후 추가/수정이 되겠지만, 초기에는 이렇게 구상하였다.
리턴값을 Flow형태나 Result형태로 하려고 하였으나, RoomDB에서는 Result 타입은 지원하지 않고, Flow 타입은 Select문에는 적용되나, Insert문에는 적용되지 않아, 어차피 try-catch를 사용해야 하며, 데이터를 실시간으로 방출하는 것이 아니기에, Flow를 사용하지 않고, dataSource에서 Result로 변환하여 사용하고자 했다.
Entity
@Entity(tableName = "slowMail")
data class LocalSlowMailInfo(
@PrimaryKey(autoGenerate = true)
val id: Long?,
val spaceId: Long?,
val title: String?,
val content: String?,
val createAt: String?,
val paperId: Long?,
val sendAt: String?
)
데이터베이스에서 사용할 엔티티이다.
편지를 작성하는데 필요한 데이터들을 저장한다. 추 후, 편지를 전송하는 상대방 Id 등이 추가 될 것으로 예상된다.
룸DB를 사용하기 위한 기본 세팅은 끝이났다.
이제, 클린아키텍처 관점으로 Di, Mapper, 로직을 위주로 살펴보자
DatabaseModule
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides
@Singleton
fun provideSlowMailBoxDatabase(
@ApplicationContext context: Context
): SlowMailBoxDatabase = Room
.databaseBuilder(context, SlowMailBoxDatabase::class.java, "slowMail.db")
.build()
@Provides
@Singleton
fun provideSlowMailBoxDao(slowMailBoxDatabase: SlowMailBoxDatabase): SlowMailBoxDao =
slowMailBoxDatabase.getSlowMailBoxDao()
}
Database와 관련된, Dao와 Database를 주입해주는 DI Module이다.
Singleton으로 제공한다.
Room 데이터베이스를 생성하는 과정은 ApplicationContext를 가지고, Room의 databaseBuilder 메서드를 활용하여 생성한다.
DataSource 인터페이스
interface SlowMailBoxDataSource {
...
suspend fun saveTempSlowMail(
additionalMailIfoDto: AdditionalMailIfoDto
): Result<Long>
suspend fun getAllTempSavedMails(spaceId: Long): Result<LocalSlowMailInfoList>
}
repository 전달할 내용들을 담은 인터페이스이다.
실제 작동시킬 함수를 선언해두었다. 리턴값을 보면 Result로 리턴하도록 선언하였다.
RemoteSource 구현체
class SlowMailBoxRemoteSource @Inject constructor(
private val slowMailBoxService: SlowMailBoxService,
private val slowMailBoxDao: SlowMailBoxDao
) : SlowMailBoxDataSource {
...
override suspend fun saveTempSlowMail(additionalMailIfoDto: AdditionalMailIfoDto): Result<Long> {
return try {
Result.success(slowMailBoxDao.insertSlowMail(additionalMailIfoDto.toLocalModel()))
}catch (e:Exception) {
Result.failure(e)
}
}
override suspend fun getAllTempSavedMails(spaceId: Long): Result<LocalSlowMailInfoList> {
return try {
Result.success(LocalSlowMailInfoList(slowMailBoxDao.getAllSlowMails(spaceId)))
}catch (e:Exception) {
Result.failure(e)
}
}
}
이전에 선언한 DataSource를 구현한 RemoteSource이다.
추가적인 내용은 없으며, dao를 주입받아 사용하며, 원하는 타입으로 변환하기 위한 매핑을 진행한다.
ex) try - catch를 활용하여 Result 타입으로 리턴하기
ex2) .toLocalModel()을 활용하여 repository에서 넘어온 데이터를 로컬DB에 저장할 타입으로 매핑
Mapper
...
fun AdditionalMailIfoDto.toLocalModel(): LocalSlowMailInfo {
return LocalSlowMailInfo(
id = null,
spaceId = this.spaceId,
title = this.title,
content = this.content,
createAt = LocalDateTime.now().toString(),
paperId = this.paperId,
sendAt = this.sendAt
)
}
fun LocalSlowMailInfo.toAdditionalMailInfo(): AdditionalMailInfo {
return AdditionalMailInfo(
spaceId = this.spaceId ?: 0,
title = this.title ?: "",
content = this.content ?: "",
paperId = this.paperId ?: 0,
sendAt = this.sendAt ?: ""
)
}
fun LocalSlowMailInfoList.toAdditionalMailInfoList(): List<AdditionalMailInfo> {
return this.localSlowMailInfoList.map { it.toAdditionalMailInfo() }
}
...
모든 데이터 타입과 매퍼를 보여주는 것은 시간 낭비이니, 이렇게만 보여주겠다.
Repository 인터페이스
interface SlowBoxRepository {
...
suspend fun saveTempSlowMail(additionalMailInfo: AdditionalMailInfo): Result<Long>
suspend fun getAllTempSavedMails(spaceId: Long): Result<List<AdditionalMailInfo>>
}
DataSource에서 받아온 데이터들을 Domain계층에서 사용하기 위한 Repository 인터페이스이다.
Repository 구현체
class DefaultSlowBoxRepository @Inject constructor(private val slowMailBoxDataSource: SlowMailBoxDataSource) :
SlowBoxRepository {
...
override suspend fun saveTempSlowMail(additionalMailInfo: AdditionalMailInfo): Result<Long> {
return slowMailBoxDataSource.saveTempSlowMail(additionalMailInfo.toDto())
}
override suspend fun getAllTempSavedMails(spaceId: Long): Result<List<AdditionalMailInfo>> {
return slowMailBoxDataSource.getAllTempSavedMails(spaceId).map{it.toAdditionalMailInfoList()}
}
}
repository 구현체이다.
dataSource를 주입받아 사용하며, 리턴 받은 값들을, useCase에서 사용할 수 있는, 형태로 매핑하여 사용
UseCase
class GetAllTempSavedMailsUseCase(private val slowBoxRepository: SlowBoxRepository) {
suspend operator fun invoke(spaceId: Long): Result<List<AdditionalMailInfo>> {
return slowBoxRepository.getAllTempSavedMails(spaceId)
}
}
class SaveTempSlowMailUseCase(private val slowBoxRepository: SlowBoxRepository) {
suspend operator fun invoke(additionalMailInfo: AdditionalMailInfo): Result<Long> {
return slowBoxRepository.saveTempSlowMail(additionalMailInfo)
}
}
repository를 주입받아 사용하는 UseCase
해당하는 로직이 있으면 추가 구현해야하나, 현재는 그냥 넘겨주는 형태이다.
ViewModel
fun saveTempMailInfo(title: String, content: String) {
setEditSlowMailBoxUiState(EditSlowMailBoxUiState.Loading)
viewModelScope.launch {
delay(500)
slowMailBoxUseCases.saveTempSlowMailUseCase(
getAdditionalMailInfoUiModel(
title,
content
).toDomainModel()
).onSuccess {
setEditSlowMailBoxUiState(EditSlowMailBoxUiState.SuccessTempSave)
}.onFailure {
Log.e(
"${this::class.simpleName}",
(it as RemoteError).toStringForLog()
)
setEditSlowMailBoxUiState(EditSlowMailBoxUiState.Error((it).toStringForUser()))
}
}
}
useCase를 사용하는 viewModel로서, 임시저장의 경우, title과 content를 가지고 domain에 해당하는 모델로 만들어서 useCase로 전달한다.
이 외에 정보들은, viewModel에서 관리하여, 파라미터로 전달 받지 않고 변환한다.
result를 가지고 Success/Failure에 따라 해당 데이터를 처리한다.
리스트를 불러오는 함수의 경우 Success일 때, UiModel로 매핑하는 과정이 추가로 필요 할 것이다.
delay(500)을 준 이유는, RoomDB에 저장하는 과정이 너무 빨라, Loading Progress가 사용자에게 보이지 않을 정도의 속도라서, 앱이 강제종료 되는 것 처럼 보이기에, 딜레이를 주어 progress bar를 보여주도록 구현하고, 완료되었을 시, 액티비티 종료와 함께, Toast를 띄우도록 구현하였다.
Activity
private fun confirmTempSave() {
ConfirmTempSaveDialog.newInstance(
positiveCallback = {
viewModel.saveTempMailInfo(
binding.etTitle.text.toString(),
binding.viewRecordCard.etContent.text.toString()
)
}
).apply {
show(supportFragmentManager, ConfirmTempSaveDialog.CONFIRM_TEMP_SAVE_DIALOG)
}
}
임시 저장을 진행할 액티비티이다.
다이얼로그를 띄우고, 저장 버튼을 클릭 시, title과 content를 viewModel에 보내도록 한다.
다이얼로그에서 버튼을 선택했을 때, 인터페이스를 통해 사용하는 방식도 있으나, 컴포즈를 사용하면서 활용한 () -> Unit 형태의 리스너가 편하여 이렇게 작성하였다.
결과
Database Inspector
원하는 내용이 잘 저장된 것을 볼 수 있다.
후기
RoomDB를 활용하여 데이터 저장을 여러번 해본 적이 있어서 금방 했던 것 같다. 총 소요시간 4시간 정도?
Result타입이 되지 않는다는 점과, List타입은 되나, List타입이 들어있는 객체는 안 된다는 것을 깨닫는데에 오래 걸렸던 것 같다. DI를 사용하니.. Impl이 제대로 생성되지 않았다는 에러 메세지만 보였기 때문이었다.
클린아키텍처 구조에서도 사용해 본적이 있으나, 확실히 하고 넘어 간 것이 아닌 것 같아 RoomDB에 대해서 안 그래도 정리하는 게시글을 작성해야 겠다고 생각하였었다.
기회가 되어 이렇게 작성하게 되었으며, 다음 업데이트를 진행할 때 migration이 필요할 것 같으니, 추 후 그것도 포함하여 업로드 할 예정이다.
'안드로이드 개발 > 클린아키텍처' 카테고리의 다른 글
[Android] 클린 아키텍처 회사 프로젝트에 적용하기 (0) | 2024.03.26 |
---|---|
[Android] 클린아키텍처(CleanArchitecture) 개념 (0) | 2024.02.27 |