# 의존과 주입

A가 B에 의존적이라는 것은 B가 A의 변경에 맞춰 함께 변경되어야 하는 경우를 의미한다.

class A{
    var name: String = "박경준"
}

class B{
    var a = A()

    func printName(){
        print(a.name)
    }
}

위의 경우 A클래스의 name속성의 이름이 바뀐다거나 타입이 변경되는 경우 그에 따른 B 클래스에서의 정의를 수정해줘야 하는 번거로움이 생긴다.

주입 (injection)

주입은 외부에서 생성자와 같은 방식을 통해 값을 인스턴스 내에 할당하는 것을 의미한다.

C가 A에 의존하다가 B에 의존하게 되는 코드를 봐보자.

class A{
    var name: String = "JUN"
}

class B{
    var name: String = "PARK"
}

class C{
    var a = A()

    init(a: A){
        self.a = a
    }

    func printName(){
        print(a.name)
    }
}

let aInstance = A()
aInstance.name = "A name"

let c = C(a: aInstance)

위의 C인스턴스에 a 인스턴스를 주입한다. 현재 예시가 의존성 주입은 아니다.

# 의존성 주입

의존성 주입은 프로토콜을 기반으로 구현된다.

protocol ModuledProtocol{
    var name: String { get set }
}

class A: ModuledProtocol{
    // ModuledProtocol 채택에 따라 name속성을 구현해야 하는 책임이 생긴다.
    var name: String = "A"
}

class B: ModuledProtocol{
    // ModuledProtocol 채택에 따라 name속성을 구현해야 하는 책임이 생긴다.
    var name: String = "B"
}

// ModuledProtocol 프로토콜은 일급객체이므로 타입으로 사용 가능하다.
class C{
    var moduledProperty: ModuledProtocol

    init(moduledProperty: ModuledProtocol){
        self.moduledProperty = moduledProperty
    }

    func printName(){
        // ModuledProtocol 저장속성 구현체에는 name속성이 반드시 들어가있음
        print(moduledProperty.name)
    }
}

let moduledA = A()
let moduledB = B()

let cFromA = C(moduledProperty: moduledA)
let cFromB = C(moduledProperty: moduledB)

cFromA.printName() // 모듈 A 인스턴스의 name 출력
cFromB.printName() // 모듈 B 인스턴스의 name 출력

C클래스가 A와 B클래스에 의존하고 있는 형태가 아닌 A와 B를 추상화한 프로토콜을 의존하게 된다. 이를 의존관계 역전이라고 한다.

의존성 주입 정의

의존성 주입이란

  1. 프로그램결합도를 느슨하게 되도록 하고
  2. 의존관계 역전 원칙을 따르게 하고
  3. 단일 책임 원칙을 따르게 한다.

이에 따라 클라이언트 생성에 대한 의존성을 클라이언트의 행위로부터 분리하는 것을 의미한다.

의존성 주입의 장점

  1. 객체간 의존성을 줄여 코드의 확장성, 재활용성 개선
  2. 객체간 결합도가 낮아져 유연한 코드 작성
  3. 위 두가지 장점으로 인해 유지보수가 쉬워진다
  4. 유닛테스트가 가능해진다.

# MVVM

MVVM은 Model + View + ViewModel이다. MVC에서 뷰와 컨트롤러가 각각 독립된 계층으로 구성된 형태였다면, MVVM은 뷰와 컨트롤러가 하나의 뷰로 구성되고, 합쳐진 뷰와 모델 사이를 뷰모델이 이어주는 역할을 한다. 뷰모델은 모델에 대한 로직이 정의된다.

MVC에서 MVVM으로 마이그레이션할때 할 일은 다음과 같다.

  1. 뷰모델에 데이터를 속성으로 저장한다.
  2. 저장속성 데이터를 직접 전달하기보다 전달에 대한 로직의 반환 결과를 정의하는 것이 좋다. (메서드나 계산속성)
  3. 데이터(모델)에 대한 인풋 동작과 아웃풋 동작을 정의한다. 인풋은 뷰로부터의 이벤트, 아웃풋은 가공된 데이터를 뷰로 전달하는 로직을 의미한다.
  4. 뷰로부터의 이벤트 이외에도 비동기처리 등의 함수도 인풋-아웃풋 로직에 포함된다.
class MusicViewModel{
    var music: Music?

    var albumName: String?{
        return music?.albumName
    }

    var songName: String?{
        return music?.songName
    }

    func handleButtonTapped(_ sender: UIButton){
        // 버튼에 대한 동작 정의
    }
}

위에 정의된 뷰모델을 뷰가 소유하는 형태로 클래스를 정의한다.

class ViewController: UIViewController{
    var viewModel = MusicViewModel()

