Swift/개념 & 응용

[Swift] Protocol을 이용한 ViewModel 의존성 주입

유정주 2023. 3. 18. 16:33
반응형

* 초보 개발자의 학습 내용이므로 잘못된 점이 있을 수 있습니다.

잘못된 점을 댓글로 남겨주시면 정말 도움이 많이 됩니다.

 

필요성

MVVM에서는 ViewController가 ViewModel에 의존하고 있는 구조가 많습니다.

코드로 표현하면

class ViewController {
    var viewModel = ViewModel()
 	...   
}

 

이렇게 ViewController 내부에서 ViewModel을 직접 생성하는 구조입니다.

이런 구조는 ViewModel이 없으면 ViewController를 테스트할 수 없고,

ViewController 재활용도 어렵다는 단점이 있습니다.

 

예를 들어, 리스트를 보여주는 ViewController가 있다고 합시다.

이 리스트는 영화 리스트가 될 수도 있고, 맛집 리스트가 될 수도 있습니다.

아이템 정보를 가져올 ViewModel을 각각 MovieViewModel, RestaurantViewModel라고 할 때,

  1. MovieListViewController 클래스 + MovieViewModel
  2. RestaurantListViewController 클래스 + RestaurantViewModel

이렇게 따로따로 ViewController 클래스를 만드는게 아니라,

하나의 ViewController 클래스에 MovieViewModel과 RestaurantViewModel을 주입해서 사용하면

ViewController를 재활용할 수 있게 됩니다.

 

이번 포스팅에서는 Protocol을 이용해 ViewModel 의존성 주입을 하는 방법을 알아봅시다.

 

ViewModelable 프로토콜 정의

ListViewModel에 필요한 동작을 정의한 프로토콜을 정의합니다.

ListViewController에 주입되는 모든 ListViewModel은 이 프로토콜을 준수해야 합니다.

 

protocol ListViewModelable {
    associatedtype T
    func fetch() -> [T]
}

저는 간단하게 데이터를 가져오는 fetch 메서드만 넣었습니다.

ViewController에서 호출할 모든 메서드는 이 프로토콜에 정의되어 있어야 합니다.

동적 타입을 지원하려면 associatedtype이 필요하니 위 예시처럼 적용하면 됩니다.

 

ViewModel 정의

이제 ListViewModelable 프로토콜을 준수하는 ViewModel을 정의합시다.

struct IntListViewModel: ListViewModelable {
    var arr: [Int] = [1, 2, 3, 4, 5]
    func fetch() -> [Int] {
        return arr
    }
}

struct StringListViewModel: ListViewModelable {
    var arr: [String] = ["A", "B", "C", "D", "E"]
    func fetch() -> [String] {
        return arr
    }
}

(Movie와 Restaurant 대신 Int와 String으로 바꿨습니다 ㅎㅎ;)

IntListViewModel은 Int타입 배열을 가지고 있는 ViewModel이고,

StringListViewModel은 String 타입 배열을 가지고 있는 ViewModel 입니다.

둘 다 ListViewModelable을 채택하고 있고 fetch 메서드를 구현하고 있어요.

 

associatedtype은 원래 typealias를 넣어서 어떤 타입인지 명시해줘야 하는데요.

fetch 메서드의 반환형처럼 충분히 추론이 가능하다면 위 코드처럼 생략해도 됩니다.

 

네이밍도 신경쓰면 좋습니다.

ListViewModelable이니 OOOListViewModel로 네이밍을 하면

이름만 봐도 List에 관련된 ViewModel이라는 것을 알 수 있습니다.

 

 

이제 이 ViewModel들을 어떻게 사용할지 봅시다.

 

ListViewController 정의

ViewModel을 사용할 ViewController를 만듭시다.

final class ListViewController {
    typealias AnyListViewModelable = any ListViewModelable
    
    var viewModel: AnyListViewModelable
    
    init(listViewModelable: AnyListViewModelable) {
        self.viewModel = listViewModelable
    }
    
