VIPER(View-Interactor-Presenter-Entity-Router) — Mutual Mobile 회사가 iOS 애플리케이션을 위해 개발한 모듈형 아키텍처입니다. VIPER는 애플리케이션을 5개 계층으로 나눕니다: View는 표시, Interactor는 비즈니스 로직, Presenter는 데이터 준비, Entity는 데이터 모델, Router는 모듈 간 탐색을 담당합니다. VIPER는 모바일 아키텍처 중 단일 책임 원칙을 가장 상세하게 구현한 것입니다. 자세한 내용은 objc.io 기사에서 확인하세요.
핵심 요점
VIPER(View-Interactor-Presenter-Entity-Router) — 2013~2014년 Mutual Mobile에서 대규모 iOS 프로젝트를 위해 개발된 아키텍처 패턴입니다. 애플리케이션의 각 화면은 엄격하게 정의된 책임을 가진 5가지 구성 요소의 별도 모듈입니다. VIPER는 모바일 개발에서 단일 책임 원칙(Single Responsibility Principle)을 가장 엄격하게 구현한 것입니다: 어떤 구성 요소도 다른 구성 요소가 할 수 있는 일을 하지 않습니다.
View는 Presenter가 전달한 데이터를 표시하는 역할만 담당하는 수동적 구성 요소입니다. View에는 비즈니스 로직이 없고, 탐색을 처리하지 않으며, 네트워크 요청을 하지 않습니다. iOS에서는 — ViewProtocol을 가진 UIViewController입니다. Interactor는 Entity 및 서비스(네트워크, DB, GPS)와 함께 작동하는 비즈니스 로직 계층입니다. Interactor는 UIKit을 가져오지 않습니다. Presenter는 View와 Interactor 사이의 중개자입니다: Interactor에서 데이터를 받아 표시용으로 포맷하고 View에 전달합니다. Presenter도 UIKit을 가져오지 않습니다. Entity — 데이터 모델(struct, class). Router — 탐색을 관리하고, 모듈을 생성하고, 화면을 열고, 모듈 간 데이터를 전달합니다.
| 구성 요소 | 책임 | 의존성 |
|---|---|---|
| View | 표시, 애니메이션, 제스처 | UIKit(View만) |
| Interactor | 비즈니스 로직, 네트워크, DB | Entity, 서비스 |
| Presenter | 데이터 포맷팅, View 명령 | ViewProtocol, Interactor |
| Entity | 데이터 모델 | 없음 |
| Router | 탐색, 모듈 생성 | UIViewController(전환용) |
구성 요소 간의 관계는 프로토콜로 설명됩니다. ViewProtocol은 표시 메서드를, InteractorProtocol은 비즈니스 로직 메서드를, PresenterProtocol은 이벤트 처리 메서드를, RouterProtocol은 탐색 메서드를 정의합니다. 각 구성 요소는 프로토콜을 통해서만 다른 구성 요소와 통신하므로 구현을 쉽게 교체하고 격리된 테스트가 가능합니다. 평균적으로 한 화면의 VIPER 모듈에는 5개의 프로토콜 + 5개의 클래스 + 1개의 Builder/Assembler = 11개의 파일이 포함됩니다.
VIPER 모듈 구축은 Builder(또는 Assembler)에서 수행되며, 5가지 구성 요소를 모두 생성하고 프로토콜을 통해 연결합니다. Builder는 구성 요소가 서로의 구체적인 유형을 아는 유일한 장소입니다. 조립 후 View는 표시를 위해 외부로 반환되며, 나머지 체인은 깨끗하고 격리되어 테스트됩니다.
// 프로토콜 — View
protocol UserViewProtocol: AnyObject {
func display(name: String)
func display(email: String)
func showLoading()
func hideLoading()
}
// 프로토콜 — Interactor
protocol UserInteractorProtocol {
func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void)
}
// 프로토콜 — Router
protocol UserRouterProtocol {
func navigateToProfile(userId: Int)
}
// Interactor — 비즈니스 로직
final class UserInteractor: UserInteractorProtocol {
private let service: UserService
init(service: UserService) { self.service = service }
func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) {
service.fetchUser(id: id, completion: completion)
}
}
// Presenter — 데이터 준비
final class UserPresenter {
private weak var view: UserViewProtocol?
private let interactor: UserInteractorProtocol
private let router: UserRouterProtocol
init(interactor: UserInteractorProtocol, router: UserRouterProtocol) {
self.interactor = interactor
self.router = router
}
func setView(_ view: UserViewProtocol) {
self.view = view
}
func viewDidLoad() {
view?.showLoading()
interactor.fetchUser(id: 42) { [weak self] result in
guard let self else { return }
self.view?.hideLoading()
switch result {
case .success(let user):
self.view?.display(name: user.name)
self.view?.display(email: user.email)
case .failure(let error):
// 오류 처리
}
}
}
}
// Router — 탐색
final class UserRouter: UserRouterProtocol {
private weak var viewController: UIViewController?
func setViewController(_ vc: UIViewController) {
viewController = vc
}
func navigateToProfile(userId: Int) {
let profileModule = ProfileModuleBuilder.build(userId: userId)
viewController?.navigationController?.pushViewController(profileModule, animated: true)
}
}
// Builder — 모듈 조립
enum UserModuleBuilder {
static func build(userId: Int) -> UIViewController {
let service = UserService()
let interactor = UserInteractor(service: service)
let router = UserRouter()
let presenter = UserPresenter(interactor: interactor, router: router)
let viewController = UserViewController(presenter: presenter)
presenter.setView(viewController)
router.setViewController(viewController)
return viewController
}
}
Builder/Assembler는 VIPER의 핵심 요소로, 수동으로 의존성 주입을 구현합니다. 생성자를 통한 의존성 주입(constructor injection)은 구성 요소가 의존성 없이 생성될 수 없도록 보장합니다. 현대 VIPER에서 Builder는 Swinject(DI 컨테이너)를 사용할 수 있지만, 수동 조립이 테스트에 더 투명합니다. IT Sectr에서는 복잡한 로직을 가진 모듈에 수동 조립 VIPER를 적용합니다. 이는 새 개발자의 코드 이해를 단순화합니다.
Mapper(Formatter)는 VIPER의 선택적 여섯 번째 구성 요소입니다. Mapper는 Entity(DB/서버 모델)를 ViewModel(표시 모델)로 변환합니다. Entity에는 id, first_name, last_name, email 필드가 있는 UserDTO가 포함됩니다. ViewModel — name(first_name + last_name)과 email이 있는 UserDisplayItem. Mapper는 Presenter에서 실행됩니다. 데이터 매핑이 복잡한 경우(여러 Entity → 하나의 ViewModel), Mapper는 테스트를 위해 별도의 클래스로 추출됩니다.
VIPER 모듈은 격리되어 있으며 서로에 대해 알지 못합니다. 모듈 간 통신은 Router를 통해 발생합니다. 사용자가 사용자 화면에서 "프로필" 버튼을 클릭하면 Presenter가 router.navigateToProfile(userId: 42)을 호출합니다. Router는 ProfileModuleBuilder.build(userId: 42)을 통해 새 모듈을 만들고 navigationController.push를 통해 엽니다. 데이터 흐름: 모듈 A → Router A → 모듈 B Builder → 모듈 B가 생성되고 열립니다.
데이터 반환(예: 선택 화면에서 도시 선택 → 프로필 편집 화면으로 돌아가기)은 VIPER에서 위임자 또는 클로저를 통해 구현됩니다. 모듈 B는 didSelectCity(_ city: City) 메서드로 ModuleBDelegate 프로토콜을 정의합니다. 모듈 A는 이 프로토콜을 구현합니다. Router A는 위임자를 모듈 B Builder에 전달합니다. 도시가 선택되면 모듈 B는 delegate?.didSelectCity(city)를 호출합니다. 이는 UIKit 개발자에게 익숙한 표준 iOS 관행입니다.
| 시나리오 | 메커니즘 | 예시 |
|---|---|---|
| 앞으로 탐색 | Router → Builder | navigateToProfile(userId:) |
| 데이터 반환 | Delegate | didSelectCity(_:) |
| 시스템 알림 | NotificationCenter | UserDidLogout |
| Interactor의 이벤트 | Presenter → View | WebSocket 메시지 |
NotificationCenter는 여러 모듈에 동시에 영향을 미치는 시스템 이벤트(로그아웃, 요금제 변경, 푸시 알림)에 사용됩니다. Router 또는 AppDelegate가 Notification을 구독하고 필요한 모듈을 만들거나 상태를 업데이트합니다. VIPER는 NotificationCenter를 금지하지 않습니다. 중요한 것은 1대다 이벤트에만 사용하고, 1대1 통신에는 위임자 또는 클로저를 사용하는 것입니다.
VIPER 대 MVVM — VIPER는 화면당 2~3배 더 많은 코드가 필요하지만 절대적인 구성 요소 격리를 제공합니다. ViewModel + SwiftUI를 사용한 MVVM은 더 간단하고 빠르지만 5명 이상의 개발자 팀에서는 확장성이 떨어집니다. VIPER는 누가 무엇에 책임이 있는지 엄격히 정의합니다: Interactor — 비즈니스 로직만, Presenter — 포맷팅, Router — 탐색. MVVM에서 ViewModel은 종종 비대해져 탐색과 비즈니스 로직을 인수합니다.
VIPER 대 Clean Architecture — VIPER는 iOS UIKit에 적응된 Clean Architecture의 특정 사례입니다. Interactor = Use Case, Entity = Domain Model, Presenter = Presentation, Router = Controller(Robert Martin의 용어). Clean Architecture는 Interactor와 데이터 사이에 Gateway/Repository 계층을 추가하지만, VIPER에서는 일반적으로 분리되지 않습니다. 현대 SwiftUI 프로젝트의 경우 대부분의 팀이 Clean Architecture(The Composable Architecture) 또는 MVVM을 선택하고 VIPER는 UIKit 레거시에 남겨둡니다.
VIPER를 선택해야 할 때 — 5명 이상의 개발자 팀, 50개 이상의 화면이 있는 UIKit 프로젝트, 80% 이상의 테스트 요구 사항, iOS 전용(VIPER는 재작성 없이 Android에 이식할 수 없음). VIPER는 예측 가능한 구조를 제공합니다: 새 개발자는 15분 안에 모듈을 이해합니다. 그러나 파일 수가 많기 때문에 개발 속도가 MVVM보다 20~30% 느립니다. IT Sectr에서는 3명 이상의 팀이 있는 엔터프라이즈 UIKit 프로젝트에 VIPER를 사용하고 새 SwiftUI 프로젝트에는 Clean Architecture를 선호합니다.
VIPER는 테스트용으로 설계되었습니다 — 각 구성 요소는 프로토콜을 통해 격리되어 테스트됩니다. Interactor는 모의 서비스로 테스트되어 fetchUser가 올바른 ID로 호출되고 결과가 Presenter에 전달되는지 확인합니다. Presenter는 모의 View 및 Interactor 객체로 테스트됩니다. Router는 모의 탐색으로 테스트되어 navigateToProfile이 올바른 userId로 호출되고 올바른 모듈이 생성되는지 확인합니다. View는 UI 테스트(XCUITest)로 테스트됩니다.
import XCTest
final class UserPresenterTests: XCTestCase {
func testViewDidLoad_callsFetchUserAndUpdatesView() {
// Given
let view = MockUserView()
let interactor = MockUserInteractor()
let router = MockUserRouter()
let presenter = UserPresenter(interactor: interactor, router: router)
presenter.setView(view)
let expectedUser = User(id: 42, name: "John", email: "john@test.com")
interactor.result = .success(expectedUser)
// When
presenter.viewDidLoad()
// Then
XCTAssertEqual(interactor.capturedUserId, 42)
XCTAssertEqual(view.displayedName, "John")
XCTAssertTrue(view.didShowLoading)
}
}
모의 객체는 VIPER용으로 수동으로 생성되거나(캡처 속성을 저장하는 클래스) Cuckoo / Mockingbird와 같은 라이브러리를 통해 생성됩니다. 수동 모의 클래스는 특히 새 개발자 교육에 더 간단하고 명확합니다. 각 모의 객체는 캡처된 값(capturedUserId, displayedName)과 호출 플래그(didShowLoading)를 저장합니다. 테스트가 끝나면 메서드가 호출되었는지 여부뿐만 아니라 어떤 매개변수로 호출되었는지도 확인됩니다. 이는 데이터 흐름의 정확성에 대한 확신을 줍니다.
코드 커버리지는 IT Sectr VIPER 프로젝트에서 Interactor 85~95%, Presenter 90~95%, Router 70~80%, View 30~50%(UI 테스트를 통해)에 도달합니다. View는 스냅샷 테스트(SnapshotTesting, 1.5K 스타)로 테스트됩니다. 이는 XCUITest보다 빠르고 더 많은 케이스를 다룹니다. VIPER 프로젝트의 전체 커버리지는 일반적으로 70~80%로 MVVM 프로젝트(50~65%)보다 높지만, 테스트 작성에 더 많은 시간이 필요합니다(개발 시간의 30~40% 대 MVVM의 20~25%).
자주 묻는 질문
최소 11개 파일: 5개의 프로토콜(ViewProtocol, InteractorProtocol, PresenterProtocol, RouterProtocol, Entity), 5개의 구현(ViewController, Interactor, Presenter, Router, Entity) 및 Builder/Assembler. Mapper(Formatter) 포함 시 12~13개. 50개 화면 프로젝트의 경우 VIPER 모듈만 550~650개 파일입니다. MVVM은 화면당 3개의 파일(ViewModel, View, Model)이 필요하며, 50개 화면에 150개 파일입니다.
네, 이론적으로 VIPER는 Android에 이식 가능하지만 실제로는 사용되지 않습니다. Google은 Jetpack과 함께 MVVM을 권장합니다. VIPER는 iOS UIKit용으로 만들어졌으며 ViewController는 수명 주기로 인해 테스트하기 어렵습니다. Android에서 Jetpack ViewModel은 VIPER 격리 없이 테스트 문제를 해결합니다. Android에서 VIPER에 해당하는 것은 모듈/기능 분할을 사용한 Clean Architecture입니다.
VIPER는 iOS 특화 Clean Architecture 구현입니다. Interactor는 Use Case, Entity는 Domain Model, Presenter는 Presentation 계층에 해당합니다. Clean Architecture는 Interactor와 데이터 사이에 Repository/Gateway를 추가하지만, VIPER에서는 일반적으로 Interactor 내부에 구현됩니다. Clean Architecture는 Router를 규정하지 않으며, 탐색은 구현에 맡겨집니다.
아니요 — SwiftUI는 MVVM + Combine용으로 설계되었습니다. SwiftUI의 VIPER는 중복됩니다: 선언형 UI에서 화면당 5개의 구성 요소는 이점 없이 오버헤드일 뿐입니다. SwiftUI의 경우 MVVM 또는 TCA(The Composable Architecture)를 선택하세요. VIPER는 UIKit 레거시와 iOS 12 이하가 최소 버전인 프로젝트에 계속 유효합니다.
Router를 통해. 모듈 A는 router.navigateToProfile(userId: id)를 호출합니다. Router A는 Builder를 통해 모듈 B를 만들고 userId를 전달합니다. 콜백 통신은 위임자를 통해: 모듈 B가 ModuleBDelegate 프로토콜을 정의하고, 모듈 A가 이를 구현하여 Router를 통해 전달합니다. 시스템 이벤트(로그아웃)는 NotificationCenter를 통해 처리됩니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.