이동식 저장소

Android Architecture Layers - 2. Domain 본문

Primary/Android

Android Architecture Layers - 2. Domain

해스끼 2022. 6. 18. 14:38

다음 문서를 읽고 정리하였습니다. 영어 원문으로 일독을 권합니다.

 

도메인 레이어  |  Android 개발자  |  Android Developers

도메인 레이어 도메인 레이어는 UI 레이어와 데이터 레이어 사이에 있는 선택적 레이어입니다. 그림 1. 앱 아키텍처에서 도메인 레이어의 역할 도메인 레이어는 복잡한 비즈니스 로직이나 여러 V

developer.android.com


Domain 레이어는 UI와 data 레이어 사이에 존재할 수 있는 레이어이다. 존재할 수 있다는 말은 없어도 된다는 뜻이다.

Domain 레이어는 복잡하거나, 간단하지만 자주 사용되는 비즈니스 로직을 캡슐화한다. Data 레이어와 비즈니스 로직의 구성을 보고, 필요한 경우에만 구현해도 된다.

 

Domain 레이어를 사용하면 다음의 이점이 있다.

  • 코드를 불필요하게 반복하지 않는다.
  • 따라서 Domain 레이어를 사용하는 클래스의 코드가 깔끔해진다.
  • 앱의 테스트 가능성을 높인다.
  • 비즈니스 로직을 쪼개어 클래스를 작게 유지할 수 있다.

Domain 레이어에선 로직을 Use case라는 이름으로 나눈다. 하나의 클래스는 하나의 use case만 처리해야 하며, 데이터를 저장해서는 안 된다. 데이터는 Data 레이어에 맡기자.

이름 짓는 방법

현재형 동사 + 명사(없어도 됨) + UseCase
  • ``FormatDateUseCase``
  • ``LogoutUseCase``
  • ``RequestLoginUseCase``

의존성

일반적으로 use case 클래스는 UI 레이어의 ViewModel과 Data 레이어의 repository 사이에 위치한다. Use case는 repository로부터 받은 데이터를 가공하고, 가공된 데이터를 ViewModel이 가져가는 식이다.

 

뉴스와 작가 repository로부터 받은 데이터를 결합하는 use case를 선언해 보자.

class GetLatestNewsWithAuthorsUseCase(
  private val newsRepository: NewsRepository,
  private val authorsRepository: AuthorsRepository
) { /* ... */ }

Use case가 다른 use case를 포함할 수도 있다. 

class GetLatestNewsWithAuthorsUseCase(
  private val newsRepository: NewsRepository,
  private val authorsRepository: AuthorsRepository,
  private val formatDateUseCase: FormatDateUseCase
) { /* ... */ }

의존성 그래프

Use case 사용하기

``invoke`` 함수를 정의하면 use case를 함수처럼 사용할 수 있다. 어차피 use case는 하나의 기능만을 담당하기 때문에 하나의 함수만 노출해도 된다. 그렇다면 굳이 특정한 이름을 줄 필요가 없고, 그냥 객체를 함수처럼 호출하는 게 제일 간단하다.

class FormatDateUseCase(userRepository: UserRepository) {

    private val formatter = SimpleDateFormat(
        userRepository.getPreferredDateFormat(),
        userRepository.getPreferredLocale()
    )

    operator fun invoke(date: Date): String {
        return formatter.format(date)
    }
}
class MyViewModel(formatDateUseCase: FormatDateUseCase) : ViewModel() {
    init {
        val today = Calendar.getInstance()
        val todaysDate = formatDateUseCase(today)
        /* ... */
    }
}

생명주기?

Use case는 별도의 생명주기를 갖지 않으며, 자신을 사용한 클래스의 생명주기를 따른다. 데이터를 들고 있지 않으므로 아무데서나 객체를 생성해도 된다.

다중 스레드에서

Use case는 메인 스레드를 block하지 않아야 한다. 비동기 작업을 수행해야 한다면 메인을 제외한 다른 스레드에서 실행하는 게 좋다.

 

단, 복잡한 작업의 경우에는 아예 data 레이어로 옮기는 게 나을 수도 있다. 데이터 작업을 data와 domain 두 곳으로 나눌 필요가 없기 때문이다. Domain 레이어를 빼고 전부 data 레이어에 선언해도 딱히 문제는 없다.

Domain에서 하기 좋은 작업

간단한 비즈니스 로직 재사용

비즈니스 로직이 간단하면서도 반복적으로 사용된다면, use case로 선언해 보자. 위에서 봤던 ``FormatDateUseCase``처럼.

 

간단한 로직을 ``Util.kt`` 등의 파일에 모아놓는 경우도 있다. 하지만 이렇게 하면 ``Util`` 파일이 너무 많은 기능을 담당하게 된다. 또, 에러 처리 등 use case의 공통된 기능을 하나의 클래스에 정의해 두면 코드의 재사용성을 높일 수도 있다.

Repository 결합하기

위에서 봤던 ``GetLatestNewsWithAuthorsUseCase``처럼 여러 개의 repository를 묶어야 한다면, use case에서 묶는 게 낫다. 로직이 복잡해질 수도 있기 때문이다.

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

나는 이런 작업을 ViewModel에서 구현했지만, ViewModel은 UI interaction 처리하기도 바쁜데 데이터 결합까지 하기에는 너무 책임이 많지 않은가? 이런 작업은 하위 레이어로 추상화해도 된다.

 

애초에 repository에서 묶어서 넘겨주면 해결될 문제이지만, domain 레이어에서 처리해도 딱히 큰 차이는 없다. 

예시: MyVoca의 HomeViewModel에서 여러 데이터를 combine하는 부분
Comments