시배's Android

Android | The Real Modularization in Android 본문

Android/Android

Android | The Real Modularization in Android

si8ae 2023. 9. 23. 20:31
 

The “Real” Clean Architecture in Android: Modularization

An overview of the principles behind modularization

betterprogramming.pub

“If the SOLID principles tell us how to arrange the bricks into walls and rooms, then the component principles tell us how to arrange the rooms into buildings.” ~ Robert C. Martin, Clean Architecture
레이어별로 패키징해야 하나요, 아니면 피쳐별로 패키징해야 하나요? 다른 접근 방식이 있나요?
프로젝트 컴파일 시간을 단축하려면 어떻게 해야 하나요?
엔지니어가 여러 부서로 구성된 팀에서 독립적으로 작업하려면 어떻게 해야 할까요?

What is "good modularization"?

'좋은 모듈화' 모듈의 응집력이 높고 결합력이 낮은 구성 요소 구조를 말합니다.

모듈의 응집력이 높다고 어떻게 말할 수 있을까요?
그리고 모듈의 결합력이 낮다고 어떻게 말할 수 있을까요?

모듈은 구성 요소 응집력 원칙을 준수할 때 응집력이 높습니다.

모듈은 구성 요소 결합 원칙을 준수할 때 결합력이 낮습니다.

Component Cohesion Principles

SOLID 원칙은 클린 아키텍처의 기초이며 모듈 수준에서 조정할 수 있으므로 새로운 원칙(REP, CCP, CRP)을 만들 수 있습니다.

The Common Closure Principle (CCP)

“Gather into components those classes that change for the same reasons and at the same times.
Separate into different components those classes that change at different times and for different reasons.” — All remaining quotes by Robert C. Martin, Clean Architecture

CCP는 이전 글에서 설명한 것처럼 모듈 수준에서 SRP가 진화한 것입니다.

클래스는 다른 이유로 변경되어서는 안 된다 -> 컴포넌트는 다른 이유로 변경되어서는 안 된다.

같은 이유로 변경되는 클래스는 컴포넌트 안에 그룹화해야 하고, 다른 이유로 변경되는 클래스는 컴포넌트에서 분리해야 합니다.

유지보수성은 재사용성보다 더 중요합니다. 새로운 기능을 개발하거나 요구사항이 변경될 때마다 여러 모듈을 건드리는 것보다 하나의 모듈만 건드리는 것이 좋습니다.

단일 모듈만 변경하면 다른 팀원에게 영향을 미칠 가능성이 적고 다시 컴파일, 재검증 및 재배포해야 할 컴포넌트 수가 줄어듭니다.

가능한 모든 변경 사항을 항상 단일 모듈에 그룹화하는 것은 현실적으로 불가능하므로(모놀리스로 작업하는 경우가 아니라면) 변경이 필요한 모듈의 수를 최소화하는 것이 원칙의 목표입니다.

  • 장점: 변경의 영향이 최소화되므로 유지 관리에 최적입니다.
  • 단점: 모듈을 개발하고 유지 관리하는 데 최적의 접근 방식이 라이브러리 사용자에게 모듈을 릴리스하는 데는 최적이 아닐 수 있습니다. 또한 변경할 모듈의 수를 분리하기 위해 모듈의 크기가 커지는 경향이 있습니다.

The Common Reuse Principle (CRP)

“Don’t force users of a component to depend on things they don’t need.”

이전 글에서 설명한 것처럼 CRP는 모듈 수준에서 ISP가 진화한 것입니다.

인터페이스가 작으면 필요하지 않은 메서드에 의존하지 않고 -> 모듈이 작으면 필요하지 않은 파일에 의존하지 않습니다.

클래스는 단독으로 재사용되는 경우는 거의 없습니다. 일반적으로 재사용 가능한 클래스는 재사용 가능한 추상화의 일부인 다른 클래스와 협업합니다.

CRP는 이러한 클래스가 동일한 컴포넌트에 함께 속한다고 명시합니다.

또한 함께 재사용되지 않는 클래스는 동일한 컴포넌트에 배치해서는 안 된다고 명시하고 있습니다.

이렇게 하면 이러한 클래스에 대한 업데이트가 해당 클래스를 사용하지 않는 모듈의 재컴파일, 재배포 또는 릴리스를 트리거하지 않습니다.

  • 장점: 모듈이 작을수록 모듈 사용자는 신경 쓰지 않는 변경 사항의 영향을 덜 받을 수 있습니다.
  • 단점: 개발 중에 작업해야 하는 모듈이 더 많아집니다.

