Coupang Android Architecture — Part 3

리패키징을 통한 의존성 제거(Reducing dependencies through repackaging)

지난 포스트에서는 관심사 분리(Separation of concerns)에 이어 안드로이드 애플리케이션 모듈화(Modularizing Android Application)에 대하여 알아보았다. 우리는 애플리케이션 모듈화를 통해서 의존성을 제거하여 코드의 안전성과 유지보수성을 향상시켰으며, 다른 모바일 프로젝트에서 분리된 모듈들을 재활용해 생산성을 비약적으로 높였다.

초창기 모바일 애플리케이션 가운데 상당수는 패키징 전략에 많은 시간을 투자하지 않는 경향이 있었다. 많은 클래스들을 하나의 패키지에 넣고 개발을 진행하고 애플리케이션이 점차 성장해가면 액티비티(activity), 프래그먼트(fragment)와 같은 컴포넌트 단위로 패키지를 생성하고 클래스들을 이동하는 경우가 많았다. 이런 작업을 지속적으로 진행하다보면 또 다시 하나의 패키지에 수많은 클래스들이 생기게 되고 이는 유지보수를 진행할 때 커다란 걸림돌이 되기도 한다.

우리는 13개의 코어 모듈(Core Module)을 성공적으로 분리했지만, 하나의 큰 덩어리로 남아있는 피처 모듈 역시 컴포넌트 단위로 분리되어 있는 패키지들이 수없이 존재했다. 이로 인해 패키지간의 의존성이 너무나 복잡한 상태였고, 우리는 이를 해결하기 위해 리패키징(Repackaging) 작업을 진행하기로 결정했다.

이번 포스트에서는 지속적으로 모듈화를 진행하기 위해 패키지의 의존성을 제거하는 과정에 대해 소개하기로 하겠다.

우리는 2가지의 패키징의 구조를 놓고 비교해 보기로 했다.

1.컴포넌트 패키징(Packaging by components)

컴포넌트 패키징은 액티비티, 프래그먼트, 어댑터, 핸들러와 같이 컴포넌트의 단위로 구성하는 것을 의미한다.

com.your.company.activity

com.your.company.fragment

com.your.company.adapter

com.your.company.handler

2.피처 패키징(Packaging by features)

피처 패키징은 각각의 피처단위로 패키징을 구성하는 것으로 액티비티, 프래그먼트, 어댑터 등이 하나의 피처를 중심으로 구성되어 독립적으로 분리할 수 있는 구조를 가진다. 그리고 피처의 크기가 커지면 이를 다시 세분화된 피처단위로 다시 재구성할 수 있다.

com.your.company.feature1.sub-feature1

com.your.company.feature1.sub-feature2

com.your.company.feature1.sub-feature3

com.your.company.feature2.sub-feature1

컴포넌트 패키징과 피처 패키징의 비교

확장성(Scalability): 피처 패키징의 명백한 장점은 확장성이다. 우리는 새로운 피처가 생길 때마다 손쉽게 새로운 패키지를 생성할 수 있다. 하지만 activity와 같은 컴포넌트 패키지를 만들 때 해당 패키지 안에 클래스가 100개가 넘어간다면 개발자들은 원하는 클래스를 찾기 위해 많은 시간을 소비해야 하고, 만약 클래스명을 잊어버린다면 더 많은 시간을 소비하게 될 것이다.

가독성(Readability): 우리는 몇 주에 걸쳐 혹은 수개월을 통해 새로운 피처를 개발한다. 하지만, 언제나 사용자의 니즈를 충족시킬 수는 없다. 컴포넌트 패키징 기반으로 개발을 했다면, 사용자의 니즈에 충족하지 못하는 피처를 제거하기 위해 각각의 컴포넌트들(activity, fragment, handler, DTO 외)에서 제거를 해야 한다. 반면에 피처 패키징 기반으로 개발을 했다면, top-level feature package에서 삭제할 수 있다. 그 뿐만 아니라, 일반적으로 피처의 한정적인 개발로 인해 관련 클래스들의 가독성 및 피처의 이해도를 높일 수 있다.

의존성(Dependencies): 피처 패키징 기반의 개발은 의존성을 줄이는 것에도 큰 도움이 된다. 대부분의 클래스들은 피처 패키지 내부에서 의존성을 가지게 될 것이고, 일부 공통적인 코드들에 대해서만 외부 의존성을 갖게 될 것이다. 그리고 일부 클래스에 대해서는 패키지 접근자를 통해 디테일한 제한을 할 수도 있다.

이뿐만 아니라, 피처 패키징 내에서 컴포넌트 혹은 레이어 단위로 재분할할 수도 있다. 예를 들어 MVP 패턴으로 피처 패키징을 구성한다면, view, model 그리고 presenter를 서브패키지(sub-package)로 컴포넌트화할 수 있다. 이를 통해 가독성과 유지보수성이 높아지고 쉽게 컴포넌트를 분리할 수 있기에 효율성 역시 향상된다.

피처 패키징과 MVP 패턴의 적용 예시

Login이라는 피처 패키지를 생성한다고 가정하자. 우리는 아래의 그림과 같이 model, presenter, view, util이라는 서브패키지들을 구성할 수 있다.

