
이번 포스팅에선 의존성 주입(Dependency Injection)의 개념에 대해서 알아보도록 하겠습니다.
의존성 주입이란?
→ 어떤 클래스(객체)에서 자신이 필요한 클래스(객체)를 직접 선언하는 것이 아닌 외부에서 선언해서 넣어주는(주입) 개념입니다.
즉, 필요한 클래스(객체)가 있다는 것은 내가 의존하는 클래스(객체)가 있다는 것이고 이걸 '의존성' 이라고 칭한다면 "의존성 주입"
이라는 말의 뜻이 너무 어렵지 않게 다가올겁니다. ( 그러길 바랍니다ㅎㅎ😄 )
예제코드를 통해 살펴보면
final class BankingViewController: UIViewController {
var bank = AppleBank()
// bank AppleBank 이외에도 BananaBank ~ CarrotBank 까지 존재한다고 가정하자
}
어떤 BankingViewController에 사용할 bank가 있다고 가정했을때 위와 같이 BankingViewController가 자신이 사용할 bank를 직접 선언하게 되면 자신이 사용할 bank는 AppleBank로 고정이 되어버립니다.
즉, 다른 bank(BananaBank ~ CarrotBank)를 사용하고 싶으면 다른 BankingViewController를 또 선언해야 하겠죠??
이런 상황을 좀 유식하게 개발자스럽게 얘기하자면 객체 간의 '결합도(Coupling)가 높다'고 표현합니다.
예제 코드를 통해 "내가 의존하는 클래스(객체)를 직접 선언하면 왜 안좋은데??" 에 대한 답을 알려드렸습니다.
정답: 객체 간의 결합도(Coupling)가 높아져 유지보수의 측면에서 바람직하지 않음.
그렇다면 이제 의존성 주입을 통해 결합도를 낮추는 예제코드에 대해서 알아보도록 하겠습니다.
3가지의 방식이 있습니다.
- Initializer Injection
final class BankingViewController: UIViewController {
var bank: AppleBank
init(bank: AppleBank) {
self.bank = bank
}
}
let appleBank = AppleBank()
let vc = BankingViewController(bank: appleBank)
- Property Injection
final class BankingViewController: UIViewController {
var bank: AppleBank?
}
let appleBank = AppleBank()
let vc = BankingViewController()
vc.bank = appleBank
- Method Injection
final class BankingViewController: UIViewController {
var bank: AppleBank?
func setBank(_ bank: AppleBank) {
self.bank = bank
}
}
let appleBank = SampleSubViewA()
let vc = BankingViewController()
vc.setBank(appleBank)
3가지 방식 모두 외부에서 bank를 선언해서 주입시켜주는 모습이 보이시나요?
이렇게 되면 BankingViewController는 결합도(Coupling)가 낮아져서 좀 더 유연한 모습을 가질 수 있는거 보이시면 의존성 주입을 왜 하는지 이해가 되신겁니다.
"의존성 주입" 이라는 단어가 가진 뭔가 전문적인 개념일것 같은 포스가 있어서 그렇지 ㅎㅎ 어려운 개념이 아니에요~👍
하지만 "의존성 주입" 만으로는 아직 해결되지 않은 문제점이 하나 있습니다. 바로 의존성이 분리가 되지 않았다는 점인데요.
다음의 Initializer Injection 예제를 다시 살펴보도록 하겠습니다.
final class BakingViewController: UIViewController {
var bank: AppleBank?
init(bank: AppleBank) {
self.bank = bank
}
}
let appleBank = AppleBank()
let bankingVC = BankingViewController(bank: appleBank)
각종 은행서비스를 처리할 수 있는 BankingViewController 에 사과은행(AppleBank)을 init constructor를 통해 주입하였습니다.
하지만, 은행은 BananaBank와 CarrotBank도 있는데 현재 구조로는 AppleBank의 서비스만 이용이 가능한 모습입니다.
다른 bank(BananaBank ~ CarrotBank)를 사용하고 싶으면 다른 BankingViewController를 또 선언해야 하는 슬픈 현실에는 변함이 없는 것입니다...
즉, Dependency Inversion Principle (의존 역전 원칙)에 위배 된다고 볼 수 있습니다.
의존 역전 원칙은 의존 관계가 생길때 변하기 쉬운거(bank) 보다 변하기 어려운 것(bankService)에 의존해야 한다는 원칙입니다. 즉 DIP를 위배하지 않으려면 의존 시 클래스(AppleBank, BananaBank, CarrotBank)보다는 인터페이스 혹은 추상 클래스(BankService)와 관계를 맺는게 맞고, Swift에서는 protocol을 통하여 해결합니다.
protocol BankService {
var name: String { get set }
func transfer()
func borrowMoney()
}
class Bank: BankService {
var name: String
init(name: String) {
self.name = name
}
func tranfer() {
print("이체")
}
func borrowMoney() {
print("대출")
}
}
BankService 프로토콜을 통하면 BankginViewController는 내가 어떤 은행을 사용하는지에는 관심이 없게지게 되고, BankingViewController의 재사용성이 증가하게 됩니다.
final class BakingViewController: UIViewController {
var bank: BankService?
init(bank: BankService) {
self.bank = bank
}
}
let appleBank = Bank(name: Apple)
let bankingVC = BankingViewController(bank: appleBank)
// let bananaBank = Bank(name: Banana)
// let bankingVC = BankingViewController(bank: bananaBank)
// let carrotBank = Bank(name: carrot)
// let bankingVC = BankingViewController(bank: carrotBank)
자, 그럼 의존성(Dependency)과 관련된 2가지 개념을 정리해보았습니다. (의존성 주입, 의존성 분리)
우리가 일반적으로 말하는 Dependency Injection(DI)는 의존성 주입 + 의존성 분리 를 합쳐 부르는 개념이라고 생각하시면 됩니다.
이제 여태까지 설명한 DI(Dependency Injection)을 Swift에서 쉽게 사용할 수 있도록 도와주는 Swinject 오픈소스 라이브러리에 대한 예제 코드를 설명하고자 합니다.
Swinject에는 컨테이너(container)라는 개념이 등장하는데요. DI컨테이너라고 부르고, 특정 클래스(객체)에 주입할 의존성(Dependency)이 필요할 때 컨테이너에서 꺼내와서 주입시키는 개념입니다.
import Swinject
import SwinjectAutoregistration
/*
swinject DI 등록
*/
let container = Container {
$0.register(Networking.self, factory: { _ in
NetworkingImpl()
}).inObjectScope(.container)
// Networking 프로토콜을 준수하는 NetworkingImpl 클래스(객체)를 contatiner에 등록
$0.register(ApiServiceType.self, factory: { _ in
ApiService()
}).initCompleted { r, apiService in
apiService.networking = r.resolve(Networking.self)
}.inObjectScope(.container)
/* ApiServiceType 프로토콜을 준수하는 ApiService 클래스(객체)를 container에 등록하되,
ApiService에서 사용할 networking 은 상단에서 컨테이너에 등록한 Networking 프로토콜을
준수하는 NetworkingImpl()을 사용하겠다고 주입
즉, 컨테이너에 등록함과 동시에 Dependency를 주입하는것도 가능
*/
// 상단의 예제코드 관련
// BankService 프로토콜을 준수하는 Bank 클래스(객체)를 container에 등록하는 방법 #1
$0.register(BankService.self, factory: { _ in
Bank(name: "Apple")
}).inObjectScope(.container)
// BankService 프로토콜을 준수하는 Bank 클래스(객체)를 container에 등록하는 방법 #2
$0.autoregister(BankService.self, initializer: Bank(name: "Apple"))
.inObjectScope(.container)
// 방법 #1, 방법 #2는 동일한 과정이다.
// reactor 등록 예제
$0.autoregister(SampleViewReactor.self, initializer: SampleViewReactor.init)
$0.autoregister(SampleViewReactor.self, argument: Sample.self, initializer: SampleViewReactor.init)
}
이제 상단의 예제코드에서 BankingViewController에 Swinject를 사용하여 의존성을 주입하여 사용하는 예제코드를 통해 이번 포스팅을 마무리하고자 합니다.
import SwinjectAutoRegistration
let bank = container ~> BankService.self
let bankingVC = BankingViewController(bank: bank)'iOS_기타' 카테고리의 다른 글
| iOS 면접준비 #3 (0) | 2022.02.16 |
|---|---|
| 면접 실제 받았던 질문 정리 + 공부 (0) | 2022.02.15 |
| iOS 준비#2 (~22.01) (0) | 2022.02.03 |
| iOS 준비#1 (~22.01) (0) | 2022.02.01 |