The Reuse/Release Equivalency Principle (REP)

“The granule of reuse is the granule of release.”

재사용할 의향이 있는 가장 작은 물건이 바로 공개할 의향이 있는 가장 작은 물건입니다.

이는 라이브러리 개발자에게 매우 중요한 원칙입니다.

다른 사람이 컴포넌트를 사용할 수 있도록 하려면 릴리스 프로세스가 필요하며, 시간이 지나도 라이브러리 사용자의 코드를 손상시키지 않으려면 릴리스 번호가 있어야 합니다.

이렇게 하면 라이브러리 사용자가 최신 라이브러리 버전으로 업그레이드하지 않는 한 변경 사항이 적용되지 않습니다.

모듈의 모든 클래스는 동일한 릴리스 번호를 가지므로 단일 클래스를 업데이트하려면 동일한 모듈 아래의 모든 클래스를 새로 릴리스해야 합니다.

라이브러리는 단일 라이브러리로 제공되는 경우도 있고 라이브러리 그룹으로 제공되는 경우도 있습니다(따라서 가져올 항목과 제외할 항목을 결정할 수 있습니다).

라이브러리 그룹으로 작업할 때는 이러한 모든 모듈이 함께 재사용되므로 호환성을 보장하기 위해 모두 동일한 릴리스 번호를 가져야 한다고 예상할 수 있습니다.

릴리스 번호가 같다는 것은 모듈을 업데이트해야 다른 모든 모듈도 업데이트된 버전 번호로 릴리스해야 한다는 것을 의미합니다(변경되지 않은 경우에도 마찬가지).

 

Retrofit을 예를 들어보자.

implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'

Retrofit 개발자가 Retrofit 기본 라이브러리에 새로운 기능을 추가하면 지원되는 converter 라이브러리도 이러한 새로운 통합과 호환되도록 만들어야 하므로 모든 라이브러리 모듈의 버전 이름이 변경될 가능성이 높습니다.

특히 같은 라이브러리 그룹의 모듈이 일관성이 없는 경우에는 이 방법이 항상 최선의 방법은 아닐 수 있습니다.

 

이제 Firebase를 예로 들어 보겠습니다.

과거에는 Firebase 라이브러리의 릴리스 번호가 일치했습니다.

Firebase의 문제점은 라이브러리 집합이 매우 일관성이 없다는 것입니다.

원격 구성 라이브러리와 스토리지 라이브러리를 생각해 보세요. 이 두 라이브러리는 서로 완전히 독립적이며 아마도 서로 다른 개발팀에서 작업했을 것입니다.

두 라이브러리 중 하나에 새로운 통합 기능을 추가한다고 해서 다른 라이브러리의 새 버전을 변경 없이 출시할 필요는 없습니다.

Firebase 팀은 결국 어떻게 되었을까요?

이들은 서로 다른 라이브러리 버전을 단일 버전으로 관리할 수 있는 Maven BoM에 대한 Gradle 5.0 지원을 활용했습니다.

이렇게 함으로써 모든 라이브러리의 버전을 릴리스하는 대신 버전의 BoM 릴리스했습니다.

// BoM
 implementation platform('com.google.firebase:firebase-bom:$version')

 // modules import without version
 implementation 'com.google.firebase:firebase-core'
 implementation 'com.google.firebase:firebase-config'
 implementation 'com.google.firebase:firebase-storage'

BoM이 아니었다면 구글 플레이 서비스의 접근 방식, 즉 끝없는 라이브러리 버전 테이블(가장 사용자 친화적이지 않은 방식)을 사용했을 것입니다.

  • 장점: 재사용성에 최적화되어 있고, 다른 팀에서 모듈을 사용할 수 있으며, 버전 관리로 새 업데이트를 더 쉽게 관리할 수 있습니다.
  • 단점: 모듈의 릴리스 프로세스를 고려해야 하므로 코드베이스 유지 관리가 더 복잡해집니다.

릴리스해야 하는 모듈의 수를 줄일 있으므로 모듈이 커지는 경향이 있습니다.

The Component Cohesion Tension Triangle

