ProjectWithGitAPI
안녕하세요!
이전에 혼자 Git API를 이용한 작은 프로젝트를 진행했습니다.
당시에는 MVVM 아키텍처로 구현하려고 했는데....
진행하다 보니 스스로 아키텍처 패턴에 대한 이해가 낮은 게 느껴지더라구요.
그래서 아키텍처 패턴을 다시 학습하고!
동일한 기능을 다양한 아키텍처 패턴으로 구현하는 프로젝트를 진행하려고 합니다.
(최대한 아키텍처 패턴을 준수하도록 신경 쓰면서 코드를 짰습니다.)
해당 프로젝트가 규모는 작아도 Rest API, 네트워킹, 이미지 Caching, 페이징 처리,
외부 라이브러리 사용 등등 배웠던 기술들은 거의 다 적용되었습니다.
그래서 ProjectWithGitAPI 라는 이름으로 아키텍처 패턴을 공부하기 위한 프로젝트로 채택하게 되었습니다.
MVP
이번 프로젝트는 MVP 로 진행되었습니다.
MVP는 Model - View - Presenter로 역할을 분담하는 아키텍처 패턴입니다.
아래의 그림과 같이
- View는 UI를 담당합니다.
- Presenter는 프레젠테이션 로직과 비즈니스 로직을 담당합니다.
- Model은 비즈니스 로직을 담당합니다.
기존에 다뤘던 MVC나 MVVM 처럼 Model과 View 사이를 Presenter라는 중간 단계에서
관리하는거 같은데 무엇이 다를까요?
1. Presenter는 View의 UI 업데이트를 직접 지시합니다.
MVVM의 View도 비즈니스 로직을 제외한 수동적인 형태지만
MVP는 그보다 더 수동적이라고 볼 수 있습니다.
MVVM에서는 데이터가 바인딩 처리가 되어 있어서
ViewModel에서 비즈니스 로직을 처리하고 데이터의 변화를 View에 알리지만
MVP에서는 Presenter가 비즈니스 로직을 처리하고
대응하는 UI 업데이트를 View에 직접 지시합니다.
2. View와 Presenter는 Protocol로 소통합니다. (반드시는 아닙니다)
View와 Prensenter에 각각 Protocol을 적용하여 대응하는 함수를 동작시킬 수 있습니다.
아래와 같이 View에서 Output에 대응할 UI 로직을 정의합니다.
그리고 유저의 Input을 전달할 MainViewInput을 선언하여
presenter.buttonTapped()와 같이 유저의 Input을 Presenter로 전달할 수 있습니다.
// Presenter의 Output에 대응할 Protocol
protocol MainViewOutput: AnyObject {
func bindPresenter(presenter: MainViewInput)
func showEmptyView()
func hideEmptyView()
}
class MainViewController: UIViewController {
// 유저의 Input을 전달할 Input Protocol
private var presenter: MainViewInput
// 의존성 주입
init(presenter: MainViewInput) {
self.presenter = presenter
super.init(nibName: nil, bundle: nil)
bindPresenter(presenter: presenter)
}
func somethingTapped() {
presenter.searchUser(userID: "Dev_Kang")
}
}
// View에서 Presenter의 Output에 대응할 Protocol 적용
extension MainViewController: MainViewOutput {
func bindPresenter(presenter: any MainViewInput) {
presenter.bindView(view: self)
}
func showEmptyView() {
emptyView.isHidden = false
}
func hideEmptyView() {
emptyView.isHidden = true
}
}
그리고 Presenter에서는 아래와 같이
비즈니스 로직을 포함한 유저의 Input에 대응하는 함수를 작성합니다.
그리고 필요에 따라 Model에 작업을 지시합니다.
작업이 끝난 후 View?.showEmptyView()처럼 Output Protocol에 정의된
UI 업데이트 함수를 Presenter에서 직접 지시합니다.
protocol MainViewInput {
func bindView(view: MainViewOutput)
func searchUser(userID: String)
}
final class MainPresenter {
// MARK: - Field
weak var view: MainViewOutput?
}
// MARK: - Extensions
extension MainPresenter: MainViewInput {
func bindView(view: any MainViewOutput) {
self.view = view
}
// 네트워킹 함수
func searchUser(userID: String) {
networkProvider.fetchUserData(userID: userID, page: nil) {[weak self] userInfoList in
// 네트워킹 작업 후 빈 값이면 EmptyView를 보여준다.
if userInfoArray.isEmpty {
self?.view?.showEmptyView()
} else {
self?.view?.hideEmptyView()
}
}
}
}
+ MainViewOutput이 AnyObject 타입인 이유는 강한 참조 방지를 위해서입니다.
View와 Presenter가 서로 강하게 참조해서 순환 참조를 하게 된다면,
View가 메모리에서 해제되어야 할 상황에 해제되지 못하고
메모리에 남아있어 메모리 누구가 발생할 수 있습니다.
따라서 weak 선언을 해줘야 합니다.
그런데 AnyObject 타입을 명시하지 않으면 weak var로 선언했을 때
아래와 같이 오류가 발생하는 모습을 볼 수 있습니다.
해석하자면 weak는 클래스 범위(참조 타입)에 사용 가능하니까
참조타임을 준수하도록 해라~ 라는 뜻입니다.
생각해보면 약한 참조를 의미하는 weak는 참조 타입에서만 사용가능하겠죠?
AnyObject는 클래스에 의해서만 채택될 수 있음을 의미합니다.
(클래스 타입을 나타낸다고 볼 수 있습니다.)
그래서 AnyObject를 채택하면 해당 프로토콜이 클래스에 의해서만 사용이 가능해지고
참조 타입을 준수하게 될 테니까 이제 weak 선언이 가능해지는 것입니다!!
읽어주셔서 감사합니다!
그리고 잘못된 부분에 대한 지적은 언제나 감사합니다!
아래는 ProjectWithGitAPI 프로젝트 링크들입니다.
코드를 보시면 구체적으로 어떻게 구현되어 있는지 알 수 있습니다.
MVP
https://github.com/kangsworkspace/MVPProjectWithGitAPI/blob/main/README.md
MVPProjectWithGitAPI/README.md at main · kangsworkspace/MVPProjectWithGitAPI
Contribute to kangsworkspace/MVPProjectWithGitAPI development by creating an account on GitHub.
github.com
MVVM
https://github.com/kangsworkspace/MVVMProjectWithGitAPI
GitHub - kangsworkspace/MVVMProjectWithGitAPI
Contribute to kangsworkspace/MVVMProjectWithGitAPI development by creating an account on GitHub.
github.com
MVC
https://github.com/kangsworkspace/MVCProjectWithGitAPI
GitHub - kangsworkspace/MVCProjectWithGitAPI
Contribute to kangsworkspace/MVCProjectWithGitAPI development by creating an account on GitHub.
github.com