# 의존과 주입
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를 추상화한 프로토콜을 의존하게 된다. 이를 의존관계 역전이라고 한다.
의존성 주입 정의
의존성 주입이란
- 프로그램결합도를 느슨하게 되도록 하고
- 의존관계 역전 원칙을 따르게 하고
- 단일 책임 원칙을 따르게 한다.
이에 따라 클라이언트 생성에 대한 의존성을 클라이언트의 행위로부터 분리하는 것을 의미한다.
의존성 주입의 장점
- 객체간 의존성을 줄여 코드의 확장성, 재활용성 개선
- 객체간 결합도가 낮아져 유연한 코드 작성
- 위 두가지 장점으로 인해 유지보수가 쉬워진다
- 유닛테스트가 가능해진다.
# MVVM
MVVM은 Model + View + ViewModel이다. MVC에서 뷰와 컨트롤러가 각각 독립된 계층으로 구성된 형태였다면, MVVM은 뷰와 컨트롤러가 하나의 뷰로 구성되고, 합쳐진 뷰와 모델 사이를 뷰모델이 이어주는 역할을 한다. 뷰모델은 모델에 대한 로직이 정의된다.
MVC에서 MVVM으로 마이그레이션할때 할 일은 다음과 같다.
- 뷰모델에 데이터를 속성으로 저장한다.
- 저장속성 데이터를 직접 전달하기보다 전달에 대한 로직의 반환 결과를 정의하는 것이 좋다. (메서드나 계산속성)
- 데이터(모델)에 대한 인풋 동작과 아웃풋 동작을 정의한다. 인풋은 뷰로부터의 이벤트, 아웃풋은 가공된 데이터를 뷰로 전달하는 로직을 의미한다.
- 뷰로부터의 이벤트 이외에도 비동기처리 등의 함수도 인풋-아웃풋 로직에 포함된다.
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으로 분리하려고 하면 다음과 같은 의문이 생긴다.
- 뷰의 특정 요소의 값에 접근하여 이를 조건문에 활용해야함 ->
- 다른 조건에서는 뷰 이동이 이루어져야함. 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 ?? "")
}
}
텍스트필드 변경시 높이 및 무게값을 뷰모델에 업데이트 - 인풋에 해당하는 로직