이 다이어그램은 다른 두 가지 원칙을 위해 한 가지 원칙을 포기할 때 어떤 일이 발생하는지 보여줍니다.

아직 명확하지 않은 경우, 컴포넌트 응집력 원칙은 SOLID 원칙과 달리 서로 보완하지 않으며 프로젝트에 더 중요한 것을 선택해야 한다는 점을 기억하세요.

클래스를 유지 관리하고 재사용하기 쉽게 만드는 것은 쉽지만 모듈도 마찬가지입니다.

CCP와 REP는 포괄적인 원칙으로 모듈을 더 크게 만드는 경향이 있는 반면, CRP는 배타적인 원칙으로 모듈을 더 작게 만드는 경향이 있습니다.

CRP와 REP는 재사용에 중점을 둔 원칙입니다. 이들은 모듈을 사용하는 사용자를 위해 모듈을 최적화하는 경향이 있는 반면, CCP는 모듈을 개발하는 사용자를 위해 모듈을 최적화하는 경향이 있으므로 유지 관리에 중점을 둡니다.

이 세 가지 원칙의 균형을 모두 맞출 가능성은 거의 없으므로 어느 한 가지를 포기하거나 그 중 하나에 덜 집중할 준비를 해야 합니다.

일반적으로 프로젝트는 다음 범주 중 하나에 속합니다:

  • 앱: 앱을 빌드할 때 주요 목표는 빠르게 빌드하고 불필요한 재컴파일을 최소화하면서 빠르게 컴파일되는 프로젝트를 만드는 것입니다. 이 범주에 속하는 경우 항상 CCP와 CRP에 집중해야 합니다.
  • 라이브러리: 라이브러리를 빌드할 때는 목표가 고정되어 있지 않습니다. 오히려 시간이 지남에 따라 변할 것입니다. 라이브러리 개발을 시작할 때는 라이브러리를 빠르게 구축하는 데 중점을 두어야 합니다. 그런 다음 시간이 지남에 따라 라이브러리를 얼마나 재사용할 수 있는지에 초점을 맞추고 유지 관리에 타협해야 합니다.

이 범주에 속한다면 프로젝트가 성숙할 때까지는 삼각형의 오른쪽에 집중하고, 시간이 지남에 따라 라이브러리 사용자에 대한 책임이 점점 더 커지기 때문에 왼쪽으로 이동해야 합니다.

대부분의 프로젝트 모듈화가 실패하는 이유는 엔지니어가 프로젝트의 성격에 맞지 않는 원칙을 우선시하기 때문입니다.

다른 프로젝트의 모듈화가 실패하는 이유는 컴포넌트 구조가 요구 사항 변화에 따라 진화하지 않고 정적이기 때문입니다.

Components Coupling Principles

응집력 원칙에 따라 모듈이 어떻게 구성되어야 하는지에 대한 이론을 논의했습니다.

이제 이러한 모듈 간의 관계가 어떻게 되어야 하는지에 대해 논의해야 합니다.

The Acyclic Dependencies Principle (ADP)

 

A가 B에 의존하는 경우 B는 A에 의존해서는 안 됩니다.

이는 종속성뿐만 아니라 전이적 종속성에도 해당됩니다:

A가 B에 종속되어 있고 B가 C에 종속되어 있다면 C도 A에 종속되어서는 안 됩니다.

일부 컴파일러는 모듈의 순환을 허용하고, 다른 컴파일러는 이런 일이 발생하지 않도록 노력합니다.

사용하는 컴파일러나 언어에 관계없이 개발자는 종속성 주기를 감지하는 즉시 이를 끊는 방법을 알고 있어야 합니다.

종속성 주기는 두 가지 방법으로 끊을 수 있습니다:

  • 새 모듈에서 재사용할 클래스를 추출하는 방법.
  • 종속성 반전 원칙(DIP, 예, 다시 SOLID )을 사용하여 종속성을 "반전"시키는 방법.

솔루션 1은 많은 모듈에 공유 로직이 필요하고 많은 모듈을 공유해야 하는 경우에 이상적입니다.

솔루션 2 단일 모듈에만 공유 로직이 필요하고 공유할 내용이 많지 않은 경우에 이상적입니다.

The Stable Dependencies Principle (SDP)

모듈을 무엇에 의존하게 만들고 싶으신가요?
자주 변경되는 모듈인가요, 아니면 절대 변경되지 않는 모듈인가요?

