VIPER 패턴 - Team-HGD/SniffMEET GitHub Wiki

구성

viper
  • V (View)

    The user interface, responsible for displaying data and receiving user input.

    ViewController, View를 의미하며 사용자가 보는 화면을 보여주거나 사용자 인터렉션을 Presenter에 전달한다.

  • I (Interactor)

    Contains the business logic and communicates with data sources.

    비지니스 로직을 처리하여 데이터를 준비하거나 외부에서 데이터를 가져온다.

  • P (Presenter)

    Acts as a mediator between the View and Interactor, handling user input and updating the View.

    View에서 이벤트를 전달받아 Interactor에 요청하고 View에 data와 함께 UI Update 요청을 보내거나 Router를 통한 화면 이동을 처리한다.

    View, Router, Interactor에 대한 의존성이 있다.

  • E (Entity)

    Represents the data model or entities.

  • R (Router or Wireframe)

    Manages navigation between screens.

    화면 전환을 담당하며, 다른 화면으로의 네비게이션을 처리한다. 의존성을 주입해주는 부분이기도 하다.

VIPER의 작동 방식

  • View → Presenter

    View는 사용자 입력이 발생할 때 이를 Presenter에 전달한다.

  • Presenter → Interactor

    Presenter는 View의 요청에 따라 필요한 데이터 처리를 Interactor에 요청한다.

  • Interactor → Presenter

    Interactor는 데이터 처리가 끝나면 Presenter에 처리 결과를 전달한다.

  • Presenter → View

    Presenter는 Interactor에서 받은 데이터를 가공하여 View에 전달한다.

  • Presenter → Router

    Presenter는 네비게이션이 필요할 때 Router에 요청하여 화면 전환을 수행한다.

VIPER의 장점

  1. 모듈화: 각 컴포넌트가 독립적이고 역할이 분리되어 있어 유지보수와 확장이 용이하다.

  2. 테스트 용이성: 각 컴포넌트가 독립적이므로 단위 테스트가 용이하다.

    각 요소를 protocol을 통해 의존성 분리 작업을 진행하면 용이성을 올릴 수 있다.

  3. 유연성: 각 레이어가 독립적으로 개발되므로, 특정 레이어를 교체하거나 수정하는 것이 상대적으로 쉽다.

VIPER의 단점

  1. 복잡성 증가: MVC, MVVM보다 레이어가 많아져서 초기 구현이 복잡성이 높다. 작은 프로젝트에서는 과도할 수 있다.
  2. 상호작용이 많음: 컴포넌트 간의 상호작용이 많아져서, 각 컴포넌트가 잘못 연결되면 디버깅이 어려울 수 있다. 특히 View와 Presenter는 서로를 모두 알고 있다는 점에서 양방향 통신을 수행하고 있다.
📝

VIPER에 대한 명확한 가이드가 존재하지 않는다. 데이터 전달 방식이나 Usecase를 두어 비지니스 로직을 관리하는 것을 추가하는 등 커스텀하는 건 자유일 듯!

Viper 심화 편

[GitHub - amitshekhariitbhu/iOS-Viper-Architecture: This repository contains a detailed sample app that implements VIPER architecture in iOS using libraries and frameworks like Alamofire, AlamofireImage, PKHUD, CoreData etc.](https://github.com/amitshekhariitbhu/iOS-Viper-Architecture)

게시물 리스트를 보여주는 화면에 대한 Viper 구조

제목 없음-2024-07-25-1139(3).png

View

protocol PostListViewProtocol: class {
    var presenter: PostListPresenterProtocol? { get set }
    
    // PRESENTER -> VIEW
    func showPosts(with posts: [PostModel])
    
    func showError()
    
    func showLoading()
    
    func hideLoading()
}
  • PostListViewProtocol

    Presenter가 구현한 함수로 뷰가 보여주는 방식을 결정한다.

    Presenter에서 보여질 수 있는 방식을 함수로 캡슐화해서 손쉽게 사용 가능

import UIKit
import PKHUD

class PostListView: UIViewController {
    
    @IBOutlet weak var tableView: UITableView!
    var presenter: PostListPresenterProtocol?
    var postList: [PostModel] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        presenter?.viewDidLoad()
        tableView.tableFooterView = UIView()
    }
    
}

