본문 바로가기

[Android]/App UI 따라 만들기

[배달의민족2] 클론코딩 - 4. 약관 상세 화면(Domain 레이어)

안녕하세요! 허접 샴푸입니다.

 

이번 편은 약관 상세 화면입니다. 글이 매우 길어지는 것 같아 Domain 레이어에 대해서만 먼저 설명하도록 하겠습니다.

 

[만들고자 하는 화면]

1. 이용약관 화면

2. 과거 약관을 선택할 수 있는 하단 팝업 화면

 

[필요 사전 지식]

1. 안드로이드

2. 코틀린

* 일단 사전 지식이 없더라도 따라 만들면서 부족한 점은, 인터넷 찾아보면서, 학습하시면 됩니다.

 

[내용]

1. ViewModel, ViewModelFactory

2. MVVM, MVI 디자인 패턴

3. ViewPager2

4. UI (Event + State), State Pattern

5. Flow, Observable Pattern

6. Fragment간 통신

7. Clean Architecture, Android App Architecture (Data, Domain, Presentation Layer)

8. Coroutine

 

[시작]

그림, 색상 등과 관련한 리소스 관련 파일은 모두 맨 아래 나와있는 Github에서 확인할 수 있으니 따로 설명을 넣지 않았습니다.

1) 아키텍처

https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
App 패키지 스트럭쳐(좌) / 클린 아키텍처 그림(우) 출처: https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

- 왼쪽 그림은 저희 앱 패키지 스트럭처 입니다. / 오른쪽 그림은 클린 아키텍처 그림 입니다.

 

- 왼쪽 그림에는 총 3개의 패키지가 있습니다. data / domain / presentation

 

- Data 레이어: DB, Network API 등, 통신을 통한 데이터를 추출하는 비즈니스 로직이 담겨 있는 레이어입니다.

 

- Domain 레이어: Presentation 레이어와 Data 레이어 간 연결을 해주는 중재자 역할의 레이어입니다. Usecase를 가지고 있으며, Domain 레이어 또한 비즈니스 로직을 포함할 수 있습니다. Android 권장 앱 아키텍처에서는, Domain 레이어는 필수사항이 아니고 선택사항이지만 확장성 측면에서는 있으면 좋은 레이어라고 설명하고 있습니다.

 

- Presentation 레이어: UI 레이어라고도 하며, 화면 UI와 관련된, 상태를 지닌 상태 홀더(예: ViewModel) 등이 이 레이어에 속합니다.

 

안드로이드 권장 앱 아키텍처 / 출처: https://developer.android.com/topic/architecture

- 안드로이드 권장 앱 아키텍처에서는 위와 같이 앱을 만들면 좋다고 설명하고 있습니다.

 

- Domain Layer가 최상위 레이어가 되며, UI 레이어는 Domain Layer의 UseCase를 이용하여 Data Layer에게 데이터를 요청할 수 있습니다. 이 때문에, Domain Layer는 UI Layer와 Data Layer을 중재하는 중재자 역할을 한다고 볼 수 있습니다.

 

- 저희 앱 또한 UI Layer에서 Domain Layer에 있는 UseCase를 이용하여 비즈니스 로직을 처리하는 Data Layer에 존재하는 데이터를 돌려받습니다. 이때 돌려받는다는 것은, abstraction을 통해 동작합니다. 각 레이어는 다른 레이어가 어떻게 구현되어 있는지 알면 안 됩니다. 그래서 interface와 같은 abstraction을 이용하여 다른 레이어와 소통을 합니다. 이 이유는, 만약 구현체가 변경된다고 다른 레이어에게도 영향을 주면 레이어를 분리하는 의미가 없으며 각 레이어는 서로 의존을 한다는 의미입니다. 각 레이어는 독립적이어야 하며, 서로 다른 레이어 끼리는 영향을 주어서는 안됩니다.

 

- 그래서, Data 레이어는 Data 통신과 관련된 비즈니스 로직만을 담당하고, Domain Layer는 UseCase를 이용하여 비즈니스 로직을 캡슐화하며, Presentation Layer는 UI와 관련된 일만 담당하는 것입니다. Clean Architecture과 관련된 더 자세한 내용은 따로 다루도록 하겠습니다.

 

2) Layer 살펴보기

<Domain 레이어>

Data Layer 스트럭쳐

Domain 레이어에는

1) Domain 영역에서 사용할 Data Class 집합체인 Model 패키지,

 

2) 공용 및 공유해서 사용할 수 있는, Data 레이어와 UI 레이어를 이어주는, 비즈니스 로직을 캡슐화하는 Usecase 클래스의 집합체인 Usecase 패키지,

 

3) 각 Usecase에 필요한 Repository 추상 인터페이스를 지닌 repository 패키지가 존재합니다.

 

