본문 바로가기

[Android]/App UI 따라 만들기

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

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

 

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

 

[만들고자 하는 화면]

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 살펴보기

<Data 레이어>

Data Layer 스트럭쳐

- Data 레이어에는 1) Network 통신을 담당하는  Api 패키지, 2) Api 결과를 Repository에게 전달하는 DataSource 패키지, 3) Repository 구현체를 지닌 Repository 패키지, 4) Data 레이어에서 사용되는 Data class의 집합체인 Model 패키지가 있습니다.

 

(api 패키지)

- 네트워크 통신을 담당합니다. 사실상 저희 앱은 웹서버가 없기 때문에, Retrofit이나 Ktor과 같은 HTTP client를 사용하지 않습니다. 그러나 연습이기에 그럴싸하게 만들었습니다. TermFakeData라는 파일을 만들어, 해당 파일 안에 마치 서버통신을 하여 받은 것처럼의 가짜 데이터를 작성해두었습니다.

- BaeminApiService는 TermFakeData.kt에 있는 가짜 데이터를 리턴해주는 역할을 하는 인터페이스 입니다.

- BaeminApiServiceImpl는 BaeminApiService 인터페이스를 구현하는 구현체입니다.

 

(datasource 패키지)

- datasource는 말그대로 데이터를 전달하는 데이터 소스입니다.

- Repository에서 Api에 바로 요청하여 데이터를 받으면 되지 않냐? 라고 생각할 수 있습니다. 그러나 Api와 Repository 사이에 있는 이유가 있습니다.

이유 1) 앱의 규모가 커지게 되면, local db에서 데이터를 추출하는 역할의 datasource가 존재할 수 있으며, 반대로 network 통신을 통해 데이터를 추출하는 역할의 datasource가 존재할 수 있게 됩니다. 또한 repository 파일 또한 많아질 경우(각 기능에 맞게 분리한 경우), repository는 여러 datasource 중 필요한 데이터만을 뽑아올 수 있게 됩니다. 만약 datasource가 없게 될 경우, repository는 매번 리턴받은 데이터가 local db에서 온 것인지, network 통신을 통해 온 것인지 판별해야 합니다.

 

이유 2) 더 자세히 설명드리자면, localDbDataSource, networkDataSource와 같이 2개의 DataSource를 만든다고 가정해봅시다. 그럼 localDbDataSource는 앱 내 존재하는 local 데이터베이스 작업만을 담당할 수 있게 됩니다. 그리고 networkDataSource는 네트워크 통신과 관련된 데이터만을 다룰 수 있게 됩니다. 그럼 만약 DbRepository가 있고, NetworkRepository가 있을 경우, DbRepository는 localDbDataSource만을 참조하면 됩니다. DbRepository는 Db와 관련된 역할만 수행한다면 networkDataSource가 필요 없게 되죠. 반대로 NetworkRepository는 Db작업과 네트워크 작업 모두 담당해야 한다면, localDbDataSource와 networkDataSource를 모두 참조할 수 있게 됩니다. 이렇게 datasource를 만들게 되면, 여러 repository에 그 repository가 필요로 하는 datasource만을 제공하여 불필요한 코드 작성을 피할 수 있게 됩니다. 더불어, A라는 datasource를 사용하던 Repository에게 B라는 datasource라는 새로운 datasource를 제공해야할 경우 A와 B를 바꿔서 제공하면 되기에 Repository에서는 코드를 고칠 필요가 없어지게 됩니다.

 

이유 3) 또한 datasource를 만들게 될 경우, api 혹은 db로부터 데이터를 잘 받아오는 지, 해당 데이터를 repository에게 제공할 때 문제가 없는지 Test 코드를 작성해야할 경우, 단순히 데이터를 잘 받아와서 제공해주는 지의 Test 코드만을 작성하면 되므로, Test 코드량이 크지 않으며 코드 작성 또한 쉬워집니다. (Unit 테스트와 관련된 글 또한 따로 다루도록 하겠습니다.)

 

(repository 패키지)

- 해당 패키지는 Repository 구현체를 지니고 있습니다.

- Repository 구현체는 Data 레이어 모델(자료 클래스)을 Domain 레이어 모델(자료 클래스)로 변환하는 역할을 합니다.

- datasource는 단순히 데이터를 전달하는 역할을 한다면, repository 구현체는 datasource가 데이터를 잘 전달해주었는 지 판별을 하고 성공적이면, 해당 데이터를 domain 레이어에게 전달해야 하므로, 성공적일 경우 해당 데이터를 domain 레이어가 필요한 데이터로 변환(mapping)하여 제공합니다.

