들어가며
최근 안드로이드 진영에서 클린 아키텍처를 채택하는 흐름으로 굳혀진 것 같다. 현재 진행하고 있는 프로젝트에서도 신규 코드에 대해서는 클린 아키텍처를 도입해보자는 목표를 세웠고, 현재 진행형이다. 이번 글에서는 클린 아키텍처를 도입하며 가졌던 의문, 고민점들에 대해 공유하고자 한다.
도입 배경
클린 아키텍처를 도입했을 때 여러 이점이 있겠지만 우리팀에서 가장 크게 느낀 것은 ViewModel 의 부담을 덜어줄 수 있다는 점이다.
MVVM, MVP 구분없이 많은 프로젝트에서 위와 같은 구조로 이루어져 있을 것이라 생각된다. 복잡하지 않은 화면에서는 위 구조도 충분히 역할을 다할 수 있다. 문제는 스펙 자체가 복잡하거나 Main flow 에 속한 화면일수록 수많은 비즈니스 로직이 존재하거나 늘어나가고 있다. 즉, ViewModel 이 집중적으로 비대해지게 되되어 무슨일을 수행하는지 파악하기가 어려워지곤 했다.
ViewModel 을 만드는 기준을 Activity 가 아닌 개별 View 를 기준으로 만들어보기도 했으나 근본적인 해결책은 되지 못했다. 이에 클린 아키텍처에서 주장하는 도메인 계층의 필요성이 대두 되었다. 도메인 계층을 통해 ViewModel 에서 수행되는 비즈니스 로직을 분리하는 것이 목표였다.
클린 아키텍처 (이론)
클린 아키텍처는 로버트 마틴이 직접 쓴 책도 있고 이미 잘 설명해놓은 글들이 많아서 여기서 개념 자체를 깊게 설명하지는 않으려고 한다.
우선 클린 아키텍처자체가 새로운 개념은 아니다. 관심사의 분리, Testable 한 구조, 변경이 용이한 코드 등의 목적을 만족하기 위해 여러 아키텍처를 도입하는 시도가 있었고, 클린 아키텍처는 이러한 아키텍처들의 장점을 모아 정의한 것이라고 마틴은 얘기하고 있다.
아마 클린 아키텍처를 조사하면 가장 많이 보이는 다이어그램일텐데, 이곳에 클린 아키텍처의 핵심이 담겨있기 때문이다. 핵심은 다음과 같다.
의존성 규칙을 통한 계층 분리
즉, 내부원에 있는 것은 정책이고 바깥 원으로 갈수록 정책을 수행하는 메커니즘으로 구성된다. 소스 코드 레벨에서는 의존성은 메커니즘이 정책을 의존하고 그 반대로는 의존할 수 없다.
클린 아키텍처 (이론 in Android)
클린 아키텍처는 자유도가 높은 편이기 때문에 위 핵심 규칙을 위반하지 않다는 가정하에 다양한 형태로 구현될 수 있다. 안드로이드 진영에서는 일반적으로 다음과 같은 구조로 구현되고 있다.
특징은 다음과 같다.
- 3 Layer (Presentation, Domain, Data) 아키텍처 기반
- 클린 아키텍처 상의 Entity 개념은 채택하지 않는다. (위 도표에서의 Entitiy 는 서버 데이터를 parse 하기 위한 DTO 성격의 클래스로 클린 아키텍처의 Entity 개념과 다르다.) 채택하지 않은 이유는 클린 아키텍처의 Entity 는 엔터프라이즈 비즈니스 로직 즉, 최상위 레벨의 정책을 수행하는 계층인데 이러한 정책은 보통 서버레벨에서 수행되기 때문이 아닐까 싶다.
위 도표는 의존성 방향이 잘 보이지 않아 좀 더 단순화하여 다시 표현해보면 다음과 같다.
Presentation 과 Data 계층은 도메인 계층 대비 상세 구현에 해당하기 때문에 도메인 계층을 의존하는 방향으로 구현된다. 여기까지가 모든 프로젝트에서 공통으로 가져갈 내용이라고 생각된다. 이제부터는 프로젝트에서 직접 적용하며 생겼던 궁금증에 대해 다뤄보고자 한다.
클린 아키텍처 (실전)
ViewModel to UseCase
클린 아키텍처의 필요성은 모두 인지 되었고, 도입을 고려하는데 가장 처음에 막힌 부분은 ‘ViewModel 과 UseCase 에서 수행하는 일을 구분하는 것’ 이었다.
로버트 마틴이 주장하는 UseCase 의 특징은 다음과 같다.
- 업무 요구사항을 담고 있다.
- 입력, 결과물, 그리고 결과물을 생성하기 위한 처리단계를 기술한다.
의도는 이해가 되었다. 문제는 위 특징을 ViewModel 에 대입해도 어색하지 않다는 것이다. 기존에 ViewModel 이 처리하고 있던 것이기도 했고.
좀 더 적용 가능한 특징을 도출해보기로 했다.
-
Android 의존성 없이 수행될 수 있는 로직인가?
→ 도메인 레이어에 속한 UseCase 는 ‘정책’에 가깝다. 즉, 이곳에서 수행되는 로직은 Android, IOS, Web 의 구분없이 수행되어야 할 것이다.
-
개발지식이 없는 타 부서 사람에게 UseCase 리스트만 보여주었을 때 해당 도메인이 무슨일을 수행하는지 이해 할 수 있는가?
→ 그만큼 추상화된 동작으로 표현되어야 할 것이다. 세부 내용 (= 구현)은 들어갈 수 없다.
위 규칙을 ‘댓글 리스트 화면’ 에 도입해보면 다음과 같은 UseCase 로 분리가 가능하다.
- 댓글을 작성한다.
- 댓글 리스트를 불러온다.
- 댓글을 삭제한다.
물론 실제 스펙들은 훨씬 복잡하고 예외상황이 많기 떄문에 위 규칙만으로 커버가하기 어려운 부분도 분명히 있다. 그때는 클린 아키텍처의 대전제를 기준으로 팀내 논의를 통해 구분해보면 좋을 것 같다.
Useless UseCase?
그런데 이런 문득 이런 의문이 들었다. ‘대부분의 UseCase 는 Repository 의 래핑 클래스 밖에 안될거 같은데.. 도입했을 때 우리가 기대한만큼 이점이 클까?’
고민과 논의 끝에 다음과 같은 장점으로 인해 래핑 클래스 이상의 가치가 있다고 결론을 내렸다.
-
변경이 전파되는 것을 막을 수 있다.
→ 처음에는 단순히 래핑 클래스 형태일지리도, 늘 그랬듯 시간이 지나면 변경이 발생할 수 있다. 이때 해당 비즈니스 로직을 수행하는 과정이 복잡하고, 해당 로직을 사용하는 곳이 많아질수록 관리가 어려워진다. 따라서 이러한 로직을 별도 계층으로 격리시켰을 경우 재사용이 용이하고 변경에도 쉽게 대처가 가능하다.
-
도메인의 요구사항을 파악하기 쉽다.
→
UseCase 만 잘 분리해놓아도 신규 입사자 혹은 타 부서에게 도메인과 관련된 커뮤니케이션을 진행할 때 해당 도메인이 무슨일을 수행하는지 쉽게 파악이 가능하다.
UseCase Rule
UseCase 라는 개념을 도입한 것이 처음이다보니 팀원 각자의 이해도 및 해석이 다를 수 있어, 처음부터 너무 강한 규칙을 정하지는 않았다. 초창기에 UseCase 작성시 지켜야하는 룰은 다음과 같았다.
- UseCase 는 Android 와 연관된 의존성이 들어갈 수 없다.
- UseCase 는 실행할 수 있는 단일 public 메소드만 외부에 제공한다.
- UseCase 에 Repository 를 주입할 때는 인터페이스 타입으로 받는다.
1번 규칙을 강제하기 위해, UseCase 를 포함한 도메인 계층은 애초에 다른 모듈로 분리하였다. 해당 모듈은 어느 모듈도 의존하지 않는다.
3번 규칙에 대한 내용은 밑에서 따로 다룰 것이다.
UseCase in UseCase
UseCase 규칙에 대해 다룰때 추가로 논의가 되었던 내용이 있는데, ‘UseCase 는 다른 UseCase 를 주입받을 수 있는가?’ 였다. 나는 가능하다고 생각한다. 가령 다음과 같은 예를 들 수 있다.
외부에서는 ‘댓글 작성’ 이라는 UseCase 를 수행하고자 한다. 세부 내용은 알지 못한다. 그런데 실제 댓글 작성은 다음과 같은 순서로 진행된다.
- 이미지가 포함된 댓글일 경우 ‘이미지 URL 을 받아오는’ UseCase 가 필요하다.
- 이미지 URL 을 포함한 ‘내용을 전달하여 댓글을 등록’하는 UseCase 가 필요하다.
그럼 댓글 UseCase 는 UseCase 1, 2 를 주입받아 ‘댓글 작성’ 이라는 좀 더 추상화된 과업을 수행할 수 있다. 즉, 2 가지 형태의 UseCase 가 존재한다고 생각한다.
- Low level UseCase - 단일한 비즈니스 로직을 수행한다. ex/ 이미지 URL 을 받아온다.
- High level UseCase - Low level UseCase 를 조합하여 비즈니스 로직을 수행한다. ex/ 댓글을 작성한다.
여기서 포인트는 High level UseCase 는 High level UseCase 를 참조해서는 안될 것이다. 순환 참조 이슈가 있고 관리가 어려워진다.
UseCase in Repository
UseCase 규칙 중 3번 내용 ‘UseCase 에 Repository 를 주입할 때는 인터페이스 타입으로 받는다.’ 이 필요한 이유에 대해 궁금할 수 있다. 해당 규칙이 생긴 히스토리는 다음과 같다.
클린 아키텍처의 핵심인 ‘의존성 규칙’에 따르면 내부 원에 속하는 Domain 은 외부원인 Data 계층의 존재에 대해서 알지 못하지만 통신이 필요하다. 이러한 상황에서 로버트 마틴은 DIP 원칙을 활용한 방법을 제시한다.
즉, 도메인 계층에서는 인터페이스에 의존하고, Data 계층은 인터페이스를 구현하여 의존성을 역전시킨다.
Domain 모델 Mapping 은 필수?
로버트 마틴은 데이터가 계층을 이동할 때마다 데이터를 해당 계층에 맞는 형태로 Mapping 할 것을 권장했다. 하지만 그럴 경우 무수한 수의 Mapper 및 Mapping 로직이 들어가기 때문에 선택사항으로 남겨두자…라고 마무리 될 뻔했으나 실제 구현을 해보니 필수임을 깨달았다.
이유는 의존성 규칙을 깨지 않기 위함이다. 의존성 방향과 다르게 데이터의 flow 는 Data → Domain 계층으로 움직이는데, 이때 Domain 모델로 Mapping 이 이루어지지 않으면 결국 도메인 계층에 Data 계층 모델 의존성이 생기기 때문이다. 따라서 mapping 이 없을 경우 애초에 모듈레벨로 분리했기 때문에 컴파일이 실패한다.
만약 data 와 domain 계층이 같은 모듈에 있을 경우 domain 계층에 data 모델을 그대로 써도 컴파일은 되겠으나.. 규칙을 깨는 예외가 한번 생기면 이후로는 깨기 쉬운법이다. 따라서 의존성 규칙을 지키기 위해 mapping 은 필수임을 깨달았다.
추가로, 아래 다이어그램에서 Translater (= Mapper) 가 domain 계층에 존재하는 것으로 표시되고 있고 많은 아티클에서도 동일하게 주장하고 있는데, 실제로 Mapper 는 data 계층에서 수행되어야 한다.
이유는 마찬가지로 의존성 규칙 때문이다. Mapping 을 수행하려면 Data 계층 의존성과 Domain 계층 의존성을 모두 알고 있어야 하는데 (Data → Domain 이므로) 각 의존성을 모두 알고 있는 계층은 Data 계층밖에 없기 때문이다.
Domain 계층을 설계 할때..
일반적으로 Domain 계층 이전에 Data 계층이 존재하고 있던 경우가 많으므로 Domain 계층 설계시 Data 계층의 데이터 구조를 따라오는 경우가 많다. 그런데 케이스가 많지는 않겠지만, 클라에서 정의한 도메인과 서버에서 정의한 도메인의 범주가 다를 수 있다. 따라서 이때 기계적으로 Data 계층의 구조를 옮겨온다면 클라이언트는 계속 서버의 데이터 구조에 영향을 받을 수 밖에 없다.
로버트 마틴이 프레임 워크와 DB 등을 외부 세부사항이라고 정의한 것처럼 서버의 데이터 구조도 도메인 계층 기준으로는 세부사항에 가깝다. 따라서 도메인계층의 데이터 구조 (= 클라이언트 도메인 구조)를 먼저 설계하고 데이터 계층에서 도메인 계층으로 데이터 전달 시 이를 반영해주는 것이 맞다고 생각한다. 물론 이과정에서는 불필요한 API 호출이 늘어날수도 있는데, 이는 적절한 캐시 정책 등을 활용하여 풀 수 있지 않을지.. 팀원분이 아이디어를 주셨다.
개인적으로 가장 고민이 많이 되고 있는 부분이라 추후에 관점이 바뀔 가능성이 높다.
DI 라이브러리 도입
클린 아키텍처 기반으로 구현하려면 의존성 주입이 많기 때문에 DI 라이브러리를 사용하지 않을 경우 글루코드에 고통 받을 수 있다. 만약 도입하지 않았다면 이번 기회에 같이 도입해보는 것도 좋지 않을까.
new GetUseCase(new Repository(new Service())) ...
도입을 진행하며..
클린 아키텍처는 이름과 다르게 코드가 클린해지지는 않는다. 오히려 계층이 엄격하게 분리되고 계층간 통신을 위해 추가되는 코드들이 많아진다. 하지만 장기적으로 유지보수하는 관점에서는 실보다 득이 더 크다고 느끼고 있다. 자유도가 있는 아키텍처 이기 때문에 개별 프로젝트 상황에 맞게 취할 수 있는 전략이 달라서, 딱 맞는 해결책을 찾기 어려운 것 같다. 개인적으로 주단위로 계속 생각이 바뀌고 있어서 어느정도 성숙하게 적용하려면 시간이 필요하다고 느낀다.
잊지 말아야할 것은 클린 아키텍처 도입하는 것 자체가 중요한 게 아니라 클린 아키텍처를 통해 실제 우리가 겪고 있는 어려움을 어떻게 해결할 수 있는지에 대해 집중해야 한다는 것이다.
No Silver Bullet.