Credit: Shutterstock
개발자라면 클린 아키텍처라는 단어와, 클린 아키텍처 다이어그램(과녁 그림)에 대해 한 번쯤은 들어본 적이 있을 겁니다. 저는 다양한 개발자들을 만나면서 Uncle Bob이 “Clean”라는 상표명(?)을 붙여서 만들어낸 브랜드일 뿐 어디까지나 상술이라고 얘기하는 분도 봤고, 정반대로 신앙에 가까운 믿음을 가지고 지나치게 철저히 적용하려는 분도 봤습니다.
앞으로 4번에 걸쳐서 클린 아키텍처가 실제로 Android / iOS 개발에 어떤 도움을 주는지, 내가 지금 개발하고 있는 프로젝트와는 어떤 의미가 있을 지 함께 생각해보고자 합니다.
클린 아키텍처란 무엇인가?
클린 아키텍처는 Uncle Bob이 2012년 엔터프라이즈 아키텍처에서 논의 되던 내용을 집약시킨 개념입니다.(한글 번역 링크) 클린 아키텍처는 두 가지 관점에서 볼 수 있습니다.
하나는 아키텍처 설계의 철학과 원칙입니다. SOLID 원칙 — 단일 책임 원칙(Single Responsibility Principle)을 시작으로 한 다섯 가지 원칙 — 등을 중심으로 이제까지 SW 설계에서 중요하게 거론되어온 다양한 원칙들을 일목요연하게 정리하고 있습니다.
두번째는 아래 과녁 그림(?)으로 유명한 아케텍처의 청사진입니다. 이는 Hexagonal Architecture, Onion Architecture 등 당시 널리 알려진 아키텍처들의 공통된 성과물을 정리한 것입니다. 모바일부터 백엔드까지 모든 소프트웨어에 일반적으로 필요한 내용을 담고 있으며, 각 계층을 어떻게 나누고 어떤 요소로 구성할 것인가에 대한 원칙들을 알려줍니다.
가운데로 갈 수록 높은 수준, 바깥으로 갈 수록 낮은 수준의 컴포넌트로, 이에 대한 효율적인 분리로 효과적인 설계가 가능하다는 점을 설명하고 있습니다.(이에 대한 자세한 설명은 최근 번역 출간된 책을 참고 바랍니다.)
Credit: 도서출판 인사이트
클린 아키텍처는 이후 2018년에 책으로 정리되어 출간되었지만, 그전부터도 이미 아키텍처 분야에서 가장 입에 잘 오르내리는 buzzword가 되었고, 다양한 SW의 설계에 영감을 주었습니다. 모바일 관점에서는 아래 네 가지 부분에서 큰 영향을 주었다고 생각합니다.
- 경계선(Boundaries): 계층 구조의 개념이 널리 적용됨
- 유스케이스(Use Case): 도메인 계층의 분리로 소스코드 변경 안정성(stability)이 높아짐
- 험블 객체(Humble Object): 프리젠테이션 계층의 테스트 가능성, 가독성, 유지보수성을 향상
- 의존성 역전(DIP): modular한 프로젝트 구조의 확산
이 글에서는 1.경계선에 대한 생각을 나눠볼까 합니다.
Part 1. 경계선: 계층 나누기
“소프트웨어 아키텍처는 선을 긋는 기술이며, 나는 이러한 선을 경계(boundary)라고 부른다. 경계는 소프트웨어 요소를 서로 분리하고, 경계 한편에 있는 요소가 반대편에 있는 요소를 알지 못하도록 막는다.” - Robert C. Martin, Clean Architecture, Ch. 17
암흑시대 이야기
모바일 SW 아키텍처에 대해 어느 정도 집약된 지식이 공유되기 전 시절에는 대략 아래와 같은 시도들이 패턴화 되고 있었습니다.
- **Service 분리
**앱의 여러 부분에서 사용되는 공통 부분을 별도 클래스로 떼서 Service로서 사용하는 것은 (XXXService, XXXManager 같은 이름으로) 가장 일반적인 형태입니다.
빈번히 사용되거나 메모리를 별로 사용하지 않는 객체의 경우 싱글톤 패턴으로 구성하기도 하고, 각 Service를 생성해서 인터페이스(or 프로토콜)의 인스턴스로 리턴해주는 별도의 Service를 Service Locator 패턴으로 구현하기도 합니다. - **Repository 패턴
**REST API 호출 같이 외부로부터 데이터에 접근, SQLite, Room, Realm, CoreData 등으로 내부 DB에 접근, 테스트를 위해 그냥 고정된 결과값만 리턴하는 mock object 등의 데이터 출처가 있게 마련인데, 이들을 추상화 해서 데이터 출처와 관계 없이 동일 인터페이스로 데이터에 접속할 수 있도록 만드는 것이 Repository 패턴입니다.
위의 Service들은 일부 UI에 관련된 것도 있고 (애니메이션 혹은 레이아웃을 위한 좌표 계산 등을 포함), 대부분은 업무 로직과 데이터 로직이 뒤섞여진 형태로 묶이게 마련입니다. 클린 아키텍처는 이들을 근본적으로 분리시켜주는 단초를 제공합니다.
모바일 클린 아키텍처
2012년 이후, 어떻게 하면 모바일 설계에서 클린 아키텍처의 도움을 받을 수 있을까에 대한 많은 논의와 시도가 있었는데, 그중 가장 중요한 영향은 경계(Boundaries)에 대한 것이었습니다. 즉 모듈의 변경이 다른 모듈에 영향을 미치지 않도록, 그리고 같은 모듈 내에서는 일관되고 응집력 있는 결합을 제공할 수 있도록 계층을 구분하는 것입니다.
그 결과, 원래 네 개인 계층을 재구성해서 세 개의 계층으로 분리하는 것으로 일반화되었습니다. 아래의 그림은 모바일 앱에서 전체적으로 받아들여지는 모바일 클린 아키텍처를 설명한 그림들 중, 제가 본 것들 중 가장 직관적으로 정리된 그림입니다.
Credit: koutalou
**프리젠테이션 계층 (Presentation Layer)
**화면의 표시, 애니메이션, 사용자 입력 처리 등 UI에 관련된 모든 처리를 갖고 있습니다.
- 뷰(View): 직접적으로 플랫폼 의존적인 구현, 즉 UI 화면 표시와 사용자 입력만 담당합니다. 자기가 화면에 그리는 것이 어떤 의미가 있는지는 전혀 알지 못하고, 다만 프리젠터의 명령을 받아 화면을 어떤 이미지, 어떤 색깔로 그릴까를 결정할 뿐입니다. 데이터도 화면의 좌표와 같은 것만 가집니다.
주의할 점은 View가 꼭 Activity/Fragment(안드로이드의 경우), ViewController(iOS의 경우)를 의미하지는 않는다는 점입니다. 이에 대해서는 나중에 다시 다뤄보겠습니다. - 프리젠터(Presenter): 단순히 MVP/VIPER에서의 프리젠터만을 의미하는 용어가 아니라 넓은 의미의 단어로 생각하시면 됩니다. 예를 들어 MVVM 구조에서라면 ViewModel과 동일한 의미로 생각할 수 있습니다. 프리젠터는 뷰와는 달리 플랫폼에 직접적으로 의존하지 않는 클래스입니다. 따라서 손쉽게 단위 테스트가 가능합니다. 그리고 프리젠터는 뷰와는 달리 화면에 그리는 것이 어떤 의미를 가지고 있는가를 알고 있습니다. 사용자 입력이 왔을 때 어떤 반응을 해야하는 지에 대한 판단도 역시 프리젠터가 합니다.
도메인 계층 (Domain Layer)
- 유스케이스(UseCase): 비즈니스 로직이 여기에 구현됩니다.
- 도메인 모델(Model): 앱의 실질적인 데이터가 여기에 구현됩니다. 도메인 모델은, 물론 REST API 등으로 얻어지는 외부 데이터와 같은 필요도 전혀 없습니다.
- 트랜스레이터(Translater): 데이터 계층의 엔티티 - 도메인 모델을 변환하는 mapper의 역할을 합니다.
데이터 계층 (Data Layer)
- 리포지토리(Repository. 관점에 따라 도메인 계층 소속일 수도 있고, 데이터 계층 소속일 수도 있음): 유스케이스가 필요로 하는 데이터 저장/수정 등의 기능을 제공하는 클래스입니다. 데이터 소스를 인터페이스 형태로 참조하기 때문에 이 클래스에서 데이터소스 객체를 갈아끼우는 형태로, 외부 API 호출/로컬 DB 접근/mock object 출력을 전환할 수 있습니다.
- 데이터 소스(Data Source): 실제 데이터의 입출력이 여기서 실행됩니다.
- 엔티티(Entity): 데이터 소스에서 사용되는 데이터를 정의한 모델로, 맨 위의 과녁 그림에서의 엔티티와는 다른 개념입니다. REST API의 요청/응답을 위한 JSON, 로컬 DB에 저장하기 위한 테이블이 대표적입니다.
도메인 계층의 유스케이스와 프리젠테이션 계층의 프리젠터는 어떻게 구별하나?
많은 경우, 유스케이스의 비즈니스 로직과 프리젠터의 로직은 명확히 구별이 되지만, 가끔은 매우 헷갈리는 경우가 있습니다.
이럴 경우, 가장 도움이 되는 질문은, “개발팀 외부의 사업 부서의 사람도 알고 있어야 하는 로직인가?” 여부라고 생각합니다. 비즈니스 로직은 문자 그대로 앱의 사용자 상호작용이 아닌 업무 요구사항을 담고 있는 것이기 때문입니다.
다른 케이스로, 앱의 특성상 각 프리텐터에서 자주 사용되는 공통의 로직이 꽤 발생하는 경우가 있습니다. 이 경우, 엄밀히 얘기하면 도메인 로직이라고 할 수는 없지만, ViewUseCase 형태의 클래스로 분리해서 프리젠테이션 계층에 추가하는 것은 좋은 방법이라고 생각합니다.
그렇게 계층을 나누면 뭐가 좋은가?
이렇게 계층을 분리함으로 인해 얻을 수 있는 가장 큰 이점은, 상당히 분량이 많은 앱이더라도 소스코드 전반을 쉽게 장악할 수 있다는 점입니다.
복잡한 수정 사항이 생겼을 때라도, 어떤 부분들을 고치면 되는지 금방 파악할 수 있습니다. 모듈 구성, 그리고 패키지/폴더 구성이 자연스럽게 각 계층별로 일목요연한 트리구조를 이루기 때문에(이 부분은 마지막 글에서 다뤄보겠습니다), 다른 개발자나 혹은 (다른 개발자나 다름없는) 몇 달 뒤의 내가 다시 코드를 들여봐도 금방 코드를 이해하고 수정할 수 있습니다. 정확히 얘기하면, 굳이 지금 수정할 필요가 없는 코드를 보지 않고도 필요한 부분만 보면 됩니다.
그리고 무엇보다 좋은 점은, 특정 계층에 대한 수정이 다른 계층에 거의 영향을 주지 않는다는 점입니다.
저의 경우, 전 직장에서 회원제 서비스 앱을 개발한 적이 있는데, 회원가입을 하지 않으면 아주 기본적인 소개와 튜토리얼 등 간단한 화면만 볼 수 있는 앱이었습니다. 그런데 출시를 단 이틀 남겨놓은 시점에서 기획자가 (사장님 보고 자리에서 한 소리 듣고 와서..) 다급히 Airbnb처럼 로그인 없이도 대부분의 화면을 볼 수 있게 앱을 수정해달라는 요청을 해왔습니다.
그것은 상당히 큰 변화라, 클린 아키텍처 형태의 구조가 아니었다면 구현과 리그레션 테스트/디버깅까지 포함해서 몇 일 밤을 새야할 수도 있는 수정사항이었지만, 단 3시간만에 완료해서 기획자를 놀래켜준 경험이 있습니다.
REST API를 통해 받은 정보를 보여주는 몇 개의 유스케이스를 로그인 하지 않은 유저에게도 (제한된 정보로) 값을 주도록 수정하고, 데이터 소스에서 API 호출 시 파라미터를 변경하고, 비로그인 유저가 회원관리 모드로 들어갈 수 없도록 프리젠터를 수정하고 반대로 나머지 페이지는 다 들어갈 수 있도록 네비게이션 로직을 바꾸는 걸로 모든 수정이 끝났기 때문입니다. 급격한 기획 변경에도 불구하고, 소스코드 관점에서는 변경되는 부분들이 제한되어 있었기 때문에 버그가 발생할 여지도 많지 않았습니다.
저 많은 클래스들을 꼭 다 만들어야하나요?
클린 아키텍처는 많은 장점이 있는 반면, 부작용으로 간단한 로직을 구현할 때도 상당히 많은 양의 클래스를 만들어줘야 한다는 부담이 있습니다. 이 경우의 해결책으로 몇 가지 대안을 생각해 볼 수 있겠습니다.
- 클린 아키텍처의 원리를 나름대로 재해석하기
앱이 간단한 경우, 특히 거의 모든 도메인 로직이 백엔드 서버에 있어서 딱히 비즈니스 로직이랄 것을 많이 갖고 있지 않는 앱이라면, 애초에 클린 아키텍처의 모든 측면을 일일히 구현할 이유가 없습니다. 클린 아키텍처의 설계 철학만을 잘 받아들여서, 도메인과 데이터 계층을 통합해서 고수준의 리포지토리 클래스를 구현하고, 프리젠테이션 계층에서의 로직 분리에 유의하면 충분히 훌륭한 구조가 나올 수 있습니다. 아래의 DroidKaigi 2018 앱은 그에 대한 매우 훌륭한 예를 보여줍니다.
https://github.com/DroidKaigi/conference-app-2018 - 요소들을 통합하기: 예) 데이터 소스와 리포지토리 통합
예를 들어, 데이터 패턴이 단순하다면 리포지토리와 데이터소스 클래스를 통합하는 것도 좋은 방법입니다. 또는 리포지토리를 실제로는 인터페이스(혹은 프로토콜)만 갖도록 정의하고, 그 구현 클래스가 데이터소스 역할을 하는 것도 즐겨 사용되는 방법입니다. - 필요없는 요소를 축약하기: 예) 유스케이스 생략
어떤 컨퍼런스에서, 유스케이스에 대한 단위 테스트 작성이 너무 단순해서 테스트를 만들 필요성을 못 느끼겠다는 질문을 받은 적이 있습니다. ‘그럴리가요!!’라고 생각해서 자초지종을 들어봤더니, 실은 앱에 비즈니스 로직이 거의 없다보니 프리젠테이션 계층과 데이터 계층 사이의 중계만을 위한 단순한 내용의 유스케이스를 만든 경우였습니다. 이렇게 비즈니스 로직의 양이 많지 않은 경우라면 굳이 유스케이스를 만들기보다는 일부는 리포지토리로, 일부는 프리젠터로 흡수시켜도 됩니다. - **최적화가 신경쓰여요
**클린 아키텍처 적용으로 적지 않은 양의 클래스가 추가된다고는 해도, 객체 숫자로 인한 성능과 메모리의 손실은 상당히 미미합니다. 다만 트랜스레이터에서의 변환 과정은 데이터 량이 많을 경우 무시 못할 성능 저하를 가져올 수 있습니다. 예를 들어 URL 파싱과 같은 처리가 변환 과정에서 필연적으로 일어나는 경우를 생각할 수 있습니다.
이 경우, 데이터를 극력 reference copy로 전달되도록 하고, 대신 이 경우 생길 수 있는 부작용(특히 iOS의 경우)을 방지할 방어 로직을 추가해줄 필요가 있습니다.
모델-엔티티 사이의 내용 차이가 거의 없는 경우라면, 굳이 변환으로 인한 오버헤드를 발생시킬 필요없이 도메인 모델로 (리포지토리를 중심으로 하고 싶으면 반대로 엔티티로) 모든 모델을 통일하면 됩니다. 이 경우, 개념을 명확히 하기 위해 편의상 모델의 alias(Kotlin, Swift의 경우typealias
로 지정. Java는 불가능)를 엔티티에 추가해서 데이터 레이어에서는 이것을 이용해 참조하도록 하기도 합니다. - **난 그런 것 없이도 잘 살았어요. 꼭 적용해야 하나요?
**안 해도 괜찮습니다! 유행하는 설계 원칙이라고 해서 반드시 적용해야 한다는 압박감을 가질 필요는 없습니다. 클린 아키텍처의 내용을 충분히 학습해서, 좋은 원칙들을 내 프로젝트에 조금씩 적용하는 것만으로도 충분한 효과를 얻을 수 있다고 생각합니다.
오히려 크게 문제가 될 만한 구석이 없는 단선적인 구조의 앱에 굳이 무리해서 적용할 경우, 필요없는 클래스를 대량으로 만들어야 하는 부작용과, 새로운 설계를 학습해가면서 리팩토링을 병행해야 하는 과정에서 예기치 못하게 늘어나는 버그들로 인해 상당한 스트레스를 경험해야할 수도 있습니다.
다만, 혹시 뷰와 프리젠터를 굳이 나눌 필요가 없는 것처럼 생각되시는 경우라면, 다시 한 번만 더 생각해 보시길 간곡히 권합니다. 지금 만드시는 게 아무리 단순한 앱이라고 해도.. 그럴리가 없습니다.
다음 주에는 모바일 개발에서 자주 거론되지 않은 도메인 계층에 대해서 나눠보고자 합니다.(이번엔 소스코드도..)
그리고 최신 모바일 개발 소식이 알고 싶은 분들은 아래 링크들을 눌러누세요~ =)