=====================================================================================

(Model 패키지)

- 도메인 영역에서 사용할 Data Class를 작성하는 곳입니다. Ui 레이어는 Domain 영역보다 Low-Level이며 Domain 영역에 의존하기에, Domain 영역에 존재하는 data class를 사용하여 ui를 나타낼 수 있습니다.

 

- Term이라는 Data class는 아래와 같습니다.

- Term 데이터 클래스는 id, title, content 항목을 지닙니다.

 

- 아래 그림과 같이 title은 "2022년 11월 22일 시행 안"이라는 문자열을 지니게 되며, content의 경우 약관 상세 내용을 담는 문자열입니다. id의 경우 해당 약관 항목을 고유하게 나타낼 수 있는 값입니다.

* data 영역의 model과 domain 영역의 model을 나누는 이유

1) Clean Architecture이라고 하면, 기능에 맞게 레이어를 나누는 것입니다. 프로젝트가 커지게 되면, 지금처럼 패키지 분리가 아닌 모듈로서 분리를 하게 되는데, 각 모듈(레이어)은 자신의 레이어에서 사용될 Data class를 개별적으로 가지고 있어야 합니다.

 

2) 만약 Term.kt Data class가 Domain 영역이 아닌 Data 영역에만 존재한다고 할 경우, Data 레이어 보다 상위 레벨의 레이어인 Domain 레이어에서는 해당 Data Class에 접근을 할 수 없게 됩니다.

 

3) Domain 레이어에 한정된 것이 아닌, 각 레이어는 각 레이어에 맞는 Data Class를 가짐으로써 불필요한 필드(Data Class 내에 작성되어 있는 변수)를 제거할 수 있게 됩니다. 혹은 다른 레이어에게 필요한 정보를 제공하기 위해 해당 정보를 가공해서 전달할 수 있습니다. 이때, 저희는 Mapping이라고 하여 해당 정보를 가공할 수 있게 됩니다. (아래를 보시기 바랍니다)

- 앞서서 본 Data Layer에 있는 TermApiModel 데이터 클래스입니다.

 

- 확장 함수로 Mapping을 담당하는 "asDomainModel()"이 존재합니다.

 

- 만약 아래 그림(코드)과 같이 "date"이 LocalDate 타입의 값으로 오게 될 경우, 굳이 Domain 영역에게 LocalDate 타입의 값을 그대로 전달할 필요가 없게 됩니다.

- 이때, 저희는 "asDomainModel()"이라는 도메인 영역에 맞는 Data Class로 변환해주는 함수를 사용하여 LocalDate 타입의 값을 "yyyy년 MM월 dd일 시행안" 형태의 문자열 값으로 변환해주는 것입니다.

 

- 물론, Ui 레이어에서 Model 패키지를 만들고, 해당 패키지 내부에 Presentation 레이어에서 사용할 알맞은 Data class를 만들어서 변환해주어도 되지만(왜냐하면 "yyyy년 MM월 dd일 시행안"은 Ui에 보여줄 문구이지, Domain 영역에서 필요한 값은 딱히 아니기 때문입니다.) 이 샘플 프로젝트에서는 단순히 Data 영역에서 변환을 담당하도록 하였습니다.

 

4) 위와 같이 분리를 하게 될 경우, Data 영역에서 Unit Testing을 진행한다고 하면, 네트워크 통신을 하여 받아온 값이 LocalDate 타입인지 체크하는 테스트 코드를 작성할 수 있게 되며, Domain 영역에서는 Data 레이어로부터 받아온 값이 "yyyy년 MM월 dd일 시행안" 문자열 형태의 값인지를 테스트할 수 있게 됩니다. 이렇게 분리를 통해 각 레이어에 맞는 작은 규모 단위의 단위 테스트가 가능해져 오류 발생률을 줄일 수 있는 장점을 가지게 됩니다.

 

=====================================================================================

(repository 패키지)

- Repository는 Data 영역에 존재해야 하는 것 아닌가요?

- Clean Architecture의 핵심은, 각 기능에 맞게 레이어를 분리하고, 각 레이어는 다른 레이어의 구현체에 의존하는 것이 아닌 추상화에 의존하는 것입니다. 즉, Repository의 구현체인 RepositoryImpl은 Data 영역에 존재하고, Repository 의존성을 지닌 Usecase의 경우 구현체가 아닌 추상 인터페이스에 의존하게 됩니다.

 

- 위와 같은 이유는, 만약 RepositoryImpl(구현체)가 변경될 경우, 즉 다른 레이어에 어떠한 기능이 변경된다고 한들, 현재 레이어는 영향을 받지 않도록 하기 위함입니다.