우리는 모듈이 절대 변경되지 않는 모듈에 의존하기를 원합니다.

모듈의 종속성이 변경될 때마다 모듈을 다시 컴파일해야 하며, 변경 사항을 처리해야 수도 있습니다.

어떤 모듈이 안정적인가요?

안정적인 모듈은 변경하기 어려운 모듈입니다.

Kotlin String 클래스를 생각해 보세요. Kotlin 팀이 이러한 클래스를 변경할 가능성이 얼마나 될까요? 만약 변경한다면 전체 Kotlin 언어에 얼마나 많은 변경 사항이 생길까요? 제정신인 개발자라면 절대 변경하지 않을 클래스입니다.

가장 이상적인 시나리오는 모듈이 이와 같이 안정적인 것에 의존하는 것입니다.

안타깝게도 우리는 이상적인 세계가 아닌 현실 세계에 살고 있습니다.

우리가 사용하는 대부분의 모듈은 100% 안정적이지 않지만, 이것이 반드시 나쁜 것은 아닙니다.

변화할 수 없는 모듈은 개선될 수도 없습니다.

뿐만 아니라 모듈이 전혀 변경되지 않으면 코드를 변경할 없기 때문에 새로운 기능을 추가할 수도 없습니다.

그렇다면 안정성을 어떻게 재정의할까요?
모듈은 언제 충분히 안정적일까요?

모듈은 종속성이 적고 종속 모듈이 많을 때 안정적이며, 따라서 책임감 있는 모듈이 됩니다.

컴포넌트 종속성 그래프를 보면 하단에 더 안정적인(책임 있는) 모듈이 있고 상단에 더 불안정한(종속적인) 모듈이 표시됩니다.

프로젝트에는 안정적인 모듈과 불안정한 모듈이 모두 존재하므로 모듈은 자신보다 안정적인 모듈에 종속되어야 한다는 것이 황금률입니다.

The Stable Abstractions Principle (SAP)

SDP는 안정 모듈을 "변경"하기 어렵다고 정의합니다.

이는 기존 코드를 쉽게 수정할 수 없기 때문에 안정적인 모듈에 새로운 기능을 추가하는 것이 어렵다는 것을 의미합니다...

그렇다면 "확장"은 어떨까요? 안정 모듈을 확장할 수 있나요?

개방형-폐쇄형 원칙(OCP, , 다시 SOLID ) 따라 확장에는 개방적이고 수정에는 폐쇄적인 클래스를 제공합니다.

 

모듈은 추상적일 때 확장하기 쉽습니다. 따라서 모듈은 주로 인터페이스와 추상 클래스로 구성됩니다.

모듈이 인터페이스로 가득 차 있으면 새로운 것을 추가해야 할 때마다 추상화 중 하나에 새로운 구체적인 구현을 제공하기만 하면 됩니다.

이렇게 하면 모듈에 무언가를 적용하기 위해 안정적인 모듈의 소스 코드를 건드려 다른 종속 모듈을 손상시킬 가능성을 방지할 수 있습니다.

안정적인 모듈은 더 많은 유연성을 허용하기 위해 구체적이기보다는 추상적이어야 하지만, 불안정한 모듈은 코드 변경을 용이하게 하기 위해 추상적이기보다는 구체적이어야 합니다.

안정된 모듈은 유연성을 위해 매우 추상적이어야 하지만, 100% 추상적인 모듈은 재사용할 수 있는 실제 로직이 없기 때문에 쓸모없는 모듈입니다.

그리고 당연히 100% 구체적인 모듈은 변경하기가 매우 고통스러운 모듈입니다.

여기서 황금률은 모듈은 구체성보다는 의존성의 추상성에 의존해야 한다는 것입니다.

클래스가 의존성 반전 원칙을 준수한다면 원칙을 무료로 얻을 있을 것입니다.

Package Design Solutions

이제 높은 응집력과 낮은 결합을 달성하는 방법을 알았으니, 어떤 접근 방식이 효과가 있고 어떤 접근 방식이 효과가 없는지 논의할 차례입니다.

Package by layer

이 접근 방식은 매우 쉽지만 위에서 언급한 대부분의 원칙을 위반합니다.

새로운 기능을 작업할 때마다 모든 모듈을 수정해야 할 가능성이 높습니다.