    // ..레이블 UI속성들
    func configureUI(){
        self.songNameLabel.text = viewModel.songName
        self.albumNameLabel.text = viewModel.albumName
    }

    // 이벤트 셀렉터에서 뷰모델을 호출
    @objc func buttonTapped(_ sender: UIButton){
        viewModel.handleButtonTapped(sender)
    }
}

데이터가 업데이트 된 경우 didSet에서 이를 감지하게 되는데, 매번 뷰모델에 의존하는 뷰 각각을 직접 지정하여 호출하는 형태는 확장성 있는 코드가 되지 못한다.

class MusicViewModel{
    var music: Music?{
        didSet{
            onComplete()
        }
    }

    // () -> ()는 함수 타입
    // {}는 함수 정의부
    var onComplete: () -> () = { }
}

뷰모델을 갖는 뷰에서 onComplete 클로저를 구현하면 된다.

class MusicViewController: UIViewController{
    var viewModel = MusicViewModel()

    override func viewDidLoad(){
        super.viewDidLoad()

        viewModel.onComplete = {
            // 데이터 변경 감지 후 UI업데이트
            setupUI()
        }
    }
}

화면이동

MVC에서는 뷰컨에 직접 데이터를 전달하면 됐었지만, MVVM은 뷰에서 다음 뷰모델에 데이터를 전달해야 한다. 다음 뷰에 이동 대상 뷰모델까지 할당하면 된다.

이러한 작업은 메서드를 만들어 한꺼번에 처리한다.

class MusicViewModel{
    // ...
    func getDetailViewModel() -> DetailViewMode{
        let detailVM = DetailViewModel()

        detailVM.music = self.music
        detailVM.imageURL = self.music?.imageURL

        return detailVM
    }
}

의문점

뷰마다 뷰모델을 각각 정의해야하는가? - 의존성 주입을 이때 사용하여 확장성 있는 뷰 로직 구축이 가능하다.

# MVC에서 MVVM으로 리팩토링하기 1. BMI 앱

// BMI계산하기 - 버튼 누르면(다음화면)
@IBAction func calculateButtonTapped(_ sender: UIButton) {

    if heightTextField.text == "" || weightTextField.text == "" {
        mainLabel.text = "키와 몸무게를 입력하셔야만 합니다!!!"
        mainLabel.textColor = UIColor.red
    } else {
        mainLabel.text = "키와 몸무게를 입력해 주세요"
        mainLabel.textColor = UIColor.black

        let secondVC = storyboard?.instantiateViewController(withIdentifier: "secondVC") as! SecondViewController

        self.bmi = bmiManager.calculateBMI(height: heightTextField.text!,
                                            weight: weightTextField.text!)
        secondVC.bmi = self.bmi

        secondVC.modalPresentationStyle = .fullScreen
        self.present(secondVC, animated: true)

        heightTextField.text = ""
        weightTextField.text = ""
    }
}

위의 셀렉터 함수를 MVVM으로 분리하려고 하면 다음과 같은 의문이 생긴다.

  1. 뷰의 특정 요소의 값에 접근하여 이를 조건문에 활용해야함 ->
  2. 다른 조건에서는 뷰 이동이 이루어져야함. next 뷰모델은 의존성 주입으로 구현 가능한가? -> 다음 뷰가 동일한 모델의 데이터를 기반으로 동작한다면 뷰모델이 다를 필요가 없음
@IBAction func calculateButtonTapped(_ sender: UIButton) {
    viewModel.handleButtonTapped(storyBoard: self.storyboard, fromCurrentVC: self, animated: true)

    setupMainText()

    // 다음화면으로 갈때 텍스트필드 비우기
    heightTextField.text = ""
    weightTextField.text = ""
}

func setupMainText() {
    mainLabel.text = viewModel.mainTextString
    mainLabel.textColor = viewModel.mainLabelTextColor
}

// ViewModel 함수
func handleButtonTapped(storyBoard: UIStoryboard?, fromCurrentVC: UIViewController, animated: Bool) {
    if self.makeBMI() {
        heightString = ""
        weightString = ""
        goToNextVC(storyBoard: storyBoard, fromCurrentVC: fromCurrentVC, animated: animated)
    } else {
        print("다음화면으로 갈 수 없음")
    }
}

데이터 요청은 반드시 모델로부터 가져오는걸 명심

@objc private func textFieldEditingChanged(_ textField: UITextField) {
    if textField == heightTextField {
        viewModel.setHeightString(textField.text ?? "")
    }
    if textField == weightTextField {
        viewModel.setWeightString(textField.text ?? "")
    }
}

텍스트필드 변경시 높이 및 무게값을 뷰모델에 업데이트 - 인풋에 해당하는 로직