쿠팡 안드로이드 아키텍처 — Part 1

관심사의 분리 (Separation of Concerns, SoC)

쿠팡 엔지니어링
Coupang Engineering Blog
6 min readAug 3, 2022

--

By Joris Abale

시리즈

이번 포스트는 “쿠팡 안드로이드 아키텍처” 시리즈의 첫번째 이야기입니다.

안드로이드 봇

본 포스트는 영문으로도 제공됩니다.

지난 수년 간 쿠팡도 다른 기업들처럼 초기 안드로이드 SDK, 컨벤션, 패턴을 사용해 모바일 앱을 개발해 왔습니다. 해를 거듭하며 구글과 개발자 커뮤니티는 오픈 플랫폼을 통해 시각적, 개념적, 기능적으로 발전했으며 쿠팡 앱도 이에 맞게 달라졌습니다. 지난 몇 년 간 한국에서 가장 많이 다운로드되고 사용되는 안드로이드 앱 중 하나로 성장한 쿠팡 앱이 어떻게 변화되어 왔는지를 이 포스트를 통해 공유드리려고 합니다.

관심사의 분리에 대한 필요성

2016년, 쿠팡 앱의 아키텍처 개선을 위해 다들 한자리에 모였습니다. 수년간 개발해왔던 앱을 분석해보니 끊임없이 변하는 안드로이드 프레임워크 덕분에 처음과는 달리 앱은 많은 기능들을 갖게 되었고 내부 복잡도도 매우 커졌습니다. 수년 전의 설계를 바탕으로 쓰여진 몇 십만 라인이 넘는 코드와 천만이 넘는 사용자가 있었습니다. 코드베이스(codebase) 사이즈 때문에 유지보수도 어려왔지만 모든 기능이 단일 객체에 집중되었습니다. 개선을 위해 고객 중심적이면서도 최적의 현대화 방법이 필요했습니다.

악명 높은 안티패턴이지만 안드로이드 초창기에는 ‘갓 액티비티(God Activity)’를 가지고 있는 앱이 많았습니다. 이 문제를 해결하기 위해 구글은 액티비티(Activity)의 UI를 구성하는 요소인 프래그먼트(Fragment)를 도입했습니다. 저희도 구글의 권고를 따라 God Class를 분해하고 UI 수준 또는 OS 수준의 상호 작용에 프래그먼트를 사용하기로 결정했습니다. 이것은 관심사의 분리를 적용하는 첫 번째 시도였으며, 어떤 면에서는 변경된 모델-뷰-컨트롤러(MVC) 패턴을 사용한 방식이었습니다.

유용한 해결책이었고 덕분에 더 많은 기능을 추가하며 성장할 수 있었습니다. 그러나 이 방식에는 문제점도 많았습니다.

  • 프래그먼트와 액티비티 간의 통신이 늘 원활하지는 않았습니다.
  • 프래그먼트는 각각 다른 생명 주기(lifecycle)를 가졌기 때문에 생명 주기가 복잡하게 중첩되는 경우가 발생합니다. 이런 경우가 종종 발견하거나 고치기 힘든 버그의 원인이 될 때가 있었습니다.
  • 프래그먼트는 우리가 기대했던 뷰 컨트롤러의 역할에 정확히 부합하는 것은 아니었고 뷰와 밀접하게 연관되어 있었습니다.
  • 안드로이드 특화 코드를 많이 포함하고 있어 유닛 테스트가 쉽지 않았습니다.
  • 프래그먼트에 점점 더 많은 기능을 추가하다 보니 결국 ‘갓(God) 프래그먼트’가 생겨났습니다.

부적절한 MVC 구조가 쿠팡 앱의 성장에 걸림돌이 되는 지경에 이르렀고, 더 이상 새로운 기능을 사용자에게 자신 있게 내놓을 수 없게 되었습니다. 따라서 저희는 관심사(concern), 뷰(view), 모델(model), 비즈니스 로직(business logic)을 완전히 분리하여, 각각의 독립된 구성요소로 만드는 작업에 착수했습니다.

아키텍처 패턴

하나의 완벽한 아키텍처는 없으며 결정에 영향을 미치는 요소들도 많았기 때문에 알맞은 패턴을 선택하는 것은 어려웠습니다. 저희는 먼저 다음과 같은 목표를 정의했으며 이에 알맞은 패턴을 도입하기로 결정했습니다.

  • 코드 품질 향상
  • 유닛 테스트를 통한 앱 무결성 및 신뢰성 향상
  • 코드의 가독성 및 유지 관리성 향상
  • 업계에서 인정하는 표준 및 패턴 사용

옵션 1: MVC 패턴

Model-View-Controller (MVC) 패턴은 애플리케이션의 구성요소를 3가지로 나눕니다. Model은 앱의 데이터 및 비즈니스 기능을 나타냅니다. View는 모델에 저장된 것을 시각적으로 표현합니다. Controller는 사용자 입력에 반응하고 Model과 View에게 어떻게 반응해야 하는지 알려줍니다.