이렇게 하면 다른 기능의 코드를 손상시키고, 팀원의 발을 밟고, 새로운 반복 작업 시 전체 종속성 그래프를 다시 컴파일해야 할 수도 있습니다.

또한 모듈은 앱에 있는 모든 기능의 레이어 로직을 포함하므로 매우 커집니다.

클린 아키텍처에 대한 과대 광고가 시작된 이래로 클린 아키텍처 기사를 읽어보셨다면, 대부분의 작성자가 모듈화를 논의할 때 레이어(프레젠테이션-도메인-데이터)가 프로젝트의 모듈 구조를 결정해야 한다고 생각하면서 끊임없이 패키지 바이 레이어 접근 방식을 밀어붙이는 것을 보셨을 겁니다.

이 작가들이 처음부터 클린 아키텍처 책을 읽었다면 이 접근 방식이 가장 반대하는 접근 방식이라는 것을 알았을 것입니다.

이 잘못된 조언이 저를 화나게 하는 이유는 개발자들이 결과적으로 잘못된 모듈화 접근 방식을 사용했기 때문이기도 하지만, 이러한 계층으로 개발 팀을 분리하여 회사를 잘못된 길로 이끌었기 때문이기도 합니다.

이러한 패키징 접근 방식을 지지하는 의견에 대해 여러 번 들었는데, 짧은 대답은 '아니요, 더 좋지 않습니다'입니다.

  • 첫째, 데이터베이스를 변경하는 것은 일상적인 업무의 일부가 아닙니다. 데이터베이스를 변경하는 일은 수년에 걸쳐 발생할 수 있지만 매주 발생하는 일은 절대 아닙니다. 모바일 개발자에게는 매우 드문 작업이라는 것은 말할 것도 없습니다(일부 불운한 개발자는 Sqlite에서 Realm으로 변경했다가 다시 Sqlite에서 Room으로 변경해야 했지만, 이는 수년에 걸쳐 발생했습니다).
  • 둘째, 데이터베이스를 한 번에 교체하는 것은 좋지 않은 생각입니다. 데이터를 기능별로 새 데이터베이스로 마이그레이션하여 점진적으로 마이그레이션을 릴리스하고 발생할 수 있는 잠재적 버그의 수를 제한하는 것이 훨씬 좋습니다.

Package by feature

이 접근 방식은 많은 이점을 제공하며 수십 년 동안 가장 권장되는 접근 방식입니다:

  • 한 기능을 작업할 때 하나의 모듈만 변경하므로 유지 관리에 최적입니다.
  • 프로젝트를 열면 프로젝트가 어떤 기능에 대해 비명을 지르기 때문에 프로젝트의 기능을 정확히 알 수 있습니다(Screaming Architecture).
  • 각 cross-functional 팀은 다른 팀의 발을 밟지 않고 기능에 대해 독립적으로 작업할 수 있습니다.
  • 독립적인 팀은 또한 독립적인 모듈을 의미하므로 변경된 단일 기능만 다시 컴파일할 필요 없이 전체 컴파일 시간을 단축할 수 있는 Gradle 병렬 컴파일을 최대한 활용할 수 있습니다.
  • 레이어는 기능 모듈 내에서 패키지로 쉽게 구현할 수 있으므로 레이어를 잃지 않습니다.

이 접근 방식의 단점은 비용이 매우 많이 든다는 것입니다:

  • 기능 모듈에서 다른 기능 모듈의 일부 코드를 재사용해야 하는 경우 매우 불안정한 모듈에 종속성을 만들어야 하며, 기능에는 많은 UI 코드가 포함되고 UI 코드는 매우 구체적이기 때문에 SDP를 깨뜨릴 수 있다는 점과 SAP도 깨뜨릴 수 있다는 점입니다.
  • 기능 모듈에는 프레젠테이션, 도메인 및 데이터 로직이 포함되어 있어 자주 변경되는 코드(UI)와 거의 변경되지 않는 코드(비즈니스 로직)가 포함된 큰 모듈(CRP 위반)이 생성됩니다.
  • 기능에 따라 큰 "핵심 기능" 모듈이 생성되어 프로젝트가 점차 하나의 모놀리스로 되돌아가는 경우가 많습니다.

기능별 패키지는 백엔드 또는 오래된 프런트엔드 애플리케이션과 같이 UI가 많지 않은 프로젝트에서 잘 작동합니다.

