2023년 중후반부터 클린아키텍쳐가 핫하다는걸 알게 되었고 안드로이드 권장 아키텍쳐도 유사한 구조를 띄고 있다고 생각해 관심을 갖게 되어 조금씩 훑어보곤 했었는데 2024년 새롭게 시작하는 AVIRO 프로젝트에서 클린 아키텍쳐를 도입해보면서 공부한 내용을 정리해보려고 한다.
💡 클린아키텍쳐는 뭘까?
- UI 와 Database를 분리
- 외부적인 설정에 독립적
- 프레임워크에 의존적이지 않은 공통적인 코드 개발 가능
클린아키텍쳐를 적용하지 않았던 프로젝트를 보면 하나의 패키지 폴더에 UI, Data, Utils 등 여러 코드들이 혼재되어 들어있다. 이때 당시에는 Repository 와 Datasource를 구분하고 추상화하여 사용하는 방법만을 중요하게 생각했기 때문에 그 이외의 구조에 대해서는 크게 신경쓰지 않았었다.
클린 아키테쳐에서는 위 사진처럼 크게 3개의 계층을 나누고 각각의 계층이 독립적인 책임을 갖고 있으며 의존성은 안쪽으로만 향한다.
이런 아키텍쳐가 있으면 새로운 기능이 생겨서 뷰나 데이터소스가 추가 되어도 체계가 잡혀 있기 때문에 어렵지 않게 코드를 추가하고 수정할 수 있을 것 같다.
📢 사용자가 앱을 사용하면 이벤트의 발생과 그 흐름 (단방향 구조)
사용자 이벤트 → UI → 프레젠터 → 유즈 케이스 → 엔티티 → 리포지터리 → 데이터 소스
의존성이 안쪽으로 향한다?
‘의존성이 안쪽을 향한다’ 라는 말에 의하면 도메인 계층은 데이터 계층을 참조하고 있어야 한다.
하지만 실제로 도메인 계층은 데이터 계층을 참고하고 있지 않는다고 한다. (What the…??)
이유는 리포지터리에서 이루어지는 의존성 역전 법칙 때문인데
일반적으로 의존관계라 하면 상위의 추상계층이 하위의 세부 계층에 의존하는 것을 말하는데 의존성 역전은 이게 반대가 되는 것이다. 즉, 이 문제에 적용해 보면 도메인 레이어(상위)에서 레포지토리(하위)의 세부 데이터 처리 로직 함수를 사용하기 때문에 상위에서 하위 레벨에 의존해야 할 것 같지만 그 관계를 역전시켜 레포지토리의 세부사항을 의존하는 것이 아닌 레포지토리 인터페이스를 참조하고 있다.
그래서 도메인 레이어는 레포지토리 세부 사항에 관심이 없다.
그저 인터페이스만을 참조할 뿐이고 의존성 주입 등을 사용해 세부 구현체를 도메인에 주입해준다. 이렇게 설계하면 도메인 계층은 철저히 프레젠테이션, 데이터 레이어가 뭘 하든 신경 쓰지 않고 독고다이 할 수 있어진다.
그렇다면 도메인 계층만 이렇게 독고다이하면서 의존성 역전까지 하는 이유는 뭘까? 이 이유는 의존성 주입을 하는 이유와도 밀접한 관련이 있는 것 같다.
- 도메인 코드가 앱에서 굉장히 중요한 핵심 코드이고 그만큼 SOILD 원칙에도 철저한 것 같다. 만약 의존성 역전을 사용하지 않고 repository 구현체에 그대로 의존하고 있다면 사용할 구현체가 바뀔때마다 해당 도메일 코드에 repositoryImp1 → repositoryImp2 이런식으로 일일히 바꿔줘야 할 것이다. 하지만 추상체를 사용하고 있다면 외부에서 의존성 주입을 통해서 도메인 코드를 건드리지 않고 수정사항을 반영해줄 수 있다
- 또 이렇게 의존성 역전을 통해서 실제 데이터를 가져오는 repository 대신 테스트 데이터를 가져오는 repository를 주입해 도메인 코드는 건드리지 않고 주입할 repository 구현체만 바꿔주면서 테스트를 하기에 용이하다
나는 클린아키텍쳐에 MVVM 패턴을 적용하고 싶었기 때문에 Viewmodel 을 사용해 뷰에 표시할 데이터를 관리하고 뷰는 뷰모델의 데이터를 옵져서 해서 이를 반영하도록 구조화 할 것 이다. 위 사진에서 프래젠터는 뷰모델에 해당할 것 같다
Presentation(UI) 계층
- 유일하게 안드로이드 프레임워크, 플랫폼에 종속되는 계층
- import android.xxx 를 사용할 수 있는 곳이라고 이해했다
View
- 사용자 인터페이스를 그리는 곳
- VIewModel에서 홀드하고 있는 데이터를 관찰하고 뷰에 반영
ViewModel
- View에 표시할 데이터를 홀드
- 여러 View에서 사용 될 수도 있음 (View : ViewModel = N : 1)
- View를 몰라야 하기 때문에 액티비티/프래그먼트 context나 리소스 등을 참조하면 안됨
Domain 계층
- 어떤 다른 계층에도 의존하지 않는 순수한 계층, 사업의 핵심적인 서비스와 관련된 잘 변하지 않는 비즈니스 로직
- Android 프레임워크에 의존하지 않는 순수한 Java 혹은 Kotlin 모듈
- 다른 외부 계층의 로직이 변경되어도 Domain에 있는 로직은 변경되지 않음
UseCase (option)
- 실제 사용자가 하는 일련의 행동들(행동들의 최소 단위)을 나열
- 뷰모델에서 재사용되는 로직을 정의하고 뷰모델에서 사용
- 데이터 출처에는 관여하지 않고 해당 유스케이스 로직을 usecase에 필요한 repository를 사용해아니라 다른 usecase도 사용할 수 있음
Repository(인터페이스)
- 위에서 말했듯 의존성 역전 때문에 repository의 인터페이스 부분을 usecase에서 참조하고 있다. 도메인 계층은 어떤 계층이나 다름 프레임워크에도 의존하지 않기 때문에 repository 인터페이스를 여기에 위치한 것 같다 (다른 계층에 위치하면 그 계층에 대한 의존성이 생기니까)
Entity
- 가장 핵심적인 고수준의 비즈니스 로직을 캡슐화 한 것이며 따라서 외부계층(Data, Presentation)의 변경사항이 생겨도 바뀌지 않음
- 도메인 계층은 독립적이기 때문에 Data 계층에서 가져온 데이터를 그대로 사용하면 아키텍쳐 원칙이 깨지므로 Entity 형태로 mapping 한 다음에 유스케이스에서 사용한다
- 사용자가 실제로 사용하는 형태로 바꿔준다고 볼 수도 있다
Mapper
- Data 계층을 통해 받은 데이터를 도메인 계층의 Entity로 바꿔주는 역할
- 도메인 계층은 다른 계층에 의존하지 않는 계층이므로 이런 과정이 필요하다
✔️ Optional 한 도메인 계층, 유스케이스 반드시 사용해야 할까?
안드로이드 권장 아키텍쳐에서 유스케이스는 옵션이지만 그럼에도 사용하는게 좋다는 의견이 많았다
나또한 처음에는 단순히 repository 를 사용해 결과를 return 하는 유스케이스도 있는데 이런게 꼭 필요할까? 괜히 앱만 비대해지게 만드는건 아닐까? 라는 생각을 했다.
하지만 유스케이스가 있을 때 확실히 어떤 행위를 해야 하는지 한눈에 파악하기가 좋고, 여러 repository를 사용하는 복잡한 로직이 필요한 경우에도 유스케이스에서 처리하도록 하고 뷰모델에서는 데이터만 받아와 라이브데이터에 셋팅해 좀더 UI 와 관련된 로직을 처리하는데에 집중할 수 있는 것 같다고 느꼈다
[참고]
Clean Architecture - Use case in Android" | 매쉬업 안드로이드 개발자
✔️ 데이터 계층에서 사용하는 Model 과 도메인 계층에서 사용하는 Entity의 구조가 동일하더라도 반드시 도메인에서 Entity를 정의하고 Mapping 을 해서 사용해야 하나?
- 우선 클린 아키텍쳐에서 도메인계층이 모델 계층을 몰라야 한다는 원칙 때문에 필요할 수도 있다. 데이터 계층의 model 구조 그대로 데이터를 domain에 전달하게 되면 domain은 데이터 계층의 mdoel을 import 하게 되면서 의존성이 생겨버린다
- 하지만! 구글 공식문서에서 말하고 있는 클린 아키텍쳐를 따를 경우, 도메인 계층에서 데이터 계층을 참조한다고 설명하고 있다. 그래서 이 부분은 관점에 따라/참고한 문서에 따라 조금씩 다를 수 있는 부분인 것 같다
- 개인적으로 성공, 실패에 따른 Mapping 클래스를 만들어 무조건 이걸로 맵핑하는건 어떨까 라는 생각이 들었다. 왜냐하면 서버 통신을 하는 경우 네트워크 통신 뿐만 아니라 통신 자체는 200으로 성공했지만 백엔드 내부에서 처리 로직에서 에러가 나는 경우가 있고 이런 경우 model 의 데이터 구조에 status code와 에러 메세지를 넣어서 주는 경우가 있다. 이런 경우도 체크해줘야 하는데 이 작업을 뷰모델에서 해주는 것은 화면 UI 로직에 대한 처리를 해주는 뷰모델에게는 너무 과한 임무인 것 같다고 생각했고, 사용자가 실제 사용하게 될 데이터를 전달하는게 mapper의 역할이라면 정말 바로 사용할 수 있는 데이터만 전달하는게 맞다고 보기 때문이다.
[참고]
Data 계층
- 하나의 데이터소스 당 한 유형의 데이터를 다룸 (ex. local , aws , kakao , 공공)
- 실제 api를 호출하거나 로컬 DB에 접근해 데이터를 가져오는 로직 구현
Repository
- 기능별로 데이터소스를 매칭해 데이터를 불러옴
Model
- restful API 를 사용해 request, response 할 때 사용되는 DTO 혹은 로컬 DB의 데이터 구조체(?)
[참고]
💡Multi Module? Single Module?
그런데 얼마전 클린 아키텍쳐를 다시 공부하면서 멀티 모듈의 존재를 알게 되었다…!
- 3개의 계층을 하나의 안드로이드 앱 모듈에 넣는 것 아니라 각각의 독립된 모듈로 설계
- 필요하다면 각 계층에서도 모듈을 더 잘게 설계
- 클린아키텍쳐의 각 계층 원칙을 준수하기 편리해짐
- 각의 모듈을 퍼즐 맞추듯 사용해 앱을 구성하기 때문에 변화에 유연하고 재사용 가능
단일 모듈로 설계해도 계층 원칙을 지킬 수고 완전히 틀린 것은 아니라고 본다.
하지만 단일모듈로 사용하게 된다면 각 계층 간의 원칙이 지켜지기 쉽지 않을 것 같다.
특히, 클린아키첵처의 도메인 계층 원칙을 다시 복기해보자면……
✔️ 앱 모듈에 다 넣는 경우
- 일단 한 모듈 안에 있다 보니 나도 모르게 다른 계층의 클래스를 import 해버릴 가능성이 있음
- 단일 모듈은 안드로이드 앱 모듈 안에 모든 계층이 들어가게 되기 때문에 어쩌면 그 자체로 안드로이드 프레임워크에 종속
- 앱 모듈 빌드 파일에 추가해놓은 여러가지 프레임워크나 라이브러리 의존성에 대해 사용하지 않더라도 운명공동체 같은 느낌을 주기 때문에 불편
✔️ 멀티 모듈로 분리하는 경우
- 프레젠테이션 레이어에서 안드로이드 앱이나 ui 관련된 라이브러리를 사용 / 데이터에서는 네트워크 통신 라이브러리만 추가해 사용하는 방식으로 꼭 필요한 라이브러리만 추가해 사용
- 가 실감하지는 못하지만 큰 프로젝트를 하다 보면 안드로이드 뿐만 아니라 다양한 플랫폼을 개발해야 할 수가 있는데 이때 안드 앱에 종속된 것이 아닌 독립적인 모듈인 domian 이나 data 모듈을 퍼즐 맞추듯 재사용할 수 있어 재사용성 측면 좋은듯
💡 나는 왜 클린 아키텍쳐를 적용했나?
- 남들 다 적용하는 기술을 공부해 적용해보고 싶음
- 안드로이드 공식 문서에서도 클린 아키텍쳐를 권장 하는 것 같아서 따르고 싶음
- 이전 프로젝트에 비해 발전된 코드 설계를 원함 (가독성, 유지보수에 좋은 설계 / 앞으로 유지보수하며 업데이트 해나갈 상황 고려)
- 직접 체감하면서 배워보고 싶음
솔직히 처음 클린아키텍쳐에 대한 문서를 읽었을 때 나열되어 있던 이 아키텍쳐의 개념이나 이점에 대해 완벽히 이해하지는 못했고 신규 프로젝트에 꼭 필요한지도 잘 알지 못하였다.
하지만 이제는 국룰이 되어버린 아키텍쳐인데 나만 모르고 있는 것도 마음에 걸렸다. 또 나는 몸으로 체감하지 못하면 잘 배우지 못하는 편이라 일단은 무작정 적용해보고 싶었다.
작년 프로젝트에서 사용했던 안드로이드 권장 아키텍쳐에서 가장 중요하게 생각했던 것은 Repository 와 Datasource의 분리였고 코드를 짜면서 데이터 호출 로직에 대한 관리가 수월해졌다고 느껴서 즐거움을 느끼며 이런 구조의 이점으만큼은 이해했다고 생각했다. 그래서 클린 아키텍쳐도 해보면 비슷할 거라고 생각했다.
신규 프젝의 경우 데이터소스도 다양해졌고 규모가 꽤 큰 편이라고 생각했기 때문에 작년에 진행했던 PHODO 프로젝트에 비해 더 가독성 좋고 유지보수하기 편리한 코드를 짜고 싶었다.
💡 그래서 지금 잘 적용했나?
아무래도 체감하면서 배우보자는게 첫 시작이다 보니 클린 아키텍쳐에 대해서 제대로 공부한건 이 글을 작성하면서이다. 그래서 사실 위에서 장황하게 정리해놓은 내용들 중 많은 부분을 이번에 새로 알게 되었고 현재 50% 정도 개발을 진행해오고 있는 이 시점에서 내 적용 사례는 클린 아키텍쳐를 그다지 완벽하게 지키고 있지는 않다.
1. 단일 모듈로 구현
2. 도메인 계층에서 mapper 사용해 entity 바꿔주고 있지 않음
나는 mapper를 사용하지 않고 대부분 data 계층으로부터 받은 데이터를 그대로 ui 레이어에 흘려보내 사용하고 있다. 이는 위에서 설명했던 도메인 계층이 어떤 계층도 참조해서는 안 된다는 원칙에 어긋난다.
❓ 재미있는건 어떤 계층도 참조해서는 안 된다는 원칙이 또 절대 진리는 아니란것 (What the....?)
도메인 계층은 완전 독립 -> 밥 아저씨라는 클린 아키텍쳐의 대가에 의한 원칙이고
도메인 계층이 데이터 계층을 참조 -> 구글 공식문서에서 말하는 클린아키텍쳐
나도 뒤늦게 다른분의 블로그를 보고 발견한 사실이다…..
그래서 굳이 말하자면 내가 그렇게 잘못하고 있지는 않다,,,,,? 라는 생각이 들었다
지만 대부분 밥 아저씨의 클린아키텍쳐를 많이 따르고 있는 것 같다. 그리고 Mapper 를 사용해 무조건 데이터 계층의 model을 도메인 계층의 entity로 바꿔줄 필요가 있는가에 대해서는 스스로 생각을 정리해본 결과 Success/Fail 에 따라 usecase에서 동일한 구조의 데이터를 전달하는게 좋겠다고 결심했다.
✔️ Response 구조와 통일화에 대한 고민.....
이번 프젝에서는 백엔드에서 넘겨주는 response 구조가 다 다르다 보니 매번 다른 response model을 만들어야 하고 그 구조에 따라 어떻게 정상, 실패 처리를 해줘야 할지, 재사용할 수는 없을지 고민이 많이 되었다.
나는 무조건 response 데이터를 api로부터 받을 때부터 통일된 형태로 데이터를 받을 수 있어야 한다고 생각했다. 하지만 너무나도 다 너무나도 제각각인 데이터를 모두 통합할 수 있는 model을 만들기가 어려웠다.
또 통신에러가 아닌 백엔드 내부에서 보내주는 에러를 어디서 어떻게 판단해서 통합된 데이터 구조로 만들 수 있을까? 이런 고민들을 많이 했었다.
그러다가 결국 위 1번 사진 같은 형태로 오는 데이터는 repository에서 statusCode를 체크해 MappingResult 라는 구조로 만들어 사용했고, 그 외의 형태로 오는 데이터는 그냥 그대로 viewmodel 까지 보내서 거기서 statusCode를 체크해줬다.
하지만 이번에 클린아키텍쳐를 공부하면서 이런 Mapper를 모두 적용해 뷰모델에서는 success/fail 로만 판단해 데이터를 처리하도록 해야겠다는 결심이 섰다.
또 1번 형태로 오는 데이터에 대해 MappingResult를 만들어준 것 처럼 2,3번 형태의 데이터를 맵핑해줄 수 있는 Mapper 클래스를 새로 만들면 된다는 생각을 하게 됐다.
💡 전체 회고
사실 아직까지 “클린아키텍쳐를 얼마나 완벽하게 지켜야 하는지” 에 대해서 나는 완전히 체감하고 있지는 못하다고 생각한다. 아직 앱이 출시하지도 유지보수 하고 있지도 다양한 플랫폼을 지원하거나 다른 안드로이드 개발자들과 협업하고 있지도 않기 때문이다.
또 클린아키텍쳐는 모든 개발자들이 참고할 수 있는 아키텍쳐 패턴 중 하나일뿐 안드로이드 환경에서 어떻게, 얼마나 적용하느냐는 조금씩 다를 수 있다고 생각한다. 때론 아키텍쳐의 모든 부분을 다 구현해내지 못할 수도 있고 프로젝트에 맞게 룰을 만들어서 구현해내는게 가장 베스트라고 생각한다.
솔직히 나는 지금도 머리가 터질 것 같다. 이해할 때까지 꽤 깊게 공부하다 보면 자꾸 다른 의견이 나오고 파생 기술까지 나오는 것 같다. 솔직히 이 모든걸 다 지킬수고 없고 사람 의견에 휩쓸려서 계속 프로젝트를 바꾸고 싶지는 않다. 그래도 조만간 공부 겸 경험쌓기 목적으로 이 프로젝트에 mapper 를 추가하고, 멀티모듈도 시도해 볼 계획이다.
일단 이걸로 클린아키텍쳐에 대한 정리는 이쯤으로 마무리하려고 한다.
[참고자료]
'Android' 카테고리의 다른 글
[Android/세미나실] 안드로이드를 더 잘하기 위한 자료 모음집 (0) | 2024.08.12 |
---|---|
[안드로이드/BottomSheet] Persistent Bottom Sheet 외부 화면 클릭 기능 구현 (0) | 2024.02.28 |
[안드로이드/RecyclerView] 리사이클러뷰 + 데이터바인딩 + MVVM 적용하기 (0) | 2024.02.03 |
[안드로이드/소셜로그인&Oauth] 안드로이드에서 애플(Apple Sign-In) 로그인 구현하기 (1) | 2024.01.29 |
[안드로이드/DI] 의존성 주입(DI) & Hilt 시작해보기! (0) | 2024.01.09 |