이 주제를 공부하느라 꼬박 하루라는 시간을 잡아먹게되었다. 사실 이 주제를 예전부터 알고있었던건 아니었고 요즘 통학하는 시간에 유튜브에서 swift관련한 이런저런 영상들을 보는데 영상하나가 DI
라는 개념에대해 설명하는 영상이 있길래 뭐지 하고 구글링을 하다가 이건 한번쯤은 정리를 해야겠다고 마음을 먹고 정리를 하기위한 공부를 해봤다
참 중요한 개념인데 왜 써야하는지
를 스스로 설득하기 위한 공부를 하는데 시간이 좀 많이 걸렸던거같다. 지금부터는 나만의 언어로 DI
라는것에 대한 설명이라고 생각해주면 좋을거같다
결론부터 이야기하면 DI
는
클래스 내부에서 필요한 객체의 인스턴스를, 외부에서 생성한 뒤 내부로 주입받는 것이다. 이 때 이니셜라이저의 타입은 프로토콜을 활용해서 내부에서는 프로토콜 메서드를 사용한다.
이다... 물론 처음 이 설명을 들으면 이해가 안된다... 나도 그랬고 그래서 아주아주 자세하게 설명을 해보려한다
우선 설명을 시작하기 위해선 우리가 평소에 클래스를 어떤 방식으로 쓰는지를 알아야한다
class ViewModel {
var name: String
init(name: String) {
self.name = name
}
}
class ViewController: UIViewController {
let viewModel = ViewModel(name: "Youth")
override func viewDidLoad() {
super.viewDidLoad()
}
}
우리는 보통 함수를 이런식으로 쓴다. 전부는 아니겠지만 ViewModel같은 경우는 그냥 ViewController안에 ViewModel의 객체를 만들어서 변수에 넣어준다. 그런데 이 코드는 문제가 생길 가능성이 있다.
예를 들어 ViewModel
의 name
이 String
이 아니라 Int
로 바뀌면 어떻게 될까? 단순히 이렇게 이야기할 수 있다. "당연히 ViewController에 error가 뜨겠지?" 하지만 이 말은 조금더 깊데 들어가보면 클래스의 생성시점에 문제가 생긴다는 의미가된다.
swift는 생성의 조건이 저장속성 초기화 -> superClass의 init호출
의 순서로 이루어지는데 애초에 ViewModel인스턴스가 생성되는데 문제가 생겼다는 의미는 저장속성 초기화가 안됬고 결국은 init에서 문제가 발생하게 된다는의미가된다
즉, 정리해보면 다른객체(ViewModel)에 의존되어있기때문에 상위클래스에 영향을 미친다. 이런 경우 ViewController가 ViewModel을 의존한다
라고 표현한다
이런 부분을 해결하기 위해 나온 개념이 주입
이다
위에 적은 코드를 아래와 같이 바꿔볼수도있다
class ViewModel {
var name: String
init(name: String) {
self.name = name
}
}
class ViewController: UIViewController {
let viewModel: ViewModel
init(viewModel: ViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
let VM = ViewModel(name: "Youth")
let VC = ViewController(viewModel: VM)
만약에 이런방식일때 만약에 viewModel의 name이 String이 아니라 Int가 되면 어떻게 될까? 아마 애초에 VM에서 문제가 발생할것이다 애초에 ViewController에 init으로 가기도 전에 문제가 발생한다 즉, ViewController에 문제가 발생하지 않는다
이 방법은 내부에서 선언된 변수를 외부에서 주입
시켜주는 방식이다 하지만 ViewModel의 name이 변한다면 당연히 ViewController의 name도 변하기 때문에 의존성
도 여전히 존재하게 된다
그러면 이런코드가 의존성
을 주입
한 코드가 되는 것이다
그러면 의존성주입
했으니까 끝난거네?라고 생각할 수 있는데 아쉽게도 그렇지 않다. swift에서는 의존성을 주입했다고 해서 DI라고 하지 않는다
한가지 원칙을 더지켜야하는데 그것이 DIP
이다 Dependency Inversion Principle
라고 하는데 의존성 역전의 원칙
?뭐 이런식으로 해석하면될거같다.
DIP 원칙에대해서 알아보면 의존 관계를 맺을 땐, 변화하기 쉬운 것보단 변화하기 어려운 것에 의존해야 한다는 원칙
을 말한다
처음에 이 말을 듣고 아니 이게 무슨 소리야...라는 생각을 했다 그런데 천천히 생각해보니 이건 원칙이니까 이해한다기보다는 받아들여야하는 영역이기는 하다... 그래도 이왕 받아들이는거 한번 자세히 저 말을 뜯어 보면
지금까지 우리가 봤던 예시 코드는 상위모듈
이 하위모듈
에게 의존하지 않도록 설계하였다. 자그러면 다음 변화하기 어려운 것
은 뭘까...? 변화하기 어려운것은 추상적
이라는 의미인데 객체와 객체 사이에서 상호작용을 정의하는 추상적인개념
이존재한다 그러면 변화하기 어려운것 == 추상적인 개념
에 의존하면 되는거다.
다 왔다. swift에서 추상화의 방법으로 protocol
이 있다. 요약하자면 swift에서 추상화
의 의미는 세부적인 구현사항이 아닌 어떤 동작을 할것인지만을 알려주는 느낌인데 대표적인 방식이 프로토콜이 있다
그러면 우리가 위에서 한 DIP원칙을 다시 보자
의존 관계를 맺을 땐, 변화하기 쉬운 것보단 변화하기 어려운 것에 의존해야 한다는 원칙
자, 우리는 지금까지 의존성
을주입
했다. 그렇게 함으로 인해서 의존성은 유지시켰고 상위모듈이 하위모듈에는 의존하지 않도록 코드를 짰다. 그런데 계속 이렇게 짜야하는데 이렇게 코드를 짤때 protocol을 이용해서 짜야한다라고 해석을 할 수 있다.
왜냐면 따라서 DIP를 만족한다는 것은 추상화를 통해서 의존성를 가진다는 것을 의미니까. 물론, 상위객체가 하위객체에 의존하는것은 주입으로 해결한 상태여야한다
protocol Base: AnyObject {
var name: String { get }
}
class ViewModel: Base {
var name: String
init(name: String) {
self.name = name
}
}
class ViewController: UIViewController {
let viewModel: Base
init(viewModel: Base) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
let VM = ViewModel(name: "Youth")
let VC = ViewController(viewModel: VM)
추상화
즉, protocol
을 이용하면 이런식으로 구현할 수 있다. 우선 이렇게 바꾸면 장점이있다. name의 타입이 string에서 int로 바뀌면 예전코드는 name을 가지고 있는 모든 class를 찾아서 들어가서 바꿔줘야했다 그런데 이렇게 하면 protocol에서 int로 바꿔주면 알아서 xcode가 protocol을 채택한 class를 알려주니까 바꾸면된다
물론 이렇게 클래스 1~2개에서는 딱히 와닿지않을 수 있지만 프로젝트를 하다보면 클래스가 몇백개가된다.... 그때마다 타입하나바꾸겠다고 모든 클래스를 뒤적이면서 찾는게 매우 번거롭다는건 아마 이 글을 보는 모든 사람이 알것이다...
이런 코드의 장점으로 더 크게 다가오는건
class SecondViewModel {
var nickName: String
init(name: String) {
self.name = name
}
}
프로토콜이 없는 상태에서 viewModel을 바꿔야하는 경우가 생기면 ViewController에 들어가서 모든 타입을 ViewModel타입을 SecondViewModel타입으로 바꿔줘야하지만
만약 이 SecondViewController
도 Base를 채택만하고있다면 그냥 ViewController를 건드릴 필요가없다 어차피 내부변수는 Base를 채택하는 클래스면 뭐든지 다들어가도 되기 때문이다.
이런 이유때문에 의존성주입(DIP원칙을지키는)을 하게되면 유지보수
가 좋아진다고 하는것이다.
그리고 의존성 역전
이라는 단어의 뜻도 어찌보면 당연한것이 결국 상위객체가 하위객체를 의존하지 않더라도 각각의 객체가 의존하고 있는데 지금 코드를 보면 각각의 객체가 서로를 의존하는것이 아니라 추상적인개념
인 protcol
에 의존하고있는 걸 알 수 있다.
상위 객체인 ViewController 멤버변수나 메서드 모두 protocol에 의존하는 방식으로 추상화가 이루어지고, name도 상위 객체인 ViewController가 구현하는 것이 아니라 protocol을 채택한 하위 객체인 ViewModel이 구현하고 있는 형식으로 역전이 일어난것이라고도 볼 수 있다. 그리고 이런거를 IOC(Inversion Of Control)
라도고 한다
그리고 또 다른 장점으로는 의존성 주입을 하게되면 객체가 의존성을 받는 시점을 컴파일타임
이 아닌 런타임
으로 늦춰줄 수 있어서 클래스가 생성된 이후에도 변경사항을 계속 해줄 수 있는 유연성이 생긴다.
컴파일타임 의존성
이란 코드를 컴파일하는 시점
에 결정되는 의존성이며, 클래스 사이의 의존성에 해당한다. 일반적으로 추상화된 클래스나 인터페이스가 아닌 구체 클래스에 의존하면 컴파일타임 의존성을 갖게된다. 우리가 처음 코드처럼 상위객체 안에 하위객체를 선언하게되면 구체적인 클래스를 의존하게된다(강하게 의존) 그러면 컴파일타임의존성을 가지게된다는 의미이다.
런타임 의존성
이란 코드(애플리케이션)를 실행하는 시점
에 결정되는 의존성이며, 객체 사이의 의존성에 해당한다. 일반적으로 추상화된 클래스나 인터페이스에 의존할 때 런타임 의존성을 갖게 된다. 즉 지금처럼 protocol에 의존할때 런타임 의존성을 가진다.
이게 정확하게 무슨의미인지를 설명해보면 ViewController
는 컴파일시점에 Base
라는 인터페이스(protocol)
에 의존한다. 근데 protocol은 구체적인 함수를 알려주는것이 아닌 구현부가 없는 추상적인 함수만을알려준다. 어떤 task를 실행할지(즉, 함수의 구현부)는 애플리케이션이 실행되는 런타임에 되서야 알수있게된다는것이다. 이러한 이유로 런타임 의존성은 결합도가 낮으며
다른 객체들과 협력할 가능성
을 열어두므로 변경에 유연한 설계
를 갖는다라는 의미이다