백엔드 프로젝트에서 컨트롤러의 코드(프레젠테이션 계층)는 일반적으로 얇고 도메인 계층의 UseCase 또는 서비스와 일치합니다.

또한 백엔드는 모듈이 아닌 서비스(또는 마이크로서비스)에 의존할 수 있으므로 구성 요소 내의 통신을 위해 서비스가 다른 서비스의 내부 구조를 알 필요가 없습니다. 대신, 공용 API의 컨트랙트는 컴파일 시 전체 컴포넌트의 구조를 독립적으로 만듭니다.

모바일 또는 웹 프론트엔드 프로젝트에서 "화면"은 기능의 클러스터입니다.

제품을 장바구니에 추가하고 사용자의 위시리스트에 추가할 수 있는 이커머스 제품 상세 페이지를 생각해 보세요.

제품 목록 페이지나 카트 페이지 또는 위시리스트 페이지에서 동일한 작업을 수행할 있습니다.

Package by component

기능별 패키지에서 보았던 UI 로직은 확실히 아닙니다.

UseCase는 개발의 지침이 되는 것처럼 모듈화의 지침이 되어야 합니다.

UseCase는 앱이 무엇을 하는지를 알려주며 거의 변경되지 않습니다. 또한 프레젠테이션 레이어에서 볼 수 있는 유일한 아키텍처 구성 요소이기도 합니다.

데이터 계층은 도메인 계층을 지원하기 위해서만 존재합니다. 따라서 도메인 계층을 수정하려면 업데이트된 리포지토리 인터페이스와 호환되도록 데이터 계층을 수정해야 하는 경우가 많습니다.

UseCase를 사용하여 코드베이스를 수직 및 수평으로 분할하면 기능별로 패키지화할 수 없었던 재사용성을 달성할 수 있습니다.

제품 세부 정보 페이지 예제로 돌아가서(다이어그램 확인). 카트 컴포넌트 모듈, 위시리스트 컴포넌트 모듈, PDP UI 모듈이 있다면 이제 카트나 위시리스트 화면을 표시하기 위한 UI 세부 사항에 의존하지 않고 카트 코드와 위시리스트 코드를 모두 재사용할 수 있습니다.

제품 팀에서 위시리스트 화면에 카트에 추가 기능을 도입하기로 결정한 경우 카트 컴포넌트 모듈을 위시리스트 UI 모듈에 종속성으로 추가하고 연결하기만 하면 됩니다.

이제 더 재사용 가능한 접근 방식을 갖게 되었을 뿐만 아니라 자주 변경되는 클래스와 거의 변경되지 않는 클래스를 분리하여 재컴파일 횟수를 최소화할 수 있습니다.

컴포넌트가 여전히 독립적이므로 모듈을 병렬로 컴파일할 있으므로 전체 컴파일 시간이 단축됩니다.

 

컴포넌트 모듈 또는 UI 모듈 간에 코드를 공유해야 하는 경우 어떻게 하나요?

전문적인 프로젝트에서 작업하는 경우 이러한 문제에 직면할 가능성이 높으며, 해결 방법은 다음과 같습니다:

공유해야 하는 것이 기능의 특정 코드인 경우, 공유 컴포넌트 모듈 또는 공유 UI 모듈에서 재사용해야 하는 부분을 추출할 수 있습니다.

공유해야 하는 코드가 네트워크 요청이나 디자인 시스템을 수행하기 위한 코드와 같은 일반 코드인 경우, 이 모듈은 공개가 아닌 프로젝트의 비공개 모듈이라는 차이점을 제외하고는 타사 라이브러리(예: Retrofit, Dagger...)에 항상 사용하는 것과 동일한 접근 방식을 따릅니다(공개로 공유하기로 결정할 때까지).

PDP에서 장바구니 화면이나 위시리스트 화면으로 이동해야 하는 경우 어떻게 하나요?

DIP 여러분의 친구입니다. 모듈을 탐색해야 하는 경우 인터페이스만 있으면 됩니다:

interface PDPNavigator {
    // you can adapt for fragments, navigation component, compose....
    fun navigateToCart(activity: Activity) 
    fun navigateToWishlist(activity: Activity)
}
이는 메인(앱) 모듈의 클래스에 의해 구현됩니다:
class AppNavigator: PDPNavigator, WishlistNavigator, CartNavigator.... {
    override fun navigateToCart(activity: Activity) {
        //...
    }

