아키텍처 가이드 - Domain layer

dwjeong·2023년 12월 1일
0

안드로이드

목록 보기
28/28

🔎 Domain layer

도메인 레이어는 UI 레이어와 data 레이어 사이의 선택적인 레이어.

도메인 레이어는 복잡한 비즈니스 로직 또는 여러 ViewModel에서 재사용되는 간단한 비즈니스 로직을 캡슐화하는 역할을 담당함.
모든 앱에 이러한 요구사항이 있는 것이 아니기 때문에 이 레이어는 선택사항임. 복잡성을 처리하거나 재사용성을 선호하는 등 필요한 경우에만 사용.



⭐️ 도메인 레이어의 이점

  • 코드 중복 방지
  • 도메인 계층 클래스를 사용하는 클래스의 가독성이 향상됨.
  • 앱의 테스트 가능성이 향상됨.
  • 책임을 나누어 클래스가 방대해지는 것을 막음

이런 클래스를 단순하고 가볍게 유지하려면 각 유스케이스는 단일 기능에 대해서만 책임을 져야 하며, 변경 가능한 데이터를 포함해서는 안됨.
대신 UI 또는 데이터 레이어에서 변경 가능한 데이터를 처리해야함.



📖 종속성

일반적인 앱 아키텍처에서 유스케이스 클래스는 UI 레이어의 ViewModel과 데이터 레이어의 리포지토리 사이에 위치함. 즉, 유스케이스 클래스는 일반적으로 리포지토리 클래스에 종속되며, 리포지토리와 동일한 방법으로 콜백 또는 코루틴을 사용하여 UI 레이어와 통신함.


뉴스 리포지토리의 데이터와 Author 리포지토리의 데이터를 가져와서 이를 결합하는 유스케이스 클래스가 앱에 있을 수 있음.

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

유스케이스에는 재사용 가능한 로직이 포함되어 있으므로 다른 유스케이스에서도 사용 가능.

도메인 레이어에서는 여러 수준의 유스케이스가 있는 것이 일반적.

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

위에서 정의된 유스케이스는 UI 레이어의 여러 클래스가 Time zone을 사용하여 화면에 적절한 메시지를 표시하는 경우 FormatDateUseCase 유스케이스를 활용할 수 있음.



📖 코틀린에서 유스케이스 호출

코틀린에서는 연산자 제어자로 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)
        /* ... */
    }
}



📖 생명주기

유스케이스에는 자체 생명 주기가 없음. 대신 이를 사용하는 클래스로 범위가 지정됨.
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 레이어에 있는 반복 가능한 비즈니스 로직을 캡슐화해야 함.
로직이 사용되는 모든 곳에 변경 사항을 더 쉽게 적용할 수 있음.
또한 로직을 별도로 테스트할 수 있음.

⭐ 참고: 유스케이스에 존재할 수 있는 로직이 대신 Util 클래스의 정적 메서드에 포함되는 경우도 있음. 그러나 Util 클래스는 찾기 어렵고 기능도 발견하기 어려운 경우가 종종 있기 때문에 후자는 권장되지 않음.
게다가 유스케이스에서는 기본 클래스의 스레딩 및 오류 처리와 같은 공통 기능을 공유할 수 있으며, 이는 규모가 큰 팀에 도움이 될 수 있음.


📚 리포지토리 결합

뉴스 앱에는 news 및 author 데이터 작업을 각각 처리하는 NewsRepository 및 AuthorsRepository 클래스가 있을 수 있음.

NewsRepository가 노출하는 Article 클래스에는 author의 이름만 포함되어 있지만 author에 대한 추가 정보를 화면에 표시하려고함. author정보는 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
        }
}

로직은 뉴스 리스트의 모든 항목을 매핑함. 따라서 데이터 레이어가 기본 안전성을 갖추고 있더라도 이 작업은 기본 스레드를 차단하지 않음.
데이터 레이어에서 처리되는 항목 수를 알 수 없기 때문.
유스케이스에서 기본 디스패처를 사용하여 백그라운드 스레드로 작업을 옮기는 이유.


⭐ 참고: Room 라이브러리를 사용하면 데이터베이스의 서로 다른 항목 간의 관계를 쿼리할 수 있음. 데이터베이스가 정보 소스라면 모든 작업을 지원하는 쿼리를 만들 수 있음.
이 경우 유스케이스 대신 NewsWithAuthorsRepository 같은 리포지토리 클래스를 만드는 것이 좋음.




📖 기타 소비자

UI 레이어를 제외하면 도메인 레이어는 서비스 및 Application 클래스와 같은 다른 클래스에서 재사용할 수 있음.

또한 TV 또는 Wear와 같은 다른 플랫폼에서 모바일 앱과 코드베이스를 공유하는 경우 UI 레이어는 유스케이스를 재사용하여 앞서 언급한 도메인 레이어의 모든 이점을 얻을 수도 있음.



📖 데이터 레이어 액세스 제한

도메인 레이어를 구현할 때 고려해야 할 또 다른 사항은 UI 레이어에서 데이터 레이어에 직접 액세스하도록 허용해야 하는지 아니면 도메인 레이어를 통해 모든 것을 강제 적용해야 하는지 여부.

이렇게 제한하는 이점은 예를 들어 데이터 레이어에 대한 각 액세스 요청과 관련하여 분석 로깅을 실행하는 경우와 같이 UI가 도메인 레이어 로직을 우회하지 않게 된다는 것.

단점은 데이터 레이어에 대한 단순 함수 호출인 경우에도 사용 사례를 추가해야 하므로 특별한 장점 없이 복잡성이 증가할 수 있다는 것.

따라서 필요한 경우에만 유스케이스를 추가하는 것이 좋음. UI 레이어가 거의 독점적으로 유스케이스를 통해 데이터에 액세스하는 것이 확인된다면 이런 방식으로만 데이터에 액세스하는 것이 적합할 수도 있음.

데이터 레이어 액세스를 제한하기로 결정하는 것은 개별 코드베이스를 기준으로 정하면 되고, 엄격한 규칙 또는 유연한 접근 방식 중 무엇을 사용할지에 따라 달라짐.

0개의 댓글