DI (Dependency Injection) 의존성 주입
클래스 내부에서 필요한 객체의 인스턴스를 클래스 내부에서 생성하는 것이 아니라 외부에서 생성한 후 이니셜라이저 또는 setter를 통해 내부로 주입받는 것이다. 이때 이니셜 라이저의 타입은 프로토콜을 활용하여 내부에서는 프로토콜 메서드를 사용한다.
DI는 의존성을 클래스에 주입 시키는 것이고, 의존성 분리의 조건을 만족해야 한다.
의존성(Dependency) 클래스 A, 클래스 B가 있고, 클래스 B의 값이 바뀔때 클래스 A의 값도 함께 바뀌게 된다면 클래스 A는 B에게 의존성을 갖는다고 말한다.
// AP 챔피언 클래스
class ApChamp {
let name: String
init(name: String) {
self.name = name
}
}
// AD 챔피언 클래스
class AdChamp {
let name: String // 여기랑 Int로 바꾸게된다면
init(name: String) { // 여기를 Int로 바꾸게된다면
self.name = name
}
}
// 플레이어 클래스
class Player {
let apMost: ApChamp
let adMost: AdChamp
// 클래스 내부에서 ApChamp, AdChamp 인스턴스를 생성하고 있다.
init() {
self.apMost = ApChamp(name: "아칼리")
self.adMost = AdChamp(name: "요네") // AdChamp 클래스의 name 타입을 이렇게 Int로 변경한다면, ksw의 init()함수 내부 여기서 타입에러가 난다.
}
}
let ksw = Player()
print(ksw.apMost.name) // 아칼리
print(ksw.adMost.name) // 요네
주입 (Injection) 위의 Player 코드를 다음과 같이 수정할 수 있다.
class Player {
let apMost: ApChamp
let adMost: AdChamp
// init 함수 수정
init(apMost: ApChamp, adMost: AdChamp) {
self.apMost = apMost
self.adMost = adMost
}
}
// 외부에서 클래스 내부에 값을 "주입"
let apChamp = ApChamp(name: "아칼리")
let adChamp = AdChamp(name: "요네")
let ksw = Player(apMost: apChamp, adMost: adChamp)
print(ksw.apMost.name) // 아칼리
print(ksw.adMost.name) // 요네
객체의 인스턴스를 외부에서 생성한뒤 (let apChamp = ApChamp(name: "아칼리") 넣어주는것을 주입이라고 한다.
현재 코드에선 AdChamp의 name이 Int 형으로 변경된다고 해도, Player 클래스의 init()에는 문제가 생기지 않게 된다. 그래도 AdChamp의 name value 가 바뀐다면, Player의 adMost 의 name value 도 바뀌므로 의존성은 남아있다. 쉽게 말해서, 클래스 A 내부에서 다른 클래스 B를 사용하고 있다면, A가 B를 의존한다고 생각할 수 있다.
여튼 이렇게 해서 "의존성"을 "주입"했다.
의존성 분리 위 내용처럼 의존성을 주입시킨 것 만으로 DI라고 부르지 않는다. 의존성 분리의 조건을 만족시켜야 DI 라고 한다. 그리고 의존성의 분리는 의존 역전의 원칙을 기반으로 분리를 실행한다.
SOLID 5원칙 중 Dependency Inversion Principle을 의미한다. DIP원칙이란?
의존 관계를 맺을땐, 변화하기 쉬운것보다 변화하기 어려운 것에 의존해야한다는 원칙
변화하기 어려운 것이란 추상 클래스나 인터페이스를 말하고 변화하기 쉬운 것은 구체화된 클래스를 의미
따라서 DIP를 만족한다는 것은 구체적인 클래스가 아닌 인터페이스 또는 추상 클래스와 관계를 맺는다는 것을 의미
// ApChamp 와 AdChamp 이 공통으로 준수할 프로토콜.
// 프로토콜을 사용함으로써 의존 역전이 되었다.
protocol Champ: AnyObject {
var name: String { get }
}
// Champ 프로토콜 채택
class ApChamp: Champ {
let name: String
init(name: String) {
self.name = name
}
}
// Champ 프로토콜 채택
class AdChamp: Champ {
let name: String
init(name: String) {
self.name = name
}
}
class Player {
let apMost: Champ
let adMost: Champ
// 프로토콜에 대고 주입받는다.
init(apMost: Champ, adMost: Champ) {
self.apMost = apMost
self.adMost = adMost
}
}
// 외부에서 클래스 내부에 값을 "주입"
let apChamp = ApChamp(name: "아칼리")
let adChamp = AdChamp(name: "요네")
let ksw = Player(apMost: apChamp, adMost: adChamp)
print(ksw.apMost.name) // 아칼리
print(ksw.adMost.name) // 요네
이렇게 프로토콜을 활용했을때 장점은 프로토콜에서 champion 의 타입을 String 에서 Int 형으로 변경한다면 ? 모든 클래스들이 함께 에러를 갖게 된다. 프로토콜 하나만 잘 파악해 둔다면, 그 프로토콜을 준수하는 모든 클래스를 제어하고 분석하기 쉬워지게 된다.
이런 과정이 잘 일어났을 때 클래스에서 프로토콜에게 의존의 방향이 역전 되었다고한다.(=IOC Inversion Of Control), 제어 반전이라고도 한다.
그리고 만약 apMost 자리에 Support를 넣어달라고하거나 추후 수정이 될가능성이있다면..?
// Support 챔피언 클래스
class SupportChamp: Champ {
let name: String
init(name: String) {
self.name = name
}
}
// MageChamp 챔피언 클래스
class MageChamp: Champ {
let name: String
init(name: String) {
self.name = name
}
}
class Player {
let apMost: Champ
let adMost: Champ
init(apMost: Champ, adMost: Champ) {
self.apMost = apMost
self.adMost = adMost
}
}
let apChamp = ApChamp(name: "아칼리")
let adChamp = AdChamp(name: "요네")
let supportChamp = SupportChamp(name: "카르마")
let mageChamp = MageChamp(name: "조이")
// * 여기만 바꿔주면 모든 수정 사항이 완료된다.
let ksw = Player(apMost: supportChamp, adMost: adChamp)
// let ksw = Player(apMost: mageChamp, adMost: adChamp)
print(ksw.apMost.name) // 카르마
print(ksw.adMost.name) // 요네
똑같이 Champ프로토콜을 준수하는 Support, Mage 클래스를 외부에서 생성하고, 주입시키는 부분만 변경하면 모든 수정사항이 완료된다. 프로토콜을 준수하는 클래스로 교체만 해주면 된다.
Player 클래스 내부에서 프로토콜의 프로퍼티, 프로토콜의 메서드를 사용한다면 외부 객체 인스턴스를 교체해도 이상없이 동작된다.
이런 경우를 의존성이 낮아졌다. 결합도가 낮아졌다라고 표현한다.
IOC Container
여태 설명한 IOC, 의존 역전을 구현하는 프레임워크를 IOC Container 라고 한다.
제어권을 프레임워크가 가져가게 되는 것.
이를 위한 프레임워크로 Swinject 가 있다. → 추후 적용해볼것!
참고
https://ios-daniel-yang.tistory.com/71#article-2--의존성(dependency)이란? https://80000coding.oopy.io/68ee8d89-5d05-449d-87e2-5fba84d604ca