MVC 패턴
그림 1. MVC 패턴

하지만 안드로이드에서는 액티비티/프래그먼트가 뷰와 컨트롤러 역할을 동시에 하는 경우가 많기 때문에 MVC를 구현하기가 어렵습니다. 또한 보통 모놀리식(Monolithic) 구조로 만들어지며, 최신 안드로이드 앱 유형에서는 많이 선호되지 않습니다.

옵션 2: MVP 패턴

Model-View-Presenter (MVP) 패턴은 MVC 패턴에서 파생된 패턴으로 현대 안드로이드 개발에서 더 많이 보이는 패턴입니다. MVC 패턴과의 주요 차이는 Presenter가 View에서 데이터를 가져오고 View를 대신하여 시각적 요소를 업데이트하는 점에 있습니다. Presenter와 View는 일대일 관계를 가집니다. View와 Model은 분리된 것처럼 작동합니다.

MVP 패턴
그림 2. MVP 패턴

MVP는 테스트, 유지보수, 확장이 용이한 패턴입니다. 추가 라이브러리 또는 프레임워크를 필요로 하지 않으면서 기존 코드베이스에 쉽게 통합할 수 있습니다. MVP는 학습하기 비교적 용이하며 특히 MVC 패턴을 사용해 본 경험이 있다면 더 쉽게 적응할 수 있습니다.

그러나 작은 기능의 경우 MVP 패턴을 사용하면 복잡성이 증가할 수 있으며, 평범한 MVC 패턴으로 간단하게 구현하는 편이 더 나은 경우도 있습니다. 또한 MVP는 구성요소 간의 통신을 위해 콜백을 남발할 여지도 가지고 있어 이를 방지하기 위해 ‘RxJava’와 같은 반응형 프로그래밍을 적용해야 합니다.

옵션 3: MVVM

Model-View-View Model (MVVM) 패턴은 UI와 애플리케이션 로직을 분명히 구분합니다. MVC의 Controller 그리고 MVP의 Presenter 대신에 View와 View Model 사이에 양방향 데이터 바인딩이 진행됩니다.

MVVM 패턴
그림 3. MVVM 패턴

MVVM도 테스트, 유지보수, 확장이 용이하지만 구글의 데이터 바인딩 라이브러리 또는 프레임워크가 요구됩니다. 또한 디버깅 데이터 바인딩은 번거로울 수 있고 MVC나 MVP보다 더 많은 학습 시간이 필요합니다.

모두 좋은 옵션이지만 MVVM 패 턴을 고려했을 당시 구글은 아키텍처 가이드 및 구성요소를 발표하지 않은 상황이었고 구글 데이터 바인딩 라이브러리는 베타 버전이었기 때문에 우리는 MVP 패턴에 좀 더 무게를 두게 되었습니다.

리팩토링 전략

전체 코드베이스를 더 모듈화된 아키텍처로 변경하는 것은 하룻밤 사이에 이루어질 수 없고 이해관계자들 모두의 효율적이면서도 지속적인 노력이 필요합니다. 이 야심찬 프로젝트를 고객 지향적인 사고로 완수하기 위해 거친 몇가지 단계를 소개합니다.

단단한 기반에서 시작하기

저희는 잘 다져진 기반 위에서 리팩토링을 진행하려고 했으며 코드 품질 개선도 동시에 이루겠다는 의지가 있었습니다. 이를 달성하기 위해 SonarQube의 모든 기능을 활용했습니다. (SonarQube는 중복 코드, 코딩 표준, 유닛 테스트, 코드 커버리지, 코드 복잡도, 주석, 버그, 보안 취약점에 대한 보고서를 제공하는 솔루션입니다.)

SonarQube의 리포트를 쿠팡의 CI 시스템에 통합하여 매 빌드마다 새 리포트가 생성되도록 했습니다. 규칙의 기반은 안드로이드 Lint를 따르도록 하되 니즈에 더 부합하도록 수정했습니다. 유닛 테스트의 결과 또한 SonarQube의 리포트에 통합했습니다. 또한 이슈 할당과 코드 리뷰 등의 절차와 정책을 활용해 코드 품질 개선의 수고를 효율적으로 분담해 나갔습니다.

결과적으로 중복 코드나 더 이상 사용하지 않는 코드를 대폭 줄이고, 코드 커버리지를 향상시켰으며, 앱의 전반적인 복잡도를 크게 줄일 수 있었습니다.

쿠팡앱에서 줄어들고 있는 에러 수를 보여주는 그래프
그림 4. 쿠팡 앱에서 줄어들고 있는 에러 수

커스텀 라이브러리

첫 작업 완료 후, 앱 전반에 MVP 구현을 지속적으로 달성하고 MVP 패턴에 종종 따르는 ‘보일러플레이트(Boilerplate)’ 코드를 줄이기 위해 커스텀 라이브러리를 만들어 제공했습니다. View와 Presenter의 바인딩이 라이브러리 내부에서 처리되어 View안에서는 단지 createPresenter() 메소드 하나만 오버라이드해도 되게끔 진행됩니다.

