이전 포스팅에서 모듈의 정의와 모듈화를 하는 기준에 대해서 소개했다. 기존의 코드를 모듈화를 하는 과정이나, 새롭게 모듈을 구성하여 코드를 배치하는 과정 모두 문제점이 발생할 수 있다. 이번 포스팅에서는 모듈을 구성하면서 발생할 수 있는 문제와 실제 필자에게 발생했던 문제점과 이에 대한 해결책을 소개한다.
1. 순환 참조
서로의 gradle/bazel 모듈이 상호 참조하고 있는 상황이다. 처음 모듈화를 진행할 때 많이 직면할 수 있는 문제이다. 대부분의 빌드 툴은 서로의 모듈을 참조하는 것을 막고 있다.
해결책
- 각 모듈을 인터페이스와 구현체의 쌍으로 분리하여 각 모듈의 구현체는 서로의 인터페이스 모듈을 참조하도록 수정한다.
- 해당 과정으로 각 모듈은 명시적인 Api를 제공하게 된다.
- 또한 각 모듈은 서로간의 의존성이 끊어져, 추가적인 확장에 있어서 더 용이하다.
2. 거꾸로된 의존성(Inversed Dependency)
개념적으로는 둘 사이 의존성이 없어야 하는데, 여러 개의 의존성이 생길 수 있다. 이러한 의존성 그래프는 직관적이기도, 이해하기도 어렵다. (관계 그래프가 복잡성 증가) 또한 확장에서 문제가 생기고, 의존 모듈의 상태가 바뀔 때마다 의존성이 증가한다.
필자의 경우 Network Module에서 DataStore 모듈을 의존하는 상황이 발생했다. 이는 네트워크 호출을 위한 TokenInterceptor에서 DataStore의 Token에 접근하는 코드에 의해 발생하였다.
해결책
- 의존성 역전의 원칙(추상화된 모듈은 구현 모듈에 의존해서는 안된다)을 적용시킬 수 없는 경우, 구현 클래스의 인터페이스를 만들어 상위 모듈로 옮기는 형태로 의존성의 방향을 반대로 수정한다.
- 해당 과정을 통해 의존성의 방향이 직관적이고, 논리적이게 수정되며, 인터페이스와 Mock 데이터를 통해 독립적인 테스트가 가능하다.
3. 없어야 할 의존성이 존재한다.
fun showInstallMessage(title: String, isUpdate: Boolean, ...){
clickIntent = MainActvitiy.getDownloadIntent(context)
}
모듈화 이전에는 문제가 발생하지 않다가, 모듈화를 하면서 쉽게 발생할 수 있는 상황이다.
- 앱의 메인 페이지의 특정 Intent를 얻기 위해서 mainActivity 메소드를 호출하는 경우.
- TabLayout에서 차상위 프래그먼트의 멤버를 호출하는 경우
위의 예시처럼 시스템이 특정 Activity나 ParentFragment에 의존하는 상황이 발생할 수 있다. 해당 문제는 시스템이 다른 여러가지 책임 소재를 가지고 있음을 의미하며, 단일 책임 원칙을 위반하는 코드이다.
해결책
- 해당 문제는 각 Activity나 Fragment가 더 분리될 수 있음을 의미하며, 해당 문제를 관리하는 매니저를 분리하는 방식으로 해결할 수 있다.
- Intent를 제공하기 위한, IntentManager를 예시로 들 수 있다. IntentManger를 분리하고, Manager의 인터페이스를 참조하며, Impl를 의존성 주입을 통해 제공하는 방법을 통해 해결할 수 있다.
interface AppNavigator {
fun navigateToAuth(): Intent
fun navigateToAccount(): Intent
fun navigateToHome(destination: String = ""): Intent
}
필자의 경우 액티비티 간의 Navigation을 따로 구현체와 인터페이스로 분리하는 방법으로 구현했다. 해당 AppNavigator의 인터페이스만을 참조하며, 구현체의 경우 App 모듈이나 상위 모듈에서 구현하는 방식으로 해결할 수 있다.
유틸리티 클래스
유틸리티는 시스템이 커짐에 따라 끝없이 새로 생길 수 있다. 오래된 프로젝트의 경우 유틸리티의 숫자 뿐만아니라 사이즈도 문제가 된다.
유틸리티 메소드 하나를 호출하면, 유틸리티 클래스 전체에 대한 의존성을 가지게 된다.
범용 유틸리티 특성상 유틸리티가 네트워크 모듈등 여러 곳에 의존성을 가지게 되는 경우가 많고, 이렇게 되면 의존성 그래프가 매우 복잡해질 수 있다.
해결책
- 할 수 있는 한 유틸리티 패턴을 지양한다.
- 대부분의 유틸리티 메소도는 매우 한정된 호출자에서 사용됨.
- 각 호출자를 파악해, 해당 모듈의 internal이나 클래스의 private으로 만든다.
- 큰 유틸리티 클래스를 더 의미있는 작은 단위로 쪼갠다.
- 쪼갠 클래스들은 적절한 모듈로 재배치
- 일반적인 Uitls 모듈은 비즈니스 로직과 무관한 ArrayUtils, ListUtils, Common 중에서 Common으로 구성되도록 한다.
리소스
- 종종 리소스들의 분류와 기능상의 분류가 일치하지 않는 경우가 존재함.
- 리소스가 모듈 A에서만 사용한다 → 리소스를 A로 이동
- 리소스가 A,B 둘다 사용하지만, B가 A를 의존한다 → A로 이동
- 리소스가 모듈 A,B 둘다 사용되지만, 의존관계가 없다. → 논리적으로 A의 리소스가 맞다고 판단되면 A, 모듈 B가 A를 의존하도록 의존성을 추가한다.
- 특정 모듈에 속해야할 개연성이 부족하다면, common으로 이동한다.
결론
이번 다중 모듈에서 발생할 수 있는 문제점을 공부하면서, 직접 다중 모듈 프로젝트에서 구현했던 것들에서 발생하고 있는 문제를 많이 알 수 있었다. 기능과 다양한 조건에 따라서 모듈을 분리하는 것도 중요하지만, 각 시스템과 모듈의 책임을 명확하게 정의하는 것도 중요하다.
참고 문헌
강사룡의 앱 아키텍처 가이드 : 6장 멀티 모듈