외부에서 viewModel 같은 클래스 초기화시점 차이 +의존성주입복습

임혜정·2024년 8월 12일
0

1. 변수만 선언하고 나중에 초기화

private let mainViewModel: MainViewModel

이 방식은 의존성 주입이나 지연 초기화에서 자주 사용된다.
mainViewModel을 선언만 해두고 나중에 외부에서 주입한다.

class ProfileViewController: UIViewController {
    private let mainViewModel: MainViewModel
    
    init(viewModel: MainViewModel) {
        self.mainViewModel = viewModel
        super.init(nibName: nil, bundle: nil)
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        updateProfile()
    }
    
    private func updateProfile() {
        nameLabel.text = mainViewModel.userName
        emailLabel.text = mainViewModel.userEmail
        profileImageView.image = mainViewModel.profileImage
    }
}

이 방식으로 ProfileViewController 생성 시 외부에서 ViewModel 주입 가능.

2. 선언과 동시에 초기화

private let mainViewModel = MainViewModel()

간단하고 즉시 사용 가능한 것이 장점이다.
그러나 만약 ViewModel이라는 클래스의 로직이 복잡해지면 관리가 어려워질 수 있다.

class DashboardViewController: UIViewController {
    private let mainViewModel = MainViewModel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupDashboard()
    }
    
    private func setupDashboard() {
        userGreetingLabel.text = "안녕하세요, \(mainViewModel.userName)!"
        taskCountLabel.text = "할 일: \(mainViewModel.pendingTasksCount)개"
        lastLoginLabel.text = "최근 로그인: \(mainViewModel.lastLoginDate)"
    }
}

간단한 앱에서는 두 번째 방법도 괜찮지만 ..

개발자는 항상 수정과 기능의 확장을 염두해둬야하기 때문에 두번째 방법은 지양하는게 맞는 것 아닐까?

  1. 유지보수: 코드 수정이나 기능 확장 시 더 잘 대응할 수 있겠다
  2. 가짜 객체 주입이 쉬우니까 단위 테스트나 통합 테스트 시에도 좋겠고
  3. SOLID 원칙- 의존성 역전 원칙(DIP)에 있어 복잡한 의존성 관계를 더 명확하게 표현하고 관리 가능
  4. 재사용성- 다양한 상황에서 동일한 ViewModel을 재사용할 수 있어 코드량도 줄일 수 있을 것 같다
// 1. 커피만들기에 필요한 클래스 정의
class CoffeeBeans { // 커피콩과
    let type: String
    init(type: String) { self.type = type }
}

class Water { // 물이 필요
    let source: String
    init(source: String) { self.source = source }
}

// 2. 의존성 주입을 사용하는 CoffeeMachine 클래스
class CoffeeMachine {
    private let beans: CoffeeBeans
    private let water: Water
    
    init(beans: CoffeeBeans, water: Water) {
        self.beans = beans
        self.water = water
    }
    
    func brewCoffee() -> String {
        return "커피가 만들어졌어요: \(beans.type) 원두와 \(water.source) 물로 만든 커피"
    }
}

// 3. CoffeeMachine을 사용하는 CafeManager 클래스
class CafeManager {
    private let coffeeMachine: CoffeeMachine
    
    init(coffeeMachine: CoffeeMachine) {
        self.coffeeMachine = coffeeMachine
    }
    
    func orderCoffee() -> String {
        return coffeeMachine.brewCoffee()
    }
}

// 4. 실제 사용 예
let premiumBeans = CoffeeBeans(type: "콜롬비아 수프리모")
let mineralWater = Water(source: "알프스 샘물")

let fancyCoffeeMachine = CoffeeMachine(beans: premiumBeans, water: mineralWater)
let cafeManager = CafeManager(coffeeMachine: fancyCoffeeMachine)

print(cafeManager.orderCoffee())
// 출력: 커피가 만들어졌어요: 콜롬비아 수프리모 원두와 알프스 샘물로 만든 커피

뷰는 없다. 만약 이상황에서

MVC 패턴의 경우

Model: CoffeeBeans, Water 클래스
Controller: CoffeeMachine, CafeManager 클래스
View: -
premiumBeans부터 끝까지의 코드: Controller에서 Model을 초기화하고 조작하는 로직으로 볼 수 있어요.

MVVM 패턴의 경우

Model: CoffeeBeans, Water 클래스
ViewModel: CoffeeMachine, CafeManager 클래스
View: ui는 없다. premiumBeans부터 끝까지의 코드: ViewModel을 초기화하고 사용하는 부분으로, View(ViewController)

  • 추가적으로 MVVM 에서

ViewModel(CafeManager)이 Model(CoffeeBeans, Water)을 직접 다루는 대신, 데이터를 가공하여 View에 필요한 형태로 제공
View(ViewController)는 ViewModel의 데이터를 관찰하고 표시하며, 사용자 입력을 ViewModel에 전달

실제 MVVM 구현에서는 바인딩이나 리액티브 프로그래밍(*ex. rxSwift, combine)을 사용하여 ViewModel과 View 사이의 데이터 흐름을 자동화하는 경우가 많음


*ex. rxSwift, combine: 리액티브 프로그래밍을 Swift에서 구현한 대표적인 프레임워크

profile
오늘 배운걸 까먹었을 미래의 나에게..⭐️

0개의 댓글