Swift - MVVM(w. Clean Architecture)

Marble·2025년 1월 9일

Clean Architecture

목록 보기
4/5

MVVM

MVVM은 Model-View-ViewModel 패턴의 약자로 사용자 인터페이스(UI)와 비즈니스 로직을 분리하여 코드의 가독성과 유지보수성을 높이는 데 중점을 둔 소프트웨어 아키텍처 패턴입니다.

출처 : https://tech.olx.com/clean-architecture-and-mvvm-on-ios-c9d167d9f5b3

구성 요소

MVVM 패턴의 구성 요소는 다음과 같습니다:

  1. Model
    정의: 애플리케이션의 데이터 구조와 비즈니스 로직을 담당
    앞서 배운 Entity, Repository, UseCase 등 Domain과 Data Repositories 영역이 여기에 해당합니다.

  2. View
    정의: 사용자에게 보여지는 UI 요소
    사용자의 입력을 받고 뷰모델로 전달하며, 뷰모델로부터 받은 데이터를 화면에 표시합니다. 데이터 바인딩을 통해 뷰모델의 상태가 변경되면 자동으로 UI가 업데이트됩니다.

  3. ViewModel
    정의: 뷰와 모델 간의 중재 역할
    데이터 변환 및 비즈니스 로직을 포함하여 뷰가 필요로 하는 데이터를 준비합니다. 뷰에서 필요한 데이터를 가공한 후 결과값을 바인딩하여 뷰에 전달합니다. 앞서 배운 유즈케이스를 ViewModel에서 선언하여 사용합니다.

예시 코드

// 모델 정의
struct User {
    let username: String
    let password: String
}

// 리포지토리 인터페이스 정의
protocol UserRepository {
    func authenticate(username: String, password: String) -> Bool
}

// 리포지토리 구현
class InMemoryUserRepository: UserRepository {
    private var users: [String: String] = ["user1": "password1", "user2": "password2"]

    func authenticate(username: String, password: String) -> Bool {
        return users[username] == password
    }
}

// 유즈케이스 정의
class LoginUseCase {
    private let userRepository: UserRepository

    init(userRepository: UserRepository) {
        self.userRepository = userRepository
    }

    func execute(username: String, password: String) -> Bool {
        return userRepository.authenticate(username: username, password: password)
    }
}

// 뷰모델 정의
class LoginViewModel {
    private let loginUseCase: LoginUseCase
    var isLoginSuccessful: Bool = false

    init(loginUseCase: LoginUseCase) {
        self.loginUseCase = loginUseCase
    }

    func login(username: String, password: String) {
        isLoginSuccessful = loginUseCase.execute(username: username, password: password)
    }
}

// 뷰 (UI)
class LoginView {
    private let viewModel: LoginViewModel

    init(viewModel: LoginViewModel) {
        self.viewModel = viewModel
    }

    func onLoginButtonClicked(username: String, password: String) {
        viewModel.login(username: username, password: password)

        if viewModel.isLoginSuccessful {
            print("로그인 성공")
        } else {
            print("로그인 실패")
        }
    }
}

let userRepository = InMemoryUserRepository()
let loginUseCase = LoginUseCase(userRepository: userRepository)
let loginViewModel = LoginViewModel(loginUseCase: loginUseCase)
let loginView = LoginView(viewModel: loginViewModel)

loginView.onLoginButtonClicked(username: "user1", password: "password1") // 출력: 로그인 성공
loginView.onLoginButtonClicked(username: "user1", password: "wrongPassword") // 출력: 로그인 실패

위 코드를 위 이미지에 대입하면 User 구조체와 UserRepository 프로토콜이 Domain 내부에 Entity에 해당합니다. LoginUseCase 클래스는 Domain 내부에 UseCase에 해당하며, UserRepository 프로토콜을 채택한 InMemoryUserRepository 클래스는 Data Repository 영역에 해당합니다. 뷰모델인 LoginViewModel와 뷰인 LoginView는 Presentation 영역에 해당합니다.

이상한 부분이 있는데 위 그림에서 Data Repository는 Domain보다 바깥에 있기 때문에 고수준 모듈인 LoginUsecase는 저수준 모듈 InMemoryUserRepository를 몰라야 합니다. 하지만 loginUseCase를 선언한 부분을 보면 초기화값으로 InMemoryUserRepository 클래스를 전달합니다. 이는 클린 아키텍처 원칙을 위반하는거 같지만 LoginUseCase를 정의한 부분을 보면 프로토콜인 UserRepository 타입으로 선언했습니다. 이는 DIP 원칙을 이용한 것이며, 고수준 모듈이 저수준 모듈을 직접 의존하지 않기 때문에 클린 아키텍처 원칙을 위반하지 않습니다.

profile
개발자가 되고 싶은 공돌이

0개의 댓글