- Repository interface는 Domain 레이어에 존재하며, Data Layer에서는 구현체를 지니고 있습니다.

이유) Domain 레이어에 존재하는 UseCase는 Repository를 생성자 주입합니다(Constructor Injection). 그러나 Domain 레이어는 Data 레이어에 의존하지 않기에(단, Data 레이어는 Domain 레이어에 의존하기에 구현체를 작성할 수 있게 됩니다.), Repository를 Data 레이어에만 작성한다면, Domain 레이어의 Usecase는 Repository에 의존할 수 있는 방법이 없습니다. 그래서 이럴 경우 Repository interface(Abstraction)를 Domain 레이어에 작성하여, Domain Layer는 Abstraction을 통해 Data Layer와 소통하게 됩니다. 

 

(model 패키지)

- 해당 패키지는 Data 레이어에 사용될 data class를 작성하는 곳입니다.

- Data 레이어에서는 Data 레이어에서만 사용될 수 있는 자료 클래스를 지닙니다.

- "TermApiModel"이라고 작성되어 있는 파일은 Api 통신을 통해 받아오는 데이터를 지니는 역할의 자료 클래스입니다.

- 위 코드를 보시면 "asDomainModel()", "asDomainList()"라는 확장함수가 존재합니다. 이는 mapper 역할을 합니다. Clean Architecture 및 Android 권장 앱 아키텍처 가이드에서는 각 레이어(Domain, Data, Presentation)는 각 레이어에 맞게 Data Class를 가져야 한다고 합니다. 그 이유는 서로 다른 레이어는 그 레이어가 지닌 성격에 맞게 자료 클래스를 지녀야 하기 때문이죠. 예를 들어, Android Framework와 관련된 코드가 Data 레이어 혹은 Domain 레이어의 자료 클래스에 존재하면, 결국 레이어 분리가 되지 않기에 코드가 엉망이 될 수 있습니다.

- 즉 Data 레이어에서는 Domain 레이어에게 데이터를 전달하기 전에, Domain 레이어가 필요로 하는 형태의 Data class를 제공해야 합니다. 그래서 TermApiModel이라는 자료 객체를 Domain 레이어에게 넘겨줄 때는 Domain에 작성되어 있는 Term이라는 자료 클래스에 맞게 변환해주어야 합니다.

- 또한, Data 레이어에서 통신을 통해 받아오는 데이터의 경우, Domain 레이어에서는 필요없는 자료들이 있을 수 있습니다. 예를 들어, 서버에서는 A, B, C라는 데이터를 주었지만, 막상 화면에 표시할 데이터는 B와 C라고 할 경우, 굳이 Presentation Layer에게 A, B, C 데이터를 모두 전달할 필요가 없습니다. 그래서 저희는 mapper를 통해 A, B, C 데이터에서 필요한 B와 C 데이터만을 새로운 자료 객체로 변환해서 넘겨주게 됩니다. 굳이 A라는 데이터는 필요가 없고, B와 C만을 필요하기에 변환할 때, A라는 데이터는 변환하지 않으므로써 메모리를 더 절약할 수 있게 됩니다. 또한 관심사의 분리가 정확하게 이루어질 수 있습니다.

 

[흐름]

Api <--- DataSource <--- Repository : Repository는 DataSource에 의존하고, DataSource는 Api에 의존합니다.

Api ---> DataSource ---> Repository : Api의 데이터는 stream 형태로 DataSource에게 전달되고, DataSource에서는 해당 데이터를 stream 형태로 Repository에게 전달됩니다. 즉, 저희는 Coroutine을 사용하며 모든 함수들이 suspend 함수입니다. suspend라고 하면 간단히 말해 "지연"된다라고 생각하면 됩니다. Api의 데이터를 DataSource에서 관찰하며 해당 데이터를 받을 때까지 기다리며, DataSource는 그 데이터를 받게 되면, DataSource의 데이터를 관찰하는 Repository에게 전달하며 Repository에서는 DataSource가 데이터를 전달할 때까지 기다린다고 보시면 됩니다. 물론, 영영 기다릴 수 없기에 Timeout이라는 것이 존재하고 수 많은 작업들을 해주어야 합니다.(여기서 이런 자세한 내용까진 다룰 수 없기에 일단 패스하도록 하겠습니다.)

 

* Data 레이어만 파해쳐도 엄청난 것 같습니다.

혹시라도 부족한 점이나,

제가 잘못알고 있는 점이나,

궁금한 점이 있으면 댓글로 남겨주세요.

 

다음 편은 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