    func start() {
        let arr = viewModel.fetch()
        print(arr)
    }
}

(이름만 ViewController인 클래스입니다... ㅎㅎ;;)

ViewController에는 ViewModel을 ListViewModelable 프로토콜 타입으로 선언합니다.

(associatedType을 사용하기 때문에 any 키워드를 붙여야 합니다.)

그러면 ListViewModelable을 준수하는 모든 타입을 ViewModel로 받을 수 있게 되고,

ViewController의 재활용성이 높아집니다.

 

의존성을 주입하는 방법은 세 가지가 있습니다.

  1. ViewController 인스턴스 프로퍼티로 직접 주입하는 방법
  2. 생성자로 주입하는 방법
  3. 메서드로 주입하는 방법

저는 2번 생성자로 주입하는 방법을 사용했고,

아래에서 3번 메서드로 주입하는 방법도 소개하겠습니다.

 

마지막으로 viewDidLoad( ) 역할을 할 start 메서드를 하나 넣었습니다.

start 메서드에서는 viewModel의 fetch 메서드를 호출해서 데이터를 출력합니다.

 

호출부 작성

ViewController와 ViewModel을 생성해서 사용해 봅시다.

 

먼저 주입할 ViewModel을 생성합니다.

let intListViewModel = IntListViewModel()
let stringListViewModel = StringListViewModel()

 

이제 ListViewController를 생성할 때 이 ViewModel들을 주입하면 됩니다.

let intVC = ListViewController(listViewModelable: intListViewModel)
intVC.start() //[1, 2, 3, 4, 5]

let stringVC = ListViewController(listViewModelable: stringListViewModel)
stringVC.start() //["A", "B", "C", "D", "E"]

 

하나의 ListViewController 클래스에 각자 다른 ViewModel을 주입해준 결과,

다른 타입을 가진 배열을 출력할 수 있게 되었습니다.

 

프로퍼티나 메서드로 주입을 하면 하나의 인스턴스로도 가능합니다.

ListViewController에 changeViewModel 메서드를 추가합니다.

final class ListViewController {
	...    
    func changeViewModel(_ viewModel: AnyListViewModelable) {
        self.viewModel = viewModel
    }
}

매개변수를 통해 ViewModel을 주입 받습니다.

 

let vc = ListViewController(listViewModelable: intListViewModel)
vc.start() //[1, 2, 3, 4, 5]

vc.changeViewModel(stringListViewModel)
vc.start() //["A", "B", "C", "D", "E"]

changeViewModel 메서드로 주입해주면 하나의 인스턴스로 여러 ViewModel을 사용할 수 있습니다.

다만, ViewController가 어떤 ViewModel을 가지고 있는지 명확하지 않다는 단점도 있습니다.

 

마무리

이번 포스팅에서는 프로토콜을 이용해 ViewController에 ViewModel을 주입하는 방법을 알려드렸습니다.

보통 앱은 1개의 ViewController에 1개의 ViewModel이 매핑되는 경우가 많아서 다른 ViewModel로 교체하는 경우는 드뭅니다.

그래서 ViewModel정도는 프로토콜이 아니라 상세 타입으로 선언하는 경우도 많은데요.

하지만 ViewController를 재활용할 수 있다는건 강력한 장점이니 알아두면 반드시 도움이 되실겁니다.

 

또, 이 주제는 의존성 주입이라는 하나의 단어로 표현할 수 있는데요.

이 포스팅에서 다루는 방법으로 ViewModel에 UseCase를 주입하거나, UseCase에 Repository를 주입할 수 있습니다.

따라서 더 많은 의존성 주입이 궁금하시다면 "의존성 주입", "DIP", "SOLID" 등의 키워드로 검색하시면 됩니다.

(저도 차근차근 포스팅 할 예정 ㅎㅎ)

 

감사합니다!


아직은 초보 개발자입니다.

더 효율적인 코드 훈수 환영합니다!

공감 댓글 부탁드립니다.

 

 

반응형