    override fun navigateToWishlist(activity: Activity) {
        //...
    }

}

Encapsulation

모듈의 코드 중 하나를 열어보세요. 모든 클래스나 인터페이스가 공개되어 있다면 캡슐화를 잘못하고 있는 것입니다.

"공용"은 모듈 외부에서 사용하려는 클래스나 인터페이스에만 사용해야 하는 수정자입니다. 그 외의 모든 것은 "내부"여야 합니다.

모듈 내부의 모든 클래스나 인터페이스가 "공용"인 경우 개발자는 다른 모듈에서 모든 것을 재사용해야 한다고 오해하여 코드를 건드리는 것을 두려워하게 될 수 있습니다.

좀 더 훈련된 개발자는 자신감을 얻기 위해 IDE의 사용처 찾기 도구를 사용하여 이러한 클래스가 모듈 외부에서 사용되는지 확인합니다.

이러한 추가 단계를 피하고 개발자의 생산성을 높일 수 있는 수정자가 있었다면 좋았을 텐데요.

모듈 캡슐화는 신뢰성뿐만 아니라 향후 개선 사항도 고려해야 합니다.

무엇이 공개되고 무엇이 공개되지 않는지 알면, 적은 수의 클래스/인터페이스로 인해 모듈을 사용하는 경우 모듈을 종속성에서 분리할 수 있는 방법을 찾을 수 있습니다.

공개/내부 수정자로 끝나는 것이 아닙니다.

캡슐화에서 발견할 수 있는 또 다른 문제는 전이 종속성의 노출과 관련이 있습니다.

이는 종속성 중 하나가 구현 대신 API를 사용하여 전이 종속성을 누출하는 경우에 발생합니다.

이상적으로는 항상 구현을 사용하는 것이 종속성 누출과 추가 컴파일 시간을 피할 있습니다.

Encapsulation with package by component

Packabe by Component에서는 공개할 수 있는 파일 수가 몇 개에 불과하므로 캡슐화 작업이 매우 간단합니다.

  • 컴포넌트 모듈에서: 공개해야 하는 파일은 UseCase 인터페이스와 모듈 외부에서 사용해야 하는 모델뿐입니다. usecase 구현, 리포지토리 인터페이스, 리포지토리 구현, 매퍼 인터페이스, 매퍼 구현, DTO 등은 프레젠테이션 계층이 액세스해서는 안 되므로 항상 내부에 있어야 합니다.
  • 컴포넌트 모듈은 비즈니스 규칙과 UseCase를 포함하여 안정적입니다(SDP 준수). UseCase 인터페이스만 공개함으로써 다른 모듈은 이 모듈의 추상화에만 의존하게 되므로 SAP도 준수합니다.
  • UI 모듈에서: 공개해야 하는 파일은 화면(fragment, activity, composable)과 외부 내비게이터뿐입니다. UI 모듈은 매우 불안정하므로 종속성으로 추가하지 말고 메인(앱) 모듈에서만 임포트하여 내비게이션을 연결해야 합니다.

Dagger으로 작업하는 경우 이러한 종속성을 제공하는 모듈도 내부에 만들어야 합니다.

// Dagger module for a Wishlist Component Module
@Module
@InstallIn(SingletonComponent::class) // Or any other scope
internal object WishlistComponentModule {

    @Provides
    fun provideAddToWishlistUseCase(
        addToWishlistUseCaseImpl: AddToWishlistUseCaseImpl
    ): AddToWishlistUseCase = addToWishlistUseCaseImpl

    @Provides
    fun provideGetWishlistUseCase(
        getWishlistUseCaseImpl: GetWishlistUseCaseImpl
    ): GetWishlistUseCase = getWishlistUseCaseImpl

    @Provides
    fun provideWishlistRepository(
        wishlistRepositoryImpl: WishlistRepositoryImpl
    ): WishlistRepository = wishlistRepositoryImpl

    //...

}

// Dagger module for a Wishlist UI Module
@Module
@InstallIn(ActivityComponent::class) // Or any other scope
internal object WishlistUIModule {

    @Provides
    fun provideSomeDependency(
        someDependencyImpl: SomeDependencyImpl
    ): SomeDependency = someDependencyImpl

    //...

}