관리 메뉴

나만을 위한 블로그

[Android] 앱 아키텍처 - 도메인 레이어란? 본문

Android

[Android] 앱 아키텍처 - 도메인 레이어란?

참깨빵위에참깨빵_ 2024. 5. 12. 12:49
728x90
반응형

https://developer.android.com/topic/architecture/domain-layer?hl=ko

 

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

이 페이지는 Cloud Translation API를 통해 번역되었습니다. 도메인 레이어 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 도메인 레이어는 UI 레이어와 데이터 레

developer.android.com

 

도메인 레이어는 UI 레이어, 데이터 레이어 사이의 선택적 레이어다.

 

 

이 레이어는 복잡한 비즈니스 로직, 여러 뷰모델에서 재사용되는 간단한 비즈니스 로직의 캡슐화를 담당한다.

모든 앱에 이런 요구사항이 있는 건 아니라서 이 레이어는 선택사항이다. 따라서 복잡성을 처리하거나 재사용성을 선호하는 등 필요한 경우에만 써야 한다.

도메인 레이어가 제공하는 장점은 아래와 같다.

 

  • 코드 중복 방지
  • 도메인 레이어 클래스를 쓰는 클래스의 가독성 개선
  • 앱의 테스트 가능성 높임
  • 책임을 분할해 대형 클래스를 피할 수 있음

 

이름 지정 규칙

 

이 가이드에서 usecase의 이름은 담당하는 단일 작업에 따라 지정된다. 규칙은 아래와 같다.

 

현재 시제의 동사 + 명사/대상(선택사항) + UseCase

 

예를 들어 FormatDateUseCase, LogOutUserUseCase, GetLatestNewsWithAuthorsUseCase, MakeLoginRequestUseCase 등이 있다.

 

종속 항목

 

일반적인 앱 아키텍처에서 유스케이스 클래스는 UI 레이어의 뷰모델, 데이터 레이어의 레포지토리 사이에 위치한다.

즉 유스케이스 클래스는 일반적으로 레포지토리 클래스에 종속되며 레포지토리와 같은 방법으로 콜백 또는 코루틴을 써서 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
) { /* ... */ }

 

 

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

 

코틀린에서 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)
        /* ... */
    }
}

 

생명주기

 

유스케이스가 고유한 생명주기를 갖지는 않는다. 대신 그 유스케이스를 쓰는 클래스의 범위가 적용된다.

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의 경우 향후 날짜 형식과 관련된 비즈니스 요구사항이 변경되는 경우, 코드를 중앙의 한 위치에서만 바꾸면 된다.

유스케이스에 존재할 수 있는 로직이 대신 Util 클래스의 정적 메서드에 포함된 경우도 있다. 그러나 이 클래스는 찾기 어렵고 기능도 발견하기 어려운 경우가 종종 있어 권장되지 않는다. 그리고 유스케이스에선 기본 클래스의 쓰레딩, 에러 처리 등 공통 기능을 공유할 수 있어서 규모가 큰 팀에 도움이 될 수 있다.

 

레포지토리 결합

 

뉴스 앱에는 뉴스, 작성자 데이터 작업을 각각 처리하는 NewsRepository, AuthorsRepository 클래스가 있을 수 있다.

NewsRepository에서 노출되는 Article 클래스에는 작성자 이름만 포함된다. 하지만 개발자가 화면에 자세한 작성자 정보를 표시하고 싶을 수 있다. 작성자 정보는 AuthorsRepository에서 얻을 수 있다.

 

 

로직은 여러 저장소와 관련돼 있고 복잡해질 수 있으므로 GetLatestNewsWithAuthorsUseCase 클래스를 만들어 뷰모델에서 로직을 추상화하고 가독성을 높일 수 있다.

 

/**
 * 이 UseCase는 최신 뉴스와 관련 작성자를 가져온다
 */
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()
            // 이것은 병렬화되지 않고 UseCase가 선형적으로 느리다
            for (article in news) {
                // Repository는 일시 중단 기능을 노출한다
                val author = authorsRepository.getAuthor(article.authorId)
                result.add(ArticleWithAuthor(article, author))
            }
            result
        }
}

 

로직은 news 리스트의 모든 항목을 매핑한다. 따라서 데이터 레이어가 기본 안전성을 갖췄더라도 이 작업은 메인 쓰레드를 차단하지 않는다. 데이터 레이어에서 처리되는 항목 수를 알 수 없기 때문이다.

유스케이스에서 Default 디스패처를 써서 백그라운드 쓰레드로 작업을 옮기는 이유도 여기 있다.

 

데이터 레이어 액세스 제한

 

도메인 레이어 구현 시 고려해야 하는 다른 사항은 UI 레이어에서 데이터 레이어에 직접 접근하도록 허용해야 하는지, 도메인 레이어를 통해 모든 걸 강제 적용해야 하는지 여부다.

 

 

이렇게 제한할 경우 장단점은 아래와 같다.

 

  • 장점 : 데이터 레이어에 대한 각 접근 요청과 관련해 분석 로깅을 실행하는 경우 같이 UI가 도메인 레이어 로직을 우회하지 않게 됨
  • 단점 : 데이터 레이어에 대한 단순 함수 호출이라도 유스케이스를 추가해야 해서 복잡성이 증가

 

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

데이터 레이어 접근을 제한하기로 결정하는 건 개별 코드베이스 기준으로 정하면 되고, 엄격한 규칙 또는 유연한 접근 방식 중 뭘 쓸지에 따라 달라진다.

반응형
Comments