Clean Architecture (클린 아키텍처)
클린 아키텍처는 계층을 크게 나누어서 각 분리된 클래스가 한 가지 역할만 수행하도록 구현하는 방식입니다.
계층 구조를 보면, 외부에서 내부로 의존성을 가지고 있기 때문에 내부로 갈수록 의존성을 낮아지게 됩니다.
즉, 어떠한 동작을 할 때 자기보다 내부에 있는 계층이 변화하면 동작을 행하는 계층에도 영향이 있을 수도 있지만, 자신의 외부에 있는 계층이 변화하는 것 때문에 동작을 행하는 계층에 영향이 있어서는 안 됩니다.
- 쉽게 패키지 구조 탐색이 가능해집니다.
- 프로젝트의 유지 보수가 편리해집니다.
- 새로운 기능을 추가할 때, 안정적으로 빠르게 적용이 가능합니다.
- 테스트가 용이합니다.
클린 아키텍처의 구조
- 프레젠테이션 계층 (Presentation Layer)
- 뷰 (View) : 직접적으로 플랫폼 의존적인 구현, 즉 UI 화면 표시와 사용자 입력을 담당합니다. 단순하게 프레젠터가 명령하는 일만 수행합니다.
- 프레젠터 (Presenter) : MVVM의 ViewModel과 같이, 사용자 입력이 왔을 때 어떤 반응을 해야 하는지에 대한 판단을 하는 영역입니다. 무엇을 그려야 할지도 알고 있는 영역입니다.
- 도메인 계층 (Domain Layer)
- 유즈 케이스 (Use Case) : 비즈니스 로직이 들어있는 영역입니다.
- 모델 (Model) : 앱의 실질적인 데이터가 들어있는 영역입니다.
- 트랜스레이터 (Translater) : 데이터 계층의 엔티티와 도메인 계층의 모델 사이를 변환해주는 mapper 역할을 수행합니다.
- 데이터 계층 (Data Layer)
- 리포지토리 (Repository) : 유즈 케이스가 필요로 하는 데이터의 저장 및 수정 등의 기능을 제공하는 영역입니다. 데이터 소스를 인터페이스로 참조하여, 로컬 DB와 네트워크 통신을 자유롭게 할 수 있습니다.
- 데이터 소스 (Data Source) : 실제 데이터의 입출력이 실행되는 영역입니다.
- 엔티티 (Entity) : 데이터 소스에서 사용되는 데이터를 정의하는 영역입니다. REST API의 요청/응답을 위한 JSON과 로컬 DB에 저장하기 위한 테이블이 대표적입니다.
도메인 계층의 Model과 데이터 계층의 Entity
이 두 개의 Model은 겉보기에는 똑같아 보이기 때문에 둘의 차이점과 쓰임을 구별하기 어렵습니다.
서버 API를 통해 가져온 모든 데이터를 사용한다면 둘은 서로 같을 수도 있습니다.
그러나, API 응답으로 가져오는 값을 모두 사용하지 않는다면 둘은 서로 다르게 됩니다.
Data Model에서는 서버 API의 응답으로 받는 모든 값을 선언해야 하지만, Domain Model은 필요한 값만 가지도록 하면 됩니다.
또한, Mapper 클래스에서 Data to Domain으로 데이터를 넘길 때 필요한 데이터만 추출하여 넘겨주는 작업을 진행해야 합니다.
// Data Model
data class ContentDto(
val id: Int? = null,
val title: String,
val content: String,
val category: String,
val createdDate: Date? = null,
val likeCount: Int? = null,
val commentCount: Int? = null,
val viewCount: Int? = null,
)
@Entity(tableName = "Content")
data class ContentEntity(
@PrimaryKey(false)
val id: Int,
@ColumnInfo
var title: String,
@ColumnInfo
var content: String,
@ColumnInfo
var category: String,
@ColumnInfo
val createdDate: Date,
@ColumnInfo
val likeCount: Int,
@ColumnInfo
val commentCount: Int,
@ColumnInfo
val viewCount: Int,
) : Serializable
// Domain Model
data class Content(
val id: Int? = null,
val title: String,
val content: String,
) : java.io.Serializable
// Translater
object ContentMapper {
fun ContentDto.toEntity() = ContentEntity(
id = id ?: -1,
title = title,
content = content,
category = category,
likeCount = likeCount ?: 0,
commentCount = commentCount ?: 0,
viewCount = viewCount ?: 0,
createdDate = createdDate ?: Date(),
)
fun Content.toEntity() = ContentEntity(
id = id ?: -1,
title = title,
content = content,
category = "Default",
likeCount = 0,
commentCount = 0,
viewCount = 0,
createdDate = "...",
)
fun ContentEntity.toContent() = Content(
id = id,
title = title,
content = content,
)
...
}
도메인 계층의 Use Case와 Repository
Use Case는 서비스를 사용하고 있는 사용자가 해당 서비스를 통해 하고자 하는 것을 의미합니다.
먼저 Use Case를 사용하면 ViewModel이 어떤 것을 하고자 하는지 직관적으로 파악할 수 있습니다.
ViewModel에서 해당 Use Case를 파라미터로 전달받아 사용하면 바로 파악할 수 있게 됩니다.
이러한 구조는 유지/보수 측면에서도 유용하지만, 협업 과정에서도 매우 효울적으로 작동합니다.
또한, Use Case를 사용함으로써 의존성을 줄일 수 있게 됩니다.
Use Case를 사용하지 않게 되면, ViewModel은 Repository를 전달받아야 합니다.
이때, 전달받은 Repository에서 수정이 이뤄진다면 많은 부분에서 수정이 이뤄져야 할 가능성이 높습니다.
하지만 Use Case를 사용하게 되면, 이를 사용하는 부분에서만 수정하면 되기 때문에 의존성이 줄어듭니다.
Clean Architecture의 목적 중 하나인 요구사항 변경 시 변경의 최소화를 만족하기 위해서도 필요하다고 볼 수 있습니다.
// Repository
interface ContentRepository {
fun loadList(): Flow<List<Content>>
suspend fun insert(item: Content): Boolean
suspend fun update(item: Content): Boolean
suspend fun delete(item: Content): Boolean
}
// Use Case
class ContentUseCase @Inject constructor(
private val contentRepository: ContentRepository
) {
fun loadList() = contentRepository.loadList()
suspend fun insert(item: Content) = contentRepository.insert(item)
suspend fun update(item: Content) = contentRepository.update(item)
suspend fun delete(item: Content) = contentRepository.delete(item)
}
// ViewModel에서의 Use Case 사용
@HiltViewModel
class MainViewModel @Inject constructor(
private val contentUseCase: ContentUseCase
) : ViewModel() {
fun deleteItem(item : Content) {
viewModelScope.launch(Dispatchers.IO) {
contentUseCase.delete(item).also {
...
}
}
}
...
}
여기서 Repository의 구현부인 RepositoryImpl는 데이터 계층에 존재합니다.
데이터 모델은 도메인 계층에 존재하지만, 데이터 자체는 데이터 계층에 존재하기 때문에 RepositoryImpl는 데이터 계층에 존재합니다.
class ContentRepositoryImpl @Inject constructor(
private val contentDao: ContentDao,
private val contentService: ContentService
) : ContentRepository {
override suspend fun delete(item: Content): Boolean {
return try {
item.id?.let { id ->
contentService.deleteItem(id).also {
if (it.success) {
contentDao.delete(item.toEntity())
}
}
}
true
} catch (e: IOException) {
false
}
}
...
}
'안드로이드 > 개념' 카테고리의 다른 글
[Android] Flow (0) | 2023.12.01 |
---|---|
[Android] 코루틴 (0) | 2023.11.27 |
[Android] Rx와 Observable (0) | 2023.11.03 |
[Android] 앱 아키텍처 패턴 (MVC, MVP, MVVM, MVI) (0) | 2023.10.27 |
[Android] 정규표현식 (0) | 2023.10.11 |