Fig1. Login feature package

매니저, 개발자, 디자이너 등 많은 구성원들이 함께 업무를 진행하면서 어떻게 효율적으로 일을 할 수 있을지 우리와 마찬가지로 많은 고민을 했을 것이다. 우리는 피처 패키징의 컴포넌트 간 상호작용을 통해 가장 합리적인 방안을 찾을 수 있었다.

공통 클래스들의 고민들

개발자들은 피처 패키징 기반의 개발을 진행하면서 흔히 다른 피처들간의 공통 기능 혹은 공유해야할 기능들은 어떻게 처리할지 다시 고민하게 된다. 중복코드와 의존성을 최소화하면서 공통 클래스를 분리해 나아가는 것은 쉬운 일이 아니다. 한 가지 방법으로 아래와 같이 모든 공통 클래스 Top-level common package에 위치시켜 이를 명확히 할 수 있다.

com.your.company.common.feature1

com.your.company.common.feature2

com.your.company.common.feature3

그리고 피처 내에서만 사용되어지는 공통 클래스의 경우는 다음과 같이 피처 패키지 내에 위치시킬 수 있다.

com.your.company.feature1.common

com.your.company.feature1.sub-feature1

com.your.company.feature1.sub-feature2

쿠팡의 사례

쿠팡에서는 얼마 전까지 도메인별로 책임을 나누어 기능들을 개발하도록 팀이 구성되어 있었다. 이로 인해 자연스럽게 팀별로 패키지를 구성하여 업무를 진행하게 되었다. 그래서 Top-level package에는 common과 domain 이라는 두개의 패키지를 만들고, 위에서 설명한 대로 common package를 구성하였고 domain package의 경우 하나의 패키지 당 하나의 도메인으로 구성하였다.

많은 이커머스 회사들은 검색, 로그인, 장바구니 등과 같은 도메인을 대부분 가지고 있을 것이다. 그리고 하나의 큰 도메인 아래, 여러 피처들이 녹아 들어있을 것이다. 예를 들면 검색 도메인에서는 home search, map search, auto-complete 등과 같이 다양한 피처가 존재할 수 있다. 우리는 이런 피처들을 각각의 단위로 재구성할 필요성을 느꼈다. 그리고 점진적으로 이를 진행해 나아가고 있다.

리패키징 팁

리패키징 작업을 진행할 때 몇 가지 유의해야할 사항들이 있다.

특히 XML(manifest, layout 등)내에 선언되어 있는 파일 혹은 클래스들의 경로(path)를 잘 확인해야한다. 이런 부분들은 Android Studio의 refactoring tools를 활용하여 업데이트할 수 있으므로, 해당 기능을 사용하면 좀 더 안전하고 편하게 적용할 수 있다.

또한, 리패키징 작업이후에 Proguard 설정도 다시 해야하는데, 우리는 리패키징 때마다 재설정 작업을 하고 싶지 않았으므로 marker 인터페이스를 생성하여 이를 모든 클래스에 적용하였다. 예를 들어, 모든 DTO에 DTO 인터페이스를 만들었고 해당 인터페이스를 implements한 모든 클래스들의 proguard 설정을 단일화하였다.

Interface
public interface DTO {}
Proguard
-keep class * implements com.your.company.common.DTO { *; }

이 작업을 통해 우리는 Proguard에 아무런 영향없이 리패키징 작업을 진행할 수 있었다.

분석을 위한 툴들

우리는 리패키징을 통해 얻을 수 있는 혜택들을 좀 더 가시화하기 위해 여러가지 툴들을 활용했고 이를 추천하려고 한다.

  1. Code Iris (https://plugins.jetbrains.com/idea/plugin/7324-code-iris): Android Studio Plugin으로 모듈과 패키지 그리고 클래스 3가지 단계로 그래프로 의존성을 확인할 수 있다.
  2. Apk-dependency-graph (https://github.com/alexzaitsev/apk-dependency-graph): 이 툴을 통해 클래스들간의 커플링 상태를 볼 수 있다.
  3. MetricsReloaded (https://plugins.jetbrains.com/idea/plugin/93-metricsreloaded): 테이블 도표를 통해 의존성이 있는 패키지와 클래스를 분석할 수 있다.

위의 툴을 활용하여, 우리는 손쉽게 의존성의 수를 측정하고 그래프화 할 수 있었다.

Fig2. Legacy 쿠팡앱

2개월 간의 피처 패키징을 적용한 후에는 아래와 같은 그래프를 접할 수 있었다.

Fig3. 피처 패키징 이후 쿠팡앱

결론적으로, 피처 패키징을 통해 우리의 레거시 코드를 향상시키고 앞으로 진행할 모듈화의 큰 스탭 하나를 마무리할 수 있게 되었다. 확장성, 가독성 뿐만 아니라, 공통 클래스도 여러 도메인에서 식별하여 재사용하는 것이 휠씬 쉬워졌다.

이제 우리는 MVP 또는 MVVM 패턴을 사용하는 작고 독립적인 기능으로 패키지를 구성할 수 있게 되었다. 다음 포스트에서는 진보된 모듈화 전략을 알아보도록 하겠다.

박성철, Sr. Manager, Software Engineering