- 그래서 위 코드를 보시면, 단순히 Domain 영역에서 필요한 것은 추상 인터페이스뿐입니다.

Data 영역에 존재하는 Repository 구현체

- 더불어, 위(Data 영역에 존재하는 Repository 구현체) 코드를 보시면, 해당 구현체는 termDataSource라는 데이터 소스 클래스에 의존성을 지니고 있습니다. 즉 termDataSource가 있어야 해당 repository는 정상 구동이 가능해진다는 의미입니다.

 

- 만약 Usecase에서 interface가 아닌 구현체에 직접적으로 의존하게 될 경우, UseCase에서는 Repository 구현체의 termDataSource가 바뀔 때마다, UseCase 생성자에서는 새로운 DataSource로 변경해주어야 하는 매우 의존성이 강한, 유지보수가 어려운 코드가 탄생하게 될 것입니다. 이러한 이유로, 항상 High 레벨 모듈은 Low 레벨 모듈에 Abstraction(추상화)을 통해 접근하게 하는 것이며 이것이 바로 Clean Architecture입니다.

 

=====================================================================================

(usecase 패키지)

- 해당 패키지는 Usecase들을 모아둔 패키지입니다.

- Usecase는 서로 다른 레이어인 Ui 레이어와 Data 레이어를 이어주는 중재자 역할을 합니다.

- 또한 class 이름을 직관적으로 지은 만큼, 어떠한 기능을 직관적으로 나타내기 위한, 그 기능 및 동작에 맞는 결과 값을 리턴하는, 비즈니스 로직을 캡슐화(Data 레이어에서 동작을 수행해 리턴해주기 때문)하는 역할을 지니고 있습니다.

- 저희는 "GetBaeminServiceTermsUseCase"와 "GetLocationServiceTermsUseCase" 두 개의 usecase 클래스를 가지고 있습니다.

- 각각 "배달의 민족 서비스 이용약관"과 "위치 기반 이용약관"에 필요한 약관 값을 리턴하는 역할을 하고 있습니다.

 

그럼 Usecase는 왜 필요한 것인가?

- 프로젝트가 커지게 될 경우, 여러 다른 화면에서 같은 값을 이용하여 화면에 나타내야 할 경우가 생기기 마련입니다.

--> 예를 들어, 회원 가입할 때 보여주는 "약관" 내용과, 로그인을 한 후 설정 화면에서 보여줘야 하는 "약관" 내용이 대부분의 서비스의 경우 같습니다. 그럼 각기 다른 화면에서 매번 똑같은 네트워크 통신 API를 호출하고 받아온 값을 가공한다고 생각하면, 만약 API 내용이 바뀌게 되면 연관되어 있는 모든 화면의 코드를 수정해주어야 합니다. 이럴 때 usecase를 이용하여 여러 화면의 ViewModel에서는 똑같은 하나의 Usecase를 사용하여 매번 (다른 화면일지라도) 같은 값을 보장받을 수 있게 하는 것입니다. 이럴 경우, 약관 내용이 바뀌어도, 서로 다른 화면에서는 늘 같은 (변경된 약관 내용) 값을 받을 수 있게 되며 딱히 큰 수정이 들어갈 필요가 없게 됩니다. 여기서 이를 SSOT(Single Source Of Truth - 단일 진실 공급원)라고 합니다.

* 물론 usecase가 전체 프로젝트의 SSOT가 절대 될 수는 없지만, Domain 레이어를 바라보는 Ui 레이어 입장에서는 Usecase가 SSOT가 될 수 있습니다. 실질적으로 저희 프로젝트에서는 현재까지는, DataSource가  SSOT가 되지만, 추후 Room DB를 구축하고 Room DB가 SSOT가 될 수 있도록 점차적으로 코드를 추가하도록 하겠습니다. (Local Database가 결국 SSOT가 되어야 합니다, 만약 DB를 사용하지 않을 경우, Network 통신을 통해 값을 받아오는 역할을 수행하는 class가 SSOT가 됩니다.)

 

 

 

다음 편은 드디어 Ui 레이어입니다. 어떻게 "만들고자 하는 화면"을 만들었는지 설명드리도록 하겠습니다.

 

혹시라도 부족한 점이나,  제가 잘못 알고 있는 점이나,  궁금한 점이 있으면 댓글로 남겨주세요.

 

다음 편은 Domain 레이어에 관하여 살펴보도록 하겠습니다.

 

[Github] 코드를 다운로드하여서 실행해보시기 바랍니다.

https://github.com/DJDrama/BaeminPractice2/tree/3_termsDetailScreen

 

GitHub - DJDrama/BaeminPractice2

Contribute to DJDrama/BaeminPractice2 development by creating an account on GitHub.

github.com