extension PostListView: PostListViewProtocol {
    
    func showPosts(with posts: [PostModel]) {
        postList = posts
        tableView.reloadData()
    }
    
    func showError() {
        HUD.flash(.label("Internet not connected"), delay: 2.0)
    }
    
    func showLoading() {
        HUD.show(.progress)
    }
    
    func hideLoading() {
        HUD.hide()
    }
    
}

Presenter

protocol PostListPresenterProtocol: class {
    var view: PostListViewProtocol? { get set }
    var interactor: PostListInteractorInputProtocol? { get set }
    var wireFrame: PostListWireFrameProtocol? { get set }
    
    // VIEW -> PRESENTER
    func viewDidLoad()
    func showPostDetail(forPost post: PostModel)
}

protocol PostListInteractorOutputProtocol: class {
    // INTERACTOR -> PRESENTER
    func didRetrievePosts(_ posts: [PostModel])
    func onError()
}
  • PostListPresenterProtocol 프로토콜 구현체

    View의 이벤트에 따라 View를 보여주거나 Router를 활용하여 화면전환을 수행한다.

  • PostListInteractorOutputProtocol 프로토콜 구현체

    Interactor의 output에 따라 어떤 것을 보여줄지 presenter가 결정한다.

class PostListPresenter: PostListPresenterProtocol {
    weak var view: PostListViewProtocol?
    var interactor: PostListInteractorInputProtocol?
    var wireFrame: PostListWireFrameProtocol?
    
    func viewDidLoad() {
        view?.showLoading()
        interactor?.retrievePostList()
    }
    
    func showPostDetail(forPost post: PostModel) {
        wireFrame?.presentPostDetailScreen(from: view!, forPost: post)
    }
}

extension PostListPresenter: PostListInteractorOutputProtocol {
    
    func didRetrievePosts(_ posts: [PostModel]) {
        view?.hideLoading()
        view?.showPosts(with: posts)
    }
    
    func onError() {
        view?.hideLoading()
        view?.showError()
    }
}

Interactor

protocol PostListInteractorInputProtocol: class {
    var presenter: PostListInteractorOutputProtocol? { get set }
    var localDatamanager: PostListLocalDataManagerInputProtocol? { get set }
    var remoteDatamanager: PostListRemoteDataManagerInputProtocol? { get set }
    
    // PRESENTER -> INTERACTOR
    func retrievePostList()
}

protocol PostListDataManagerInputProtocol: class {
    // INTERACTOR -> DATAMANAGER
}
  • PostListInteractorInputProtocol 구현체

    Repository에서 데이터를 받아와서 이를 presenter에 해당 정보를 전달한다.

    datamanager → interactor → presenter ( in interactor) || presenter → view (in presenter)

class PostListInteractor: PostListInteractorInputProtocol {
    weak var presenter: PostListInteractorOutputProtocol?
    var localDatamanager: PostListLocalDataManagerInputProtocol?
    var remoteDatamanager: PostListRemoteDataManagerInputProtocol?
    
    func retrievePostList() {
        do {
            if let postList = try localDatamanager?.retrievePostList() {
                let postModelList = postList.map() {
                    return PostModel(id: Int($0.id), title: $0.title!, imageUrl: $0.imageUrl!, thumbImageUrl: $0.thumbImageUrl!)
                }
                if  postModelList.isEmpty {
                    remoteDatamanager?.retrievePostList()
                }else{
                   presenter?.didRetrievePosts(postModelList)
                }
            } else {
                remoteDatamanager?.retrievePostList()
            }
            
        } catch {
            presenter?.didRetrievePosts([])
        }
    }
        
}

