도메인 레이어는 UI 레이어와 데이터 레이어 사이에 있는 선택적 레이어입니다.
도메인 레이어는 복잡한 비즈니스 로직이나 여러 ViewModel에서 재사용되는 간단한 비즈니스 로직의 캡슐화를 담당합니다. 모든 앱에 이러한 요구사항이 있는 것은 아니므로 이 레이어는 선택사항입니다. 따라서 복잡성을 처리하거나 재사용성을 선호하는 등 필요한 경우에만 도메인 레이어를 사용해야 합니다.
도메인 레이어는 다음과 같은 이점을 제공합니다.
- 코드 중복을 방지합니다.
- 도메인 레이어 클래스를 사용하는 클래스의 가독성을 개선합니다.
- 앱의 테스트 가능성을 높입니다.
- 책임을 분할하여 대형 클래스를 피할 수 있습니다.
이러한 클래스를 간단하고 가볍게 유지하려면 각 사용 사례에서는 기능 하나만 담당해야 하고 변경 가능한 데이터를 포함해서는 안 됩니다. 대신 UI 레이어 또는 데이터 레이어에서 변경 가능한 데이터를 처리해야 합니다.
이 가이드의 이름 지정 규칙
이 가이드에서 사용 사례의 이름은 각각 담당하고 있는 단일 작업에 따라 지정됩니다. 규칙은 다음과 같습니다.
현재 시제의 동사 + 명사/대상(선택사항) + UseCase.
예를 들면 FormatDateUseCase
, LogOutUserUseCase
, GetLatestNewsWithAuthorsUseCase
, MakeLoginRequestUseCase
가 있습니다.
종속 항목
일반적인 앱 아키텍처에서 사용 사례 클래스는 UI 레이어의 ViewModel과 데이터 레이어의 저장소 사이에 위치합니다. 즉, 사용 사례 클래스는 일반적으로 저장소 클래스에 종속되며, 저장소와 동일한 방법으로 콜백(Java의 경우) 또는 코루틴(Kotlin의 경우)을 사용하여 UI 레이어와 통신합니다. 이에 관한 자세한 내용은 데이터 레이어 페이지를 참고하세요.
예를 들어 뉴스 저장소의 데이터와 작성자 저장소의 데이터를 가져와서 이를 결합하는 사용 사례 클래스가 앱에 있을 수 있습니다.
class GetLatestNewsWithAuthorsUseCase(
private val newsRepository: NewsRepository,
private val authorsRepository: AuthorsRepository
) { /* ... */ }
사용 사례는 재사용 가능한 로직을 포함하기 때문에 다른 사용 사례에 의해 사용될 수도 있습니다. 도메인 레이어에 여러 수준의 사용 사례가 있는 것은 정상입니다. 예를 들어 아래 예에 정의된 사용 사례는 UI 레이어의 여러 클래스가 시간대를 사용하여 화면에 적절한 메시지를 표시하는 경우 FormatDateUseCase
사용 사례를 사용할 수 있습니다.
class GetLatestNewsWithAuthorsUseCase(
private val newsRepository: NewsRepository,
private val authorsRepository: AuthorsRepository,
private val formatDateUseCase: FormatDateUseCase
) { /* ... */ }
<ph type="x-smartling-placeholder">
<ph type="x-smartling-placeholder">Kotlin에서 사용 사례 호출
Kotlin에서 operator
수정자와 함께 invoke()
함수를 정의하여 사용 사례 클래스 인스턴스를 함수처럼 호출 가능하게 만들 수 있습니다. 아래 예를 참고하세요.
class FormatDateUseCase(userRepository: UserRepository) {
private val formatter = SimpleDateFormat(
userRepository.getPreferredDateFormat(),
userRepository.getPreferredLocale()
)
operator fun invoke(date: Date): String {
return formatter.format(date)
}
}
이 예에서 FormatDateUseCase
의 invoke()
메서드를 사용하여 클래스 인스턴스를 함수인 것처럼 호출할 수 있습니다. invoke()
메서드는 특정 서명으로 제한되지 않습니다. 매개변수를 개수에 상관없이 취하고 모든 유형을 반환할 수 있습니다. 개발자는 클래스의 서로 다른 서명으로 invoke()
를 오버로드할 수도 있습니다. 위 예의 사용 사례를 호출하는 방법은 다음과 같습니다.
class MyViewModel(formatDateUseCase: FormatDateUseCase) : ViewModel() {
init {
val today = Calendar.getInstance()
val todaysDate = formatDateUseCase(today)
/* ... */
}
}
invoke()
연산자에 관해 자세히 알아보려면 Kotlin 문서를 참고하세요.
수명 주기
사용 사례는 고유한 수명 주기를 갖지는 않습니다. 대신 그 사용 사례를 사용하는 클래스의 범위가 적용됩니다. 즉, UI 레이어의 클래스에서, 서비스에서 또는 Application
클래스 자체에서 사용 사례를 호출할 수 있습니다. 사용 사례는 변경 가능한 데이터를 포함해서는 안 되므로 개발자가 사용 사례 클래스의 새 인스턴스를 종속 항목으로 전달할 때마다 그 인스턴스를 만들어야 합니다.
스레딩
도메인 레이어의 사용 사례는 기본 안전성을 갖추어야 합니다. 즉, 기본 스레드에서 안전하게 호출되어야 합니다. 장기 실행 차단 작업을 실행하는 사용 사례 클래스는 관련 로직을 적절한 스레드로 옮기게 됩니다. 그러나 개발자는 이 작업이 이루어지기 전에 계층 구조의 다른 레이어에 이러한 차단 작업이 더 잘 배치될 수 있는지 확인해야 합니다. 일반적으로 복잡한 계산은 재사용이나 캐싱을 유도하기 위해 데이터 레이어에서 이루어집니다. 예를 들어 결과를 캐시하여 앱의 여러 화면에서 재사용해야 하는 경우 대용량 목록을 대상으로 한 리소스 집약적인 작업은 도메인 레이어보다 데이터 레이어에 더 잘 배치됩니다.
다음 예는 백그라운드 스레드에서 작업을 실행하는 사용 사례를 보여줍니다.
class MyUseCase(
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
suspend operator fun invoke(...) = withContext(defaultDispatcher) {
// Long-running blocking operations happen on a background thread.
}
}
일반적인 작업
이 섹션에서는 일반적인 도메인 레이어 작업을 실행하는 방법을 설명합니다.
재사용 가능한 간단한 비즈니스 로직
UI 레이어에 있는 반복 가능한 비즈니스 로직은 사용 사례 클래스에 캡슐화해야 합니다. 그러면 그 로직이 사용된 모든 곳에 변경사항을 더 쉽게 적용할 수 있습니다. 또한 로직을 독립적으로 테스트할 수도 있습니다.
앞에서 설명한 FormatDateUseCase
예를 생각해 보겠습니다. 향후에 날짜 형식과 관련된 비즈니스 요구사항이 변경되는 경우 코드를 중앙의 한 위치에서만 변경하면 됩니다.
저장소 결합
뉴스 앱에는 뉴스와 작성자 데이터 작업을 각각 처리하는 NewsRepository
클래스와 AuthorsRepository
클래스가 있을 수 있습니다. NewsRepository
에서 노출되는 Article
클래스에는 작성자 이름만 포함됩니다. 하지만 개발자가 화면에 자세한 작성자 정보를 표시하고 싶을 수 있습니다. 작성자 정보는 AuthorsRepository
에서 얻을 수 있습니다.
로직은 여러 저장소와 관련되어 있고 복잡해질 수 있으므로 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
목록의 모든 항목을 매핑합니다. 따라서 데이터 레이어가 기본 안전성을 갖추고 있더라도 이 작업은 기본 스레드를 차단하지 않습니다. 데이터 레이어에서 처리되는 항목 수를 알 수 없기 때문입니다. 사용 사례에서 기본 디스패처를 사용하여 백그라운드 스레드로 작업을 옮기는 이유도 바로 여기에 있습니다.
기타 소비자
UI 레이어를 제외하면 도메인 레이어는 서비스 및 Application
클래스와 같은 다른 클래스에서 재사용할 수 있습니다. 또한 TV 또는 Wear와 같은 다른 플랫폼에서 모바일 앱과 코드베이스를 공유하는 경우 UI 레이어는 사용 사례를 재사용하여 앞서 언급한 도메인 레이어의 모든 이점을 얻을 수도 있습니다.
데이터 레이어 액세스 제한
도메인 레이어를 구현할 때 고려해야 할 또 다른 사항은 UI 레이어에서 데이터 레이어에 직접 액세스하도록 허용해야 하는지 아니면 도메인 레이어를 통해 모든 것을 강제 적용해야 하는지 여부입니다.
이렇게 제한하는 이점은 예를 들어 데이터 레이어에 대한 각 액세스 요청과 관련하여 분석 로깅을 실행하는 경우와 같이 UI가 도메인 레이어 로직을 우회하지 않게 된다는 것입니다.
하지만 간과할 수 없는 단점은 데이터 레이어에 대한 단순 함수 호출인 경우에도 사용 사례를 추가해야 하므로 특별한 장점 없이 복잡성이 증가할 수 있다는 것입니다.
따라서 필요한 경우에만 사용 사례를 추가하는 것이 좋습니다. UI 레이어가 거의 독점적으로 사용 사례를 통해 데이터에 액세스하는 것이 확인된다면 이런 방식으로만 데이터에 액세스하는 것이 적합할 수도 있습니다.
데이터 레이어 액세스를 제한하기로 결정하는 것은 개별 코드베이스를 기준으로 정하면 되고, 엄격한 규칙 또는 유연한 접근 방식 중 무엇을 사용할지에 따라 달라집니다.
테스트
도메인 레이어를 테스트할 때는 일반 테스트 가이드가 적용됩니다. 다른 UI 테스트에서 개발자들은 일반적으로 가짜 저장소를 사용합니다. 도메인 레이어를 테스트할 때도 가짜 저장소를 사용하는 것이 좋습니다.
샘플
다음 Google 샘플은 도메인 레이어 사용을 보여줍니다. 이러한 샘플을 살펴 가이드가 실제로 어떻게 적용되는지 살펴보세요.