MVP 패턴을 적용하는 데 라이브러리가 어떻게 도움이 되는지 명확히 알아보려면, 쿠팡의 커스텀 MVP 라이브러리 구현에 큰 영감을 준 GitHub에서 Mosby를 참고하세요.

지식 공유

지식 공유와 교육은 신규입사자 뿐만이 아니라 팀의 결속력을 위해서도 중요합니다. MVP 패턴 사용법과 코드 리팩토링 방법을 내부적으로 공유하기 위해 여러 차례의 컨퍼런스를 진행했으며 기존 기능을 리팩터링해 보는 코드랩도 진행했습니다. 또한 강력한 코드 가이드라인을 만들어 지침으로 삼았습니다.

가이드라인 관련 정보는 GitHub에서 Ribot’s Android Guidelines를 참고하세요.

구글의 아키텍처 가이드라인

구글은 I/O 2017에서 안드로이드 앱과 아키텍처를 설계할 때 참고할 권장 가이드라인과 구성 요소(component)를 발표했습니다. 발표와 동시에 저희도 해당 가이드라인과 구성 요소를 MVP 아키텍처에 적용했습니다.

발표된 구성 요소 중 하나는 생명 주기 인식 구성 요소로, 액티비티나 프래그먼트가 현재 자신의 생명 주기 상태를 기반으로 자신의 동작을 자동 조정합니다. 이러한 구성 요소를 활용해 뷰 생애주기 그리고 대기 시간 및 네트워크 호출 일시 중지와 같은 이벤트에 반응할 수 있었습니다.
예를 들어, Presenter에 다음과 같은 메소드를 구현할 수 있습니다.

@OnLifecycleEvent(value = Lifecycle.Event.ON_RESUME)
protected void onResume() {
...
}

이 방법은 액티비티 또는 프래그먼트에 일관된 특성을 추가하는 동시에 BaseActivity 및 BaseFragments에 점점 더 많은 코드를 추가함으로써 발생하는 상속 복잡성을 관리하는 데 매우 유용합니다. 그리고 저희는 상속(inheritance)보다 구성(composition)에 더 가치를 두고 중요시합니다.

ViewModel 클래스는 생명 주기를 고려하며 UI 관련 데이터를 저장하고 관리할 수 있게 디자인되어 있으며 데이터가 화면 회전과 같이 구성을 변경할 때도 데이터를 유지할 수 있게 합니다. 저희는 ViewModel을 사용해 구성을 변경하는 동안 Presenter를 유지하여 View를 다시 만들 때 Presenter를 다시 작성할 필요가 없게 사용했습니다.

LiveData 클래스는 관찰 가능한(Observable) 데이터 홀더 클래스입니다. 일반 Observable과 달리 LiveData는 생명 주기를 인식합니다. 즉, 액티비티, 프래그먼트, 서비스 등 다른 앱 구성요소의 생명 주기를 고려합니다. 생명 주기 인식을 통해 LiveData는 생명 주기 상태가 액티브(active)인 옵저버들(observers)만 업데이트합니다. LiveData는 대부분 MVVM에 사용되며 이유는 모델과 뷰가 직접적으로 커플링되지 않기 때문입니다. 하지만 저희 MVP 패턴의 Presenter를 생명 주기 인식 구성 요소로 볼 수 있습니다.

마무리하며

여러 달에 걸친 작업 끝에 저희는 MVP 패턴을 사용하여 코드베이스 대부분을 리팩토링하고 유닛테스트 커버리지를 80%로 올리겠다는 목표를 달성했습니다. 쉽지 않은 변화의 과정이었지만, 그 결과 이제는 앱에 새로운 기능을 추가하기가 훨씬 쉬워졌고 코드 품질도 크게 향상되면서, 저희에게 있어 매우 중요한 이정표가 되었습니다.

시리즈

이번 포스트는 “쿠팡 안드로이드 아키텍처” 시리즈의 첫번째 포스트였습니다. 이어지는 시리즈에서 쿠팡이 어떻게 유닛 테스트를 통해 신뢰성을 확보해냈는지, 그리고 어떻게 전체 앱의 모듈화를 통해 한 단계 더 전진했는지 확인해 보세요.

Part 1 — 관심사의 분리

Part 2 — 앱의 모듈화

Part 3 — 리패키징을 통한 의존성 제거

저희는 열린 사고로 질문하고 도전하며 역할에 책임을 다하는 인재를 환영합니다. 이 포스트가 재미있었다면 Coupang Careers에서 현재 채용 중인 포지션을 확인해 보세요!

--

--

쿠팡 엔지니어링
Coupang Engineering Blog

쿠팡의 엔지니어들은 매일 쿠팡 이커머스, 이츠, 플레이 서비스를 만들고 발전시켜 나갑니다. 그 과정과 결과를 이곳에 기록하고 공유합니다.