extension PostListInteractor: PostListRemoteDataManagerOutputProtocol {
    
    func onPostsRetrieved(_ posts: [PostModel]) {
        presenter?.didRetrievePosts(posts)
        
        for postModel in posts {
            do {
                try localDatamanager?.savePost(id: postModel.id, title: postModel.title, imageUrl: postModel.imageUrl, thumbImageUrl: postModel.thumbImageUrl)
            } catch  {
                
            }
        }
    }
    
    func onError() {
        presenter?.onError()
    }
    
}

WireFrame (Router)

protocol PostListWireFrameProtocol: class {
    static func createPostListModule() -> UIViewController
    // PRESENTER -> WIREFRAME
    func presentPostDetailScreen(from view: PostListViewProtocol, forPost post: PostModel)
}
  • PostListWireFrameProtocol

    의존성 주입 역할 수행 (Component, Builder) → 이래서 RIBs가 생긴 듯 하다.

    화면 전환 수행

class PostListWireFrame: PostListWireFrameProtocol {
    
    class func createPostListModule() -> UIViewController {
        let navController = mainStoryboard.instantiateViewController(withIdentifier: "PostsNavigationController")
        if let view = navController.childViewControllers.first as? PostListView {
            let presenter: PostListPresenterProtocol & PostListInteractorOutputProtocol = PostListPresenter()
            let interactor: PostListInteractorInputProtocol & PostListRemoteDataManagerOutputProtocol = PostListInteractor()
            let localDataManager: PostListLocalDataManagerInputProtocol = PostListLocalDataManager()
            let remoteDataManager: PostListRemoteDataManagerInputProtocol = PostListRemoteDataManager()
            let wireFrame: PostListWireFrameProtocol = PostListWireFrame()
            
            view.presenter = presenter
            presenter.view = view
            presenter.wireFrame = wireFrame
            presenter.interactor = interactor
            interactor.presenter = presenter
            interactor.localDatamanager = localDataManager
            interactor.remoteDatamanager = remoteDataManager
            remoteDataManager.remoteRequestHandler = interactor
            
            return navController
        }
        return UIViewController()
    }
    
    static var mainStoryboard: UIStoryboard {
        return UIStoryboard(name: "Main", bundle: Bundle.main)
    }
    

    func presentPostDetailScreen(from view: PostListViewProtocol, forPost post: PostModel) {
        let postDetailViewController = PostDetailWireFrame.createPostDetailModule(forPost: post)
   
        if let sourceView = view as? UIViewController {
           sourceView.navigationController?.pushViewController(postDetailViewController, animated: true)
        }
    }
    
}

Entity

해당 프로젝트에서는 각 역할 별로 Entity를 구분해서 사용하지 않았다.

해당 프로젝트에서 DTO, Model, Entity를 구분해서 사용할 필요가 있을까?

사용하지 않는다면 하나의 Entity에 너무 의존적인건 아닐까?

내 의견

  1. 양방향 통신

    View ↔ Presenter

    Presenter ↔ Interactor

    위와 같이 양방향으로 통신(?)이 이루어지는 부분이 많다.

    따라서 서로 다른 방향 로직 간의 구분할 필요가 있다. ( 가독성 측면에서 또는 테스트 용이성을 위해서)

  2. Interactor

    Interactor는 화면전환을 수행하고, 비지니스로직을 수행하고 Repository에 대한 정보도 가지고 있다.

    여러 역할을 수행하고 있다. 이를 프로토콜로 구분할 필요가 있다. 유즈케이스를 이용하여 비지니스 로직이 눈에 한번에 보일 수 있게끔 작성해도 좋을 거 같다.

Ref.

https://github.com/amitshekhariitbhu/iOS-Viper-Architecture

⚠️ **GitHub.com Fallback** ⚠️