MVC의 문제점
iOS 개발을 하시는 분들은 아마 MVC를 가장 먼저 사용해볼 것입니다.
apple이 설명하는 Cocoa MVC는 위의 그림과 같습니다.
Controller가 View의 life cycle, 비즈니스 로직, 화면 전환 로직 등 대부분의 기능을 하게 됩니다.
간단한 로직이 들어간 뷰에서는 별 문제가 없고 오히려 빠르게 구현할 수 있어서 괜찮지만, 뷰와 비즈니스 로직이 복잡해질수록 뷰, 비즈니스 로직, 화면 전환 로직 등이 얽혀 이해하기 쉽지 않아지는 경험을 했습니다.
또한 View와 Controller의 결합도(Coupling)이 높아서 Controller만 독립적으로 테스트를 하기에도 무리가 생깁니다.
ViewController가 20개가 넘어가는 Trinap을 개발을 진행하면서 View와 Controller 사이의 결합도를 낮출 수 있는 방법이 필요했습니다.
MVVM
ViewController를 단순하게 만들기
저희의 목표는 ViewController를 최대한 단순하게 만드는 것이었습니다.
ViewModel에서 전달해주는 값들만 뷰에 뿌려주는 플로우로 흘러가기를 원했고, 이를 위해서
input/output 패턴을 사용하기로 했습니다.
input에서 ViewController에서 발생하는 이벤트들을 ViewModel로 전달할 값들을 담고,
output에서 ViewModel에서 ViewController에게 전달해주는 값들을 담았습니다.
이를 통해 ViewController는 ViewModel이 어떤 일을 하는지 모르고 발생하는 이벤트들은 input에 넣어서 전달하고, 뷰에 보여줘야 하는 값들은 output으로 받아서 보여주기만 하면 되도록 할 수 있었습니다.
MVC에 비해서 ViewController가 상당히 멍청해졌죠?
또한 ViewModel과 ViewController의 결합도를 최대한 낮추기 위해서 RxSwift를 사용했습니다.
(RxSwift에 대한 글은 여기를 참조해주세요)
이벤트가 발생을 할 때 직접 값을 전달하고, 받아오는 것이 아닌 바인딩을 해둬서 ViewController가 ViewModel의 프로퍼티를 참조하는 의존성을 낮출 수 있었습니다.
Massive ViewModel
MVVM을 채택을 했지만 이제는 ViewModel의 역할이 너무 많아지고, 각각의 뷰에서 동일한 로직이 반복되는 문제가 생길 수 있습니다.
예를 들어, 저희 앱에서 유저의 정보를 서버에서 받아오는 로직은 정말 많은 부분에서 쓰이게 되는데요,
이러한 로직들을 하나의 객체로 만들면 재사용을 할 수 있고 중복되는 코드를 줄이고 해당 로직만 테스트를 할 수 있을 것입니다.
(같은 역할을 하는 로직이 많아질 때 함수로 빼서 사용하는 느낌이라고 보면 될 것 같습니다.)
Clean Architecture
클린 아키텍쳐는 개발 분야를 가리지 않고 사용되는 일종의 지침서(?)입니다.
로직에 대한 부분을 계층을 두고, 각각의 계층의 역할을 분리하는 것이죠.
위의 사진은 저희 앱에서 적용한 아키텍쳐 구조입니다.
아래에서 하나씩 설명해보도록 하겠습니다.
위의 양궁 과녁같은 사진을 보시면 안쪽의 원은 바깥쪽의 원에 있는 아이들을 몰라야 합니다.
즉, 안쪽에 있는 Layer는 바깥에 있는 Layer가 변하더라도 동일하게 작동을 해야 합니다.
(= 안쪽의 원은 바깥쪽에 의존성이 없어야 합니다.)
Presentation Layer, Domain Layer, Data Layer로 나눠져 있는데요,
각각의 역할에 따라 이 3가지 Layer가 나뉘게 됩니다.
Domain Layer
앱의 비즈니스 로직을 담는 Domain Layer가 가장 안쪽에 존재하게 되고, 이러한 Domain Layer에서 사용되는 정보들을 Entity라고 합니다.
Domain Layer는 Data Layer, Presenter Layer의 정보들을 모른 채 그저 비즈니스 로직만을 담당하게 됩니다.
앱의 로직에서 하나의 행동으로 볼 수 있는 역할을 UseCase로 이름을 붙입니다.
Domain Layer 내에 있는 UseCase는 다른 layer에 대한 의존성을 가지지 않게 하기 위해서 Protocol을 사용했습니다.
Data Layer의 repository를 사용할 때 내부가 어떻게 구현되었는지는 알 필요 없이 protocol에 정의되어 있는 함수들을 그대로 사용합니다.
외부에서 UseCase에 필요한 repository를 프로토콜 타입에 내부가 구현된 객체를 넣어서 보여주는 것입니다.
// DefaultUseCaseA: 프로토콜인 UseCaseA를 채택해서 구현한 객체
final class DefaultUseCaseA: UseCaseA {
// MARK: Properties
private let repositoryA: RepositoryA
// MARK: Initializers
init(repositoryA: RepositoryA) {
self.repositoryA = repositoryA
}
}
// UseCase 생성부분
//DefaultRepositoryA: 프로토콜인 RespositoryA를 채택해서 구현한 객체
let usecase = DefaultUseCaseA(repositoryA: DefaultRepotioryA)
Presentation Layer
Presentation Layer는 화면에 관련된 영역을 담당하는 Layer입니다.
앞서 말한 MVVM이 이 부분에 들어가고 뒤에 나올 Coordinator 또한 이 부분에 들어가게 됩니다.
Domain layer에서 처리된 데이터들을 이제 알맞은 형태로 보여줘야 할 텐데요, 이 부분을 Presentation Layer가 담당하게 됩니다.
우리 앱에서는 View, ViewController, ViewModel, Coordinator가 이 부분에 해당됩니다.
Data Layer
Data Layer는 데이터를 받아오는 계층을 의미합니다.
API를 통해서나 CoreData, Realm 과 같은 부분들을 의미합니다.
우리 앱에서는 DTO와 Repository가 이 부분에 해당됩니다.
외부에서 받아오는 데이터의 형태를 가지는 DTO와 해당 DTO를 받아오는 Repository가 있습니다.
여기서 Repository가 반환하는 Object가 DTO여야 하는지, Domain Layer의 Entity여야 하는지 혼란이 왔었는데요, DTO로 전달할 경우 UseCase가 DTO를 알게 되고, 이는 적절하지 않기 때문에 DTO를 Entity로 변환하는 것까지 Repository의 역할로 보았습니다.
Clean Architecture를 적용함으로써..
- 각각의 비즈니스 로직을 따로 테스트할 수 있었습니다.
- 각 객체의 역할을 프로토콜로 정의하고, Mock 객체를 구현하여 단위 테스트하기에 용이하도록 구현할 수 있었습니다.
- 프로토콜로 해당 클래스의 역할과 형태를 명시해서, 협업을 할 때 각 객체가 어떤 역할을 하는지 쉽게 파악할 수 있었습니다.
- 여러 뷰에서 동일한 로직이 사용될 때 UseCase를 재사용할 수 있었습니다.
Coordinator 패턴은 추후에 적어보도록 하겠습니다....
적용된 코드를 보고 싶으시다면 아래의 링크를 참고해주시면 감사하겠습니다.
https://github.com/boostcampwm-2022/iOS02-Trinap
GitHub - boostcampwm-2022/iOS02-Trinap: 우리가 여행을 추억하는 방법 📷, Trinap
우리가 여행을 추억하는 방법 📷, Trinap. Contribute to boostcampwm-2022/iOS02-Trinap development by creating an account on GitHub.
github.com
'🍎 iOS > 📱 iOS' 카테고리의 다른 글
BDD로 테스트하기(feat. Quick) (2) | 2023.10.29 |
---|---|
Custom Network Module에서 토큰 관리하기(RequestInterceptor) (0) | 2023.02.01 |
Queenfisher 이미지 캐시 개선기 (0) | 2022.12.30 |