Domain Layer은 UI Layer와 Data Layer 사이에 있는 Optional Layer이다.
Domain Layer는 복잡한 Business Logic이나 여러 ViewModel에서 재사용되는 간단한 Business Logic의 Encapsulation을 담당한다.
이는 선택적이므로 복잡성을 처리하거나 재사용성을 선호하는 등 필요한 경우에만 Domain Layer를 사용해야 한다.
Domain Layer는 다음과 같은 이점을 제공한다.
1. 코드 중복을 방지한다.
2. Domain Layer Class를 사용하는 Class의 가독성을 개선한다.
3. 앱의 테스트 가능성을 높인다.
4. 책임을 분할하여 대형 클래스를 피할 수 있다.
이러한 클래스를 간단하고 가볍게 유지하려면 각 사용 사례에서는 기능 하나만 담당해야 하고 변경 가능한 데이터를 포함해서는 안된다.
대신 UI Layer 또는 Data Layer에서 변경 가능한 데이터를 처리해야 한다.
캡슐화(Encapsulation)
객체의 속성(data fields)과 행위(methid)를 하나로 묶고, 실제 구현 내용 일부를 외부에 감추어 은닉한다.
이름 지정 규칙
현재 시제의 동사 + 명사/대상(선택사항) + UseCase
종속 항목
일반적인 앱 아키텍처에서 사용 사례 클래스는 UI Layer의 ViewModel과 Data Layer의 Repository 사이에 위치한다. 즉, UseCase 클래스는 일반적으로 Repository 클래스에 종속되며, Repository와 동일한 방법으로 콜백 또는 코루틴을 사용하여 UI Layer와 통신한다.
Ex) 뉴스 저장소의 데이터와 작성자 저장소의 데이터를 가져와서 이를 결합하는 UseCase 클래스가 앱에 있을 수 있다.
class GetLatestNewsWithAuthorsUseCase(
private val newsRepository: NewsRepository,
private val authorsRepository: AuthorsRepository
) { /* ... */ }
UseCase는 재사용 가능한 로직을 포함하기 때문에 다른 UseCase에 의해 사용될 수 있다. Domain Layer에 여러 수준의 UseCase가 있는 것은 정상이다.
Ex) 아래 정의된 사용 사례는 UI Layer의 여러 클래스가 시간대를 사용하여 화면에 적절한 메시지를 표시하는 경우 FormatDateUseCase 사용 사례를 사용할 수 있다.
class GetLatestNewsWithAuthorsUseCase(
private val newsRepository: NewsRepository,
private val authorsRepository: AuthorsRepository,
private val formatDateUseCase: FormatDateUseCase
) { /* ... */ }
Kotlin에서 UseCase 호출
Kotlin에서 operator constructor와 함께 invoke() 함수를 정의하여 UseCase 인스턴스를 함수처럼 호출 가능하게 만들 수 있다.
class FormatDateUseCase(userRepository: UserRepository) {
private val formatter = SimpleDateFormat(
userRepository.getPreferredDateFormat(),
userRepository.getPreferredLocale()
)
operator fun invoke(date: Date): String {
return formatter.format(date)
}
}
이 예에서 FormatDataUseCase의 invoke() 메소드를 사용하여 클래스 인스턴스 함수인것처럼 호출할 수 있다.
invoke() 메소드는 특정 Signature로 제한되지 않는다. 매개변수를 개수에 상관없이 취하고 모든 유형을 반환할 수 있다. 개발자는 클래스의 서로 다른 Signature로 invoke()를 오버로드 할 수 있다.
class MyViewModel(formatDateUseCase: FormatDateUseCase) : ViewModel() {
init {
val today = Calendar.getInstance()
val todaysDate = formatDateUseCase(today)
/* ... */
}
}
LifeCycle
UseCase는 고유한 수명 주기를 갖지 않는다. 대신 그 UseCase를 사용하는 클래스의 범위가 적용된다. 즉, UI Layer 클래스에서, 서비스에서 또는 Application 클래스 자체에서 UseCase를 호출할 수 있다. UseCase는 변경 가능한 데이터를 포함해서는 안 되므로 개발자가 UseCase의 새 인스턴스를 종속 항목으로 전달할 때마다 그 인스턴스를 만들어야 한다.
Threading
Domain Layer의 UseCase는 기본 안정성을 갖추어야 한다. 즉, 기본 스레드에서 안전하게 호출되어야 한다. 장기 실행 차단 작업을 실행하는 UseCase는 관련 로직을 적절한 스레드로 옮기게 된다. 그러나 개발자는 이 작업이 이루어지기 전에 계층 구조에서 이러한 차단 작업이 더 잘 배치되는 다른 레이어가 있는지 확인해야 한다.
일반적으로 복잡한 계산은 재사용이나 캐싱을 유도하기 위해 Data Layer에서 이루어진다.
Ex) 결과를 캐시하여 앱의 여러 화면에서 재사용해야 하는 경우 대용량 목록을 대상으로 한 리소스 집약적인 작업은 도메인 레이어보다 데이터 레이어에 더 잘 배치된다.
class MyUseCase(
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
suspend operator fun invoke(...) = withContext(defaultDispatcher) {
// Long-running blocking operations happen on a background thread.
}
}
상기 예는 백그라운드 스레드에서 작업을 실행하는 사용 사례를 보여준다.
일반적인 작업
일반적인 도메인 레이어 작업을 실행하는 방법을 보자
재사용 가능한 간단한 비즈니스 로직
UI Layer에 있는 반복 가능한 비즈니스 로직은 UseCase 클래스에 캡슐화 해야 한다. 그러면 그 로직이 사용된 모든 곳에 변경사항을 더 쉽게 적용할 수 있다. 또한 로직을 독립적으로 테스트 할 수 있다.
앞서 설명한 FormatDataUseCase 예를 생각해보자. 향후에 날짜 형식과 관련된 비즈니스 요구사항이 변경되는 경우 코드를 중앙의 한 위치에서만 변경하면 된다.
Combine Repository
뉴스 앱에는 뉴스와 작성자 데이터 작업을 각각 처리하는 NewsRepository 클래스와 AuthorRepository 클래스가 있을 수 있다.
NewsRepository에서 노출되는 Article 클래스에는 작성자 이름만 포함된다. 하지만 개발자는 화면에 자세한 작성자 정보를 표시하고자 할 수 있다. 작성자 정보는 AuthorsRepository에서 얻을 수 있다.
로직은 여러 Repository와 관련되어 있고 복잡해질 수 있으므로 GetLatestNewsWithAuthorsUseCase 클래스를 만들어 ViewModel에서 로직을 추상화하고 가독성을 높일 수 있다. 또한 로직을 보다 쉽게 개별적으로 테스트하고 앱의 다른 부분에서 재사용할 수도 있다.
/**
* This use case fetches the latest news and the associated author.
*/
class GetLatestNewsWithAuthorsUseCase(
private val newsRepository: NewsRepository,
private val authorsRepository: AuthorsRepository,
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
suspend operator fun invoke(): List<ArticleWithAuthor> =
withContext(defaultDispatcher) {
val news = newsRepository.fetchLatestNews()
val result: MutableList<ArticleWithAuthor> = mutableListOf()
// This is not parallelized, the use case is linearly slow.
for (article in news) {
// The repository exposes suspend functions
val author = authorsRepository.getAuthor(article.authorId)
result.add(ArticleWithAuthor(article, author))
}
result
}
}
로직은 news 목록의 모든 항목을 매핑한다. 따라서 Data Layer가 기본 안정성을 갖추고 있더라도 이 작업은 기본 스레드를 차단하지 않는다. Data Layer에서 처리되는 항목 수를 알 수 없기 때문이다. UseCase에서 기본 디스패처를 사용하여 백그라운드 스레드로 작업을 옮기는 이유도 여기있다.
'Computer engineering > Android Programming' 카테고리의 다른 글
1. Architecture Component UI Layer Library View Binding (0) | 2022.07.10 |
---|---|
5. Android App Architecture Data Layer (0) | 2022.07.10 |
3. Android App Architecture UI Event (0) | 2022.07.09 |
2. Android App Architecture UI Layer (0) | 2022.07.09 |
1. Android App Architecture 개요 (0) | 2022.07.09 |