Swift Language
의존성 주입
수색…
보기 컨트롤러를 사용한 종속성 삽입
Dependenct 주입 소개
응용 프로그램은 서로 공동 작업하는 많은 개체로 구성됩니다. 일반적으로 객체는 다른 객체에 의존하여 작업을 수행합니다. 객체가 자신의 의존성을 참조 할 책임이있는 경우, 객체가 결합되어 테스트하기 어렵고 변경하기 어려운 코드로 연결됩니다.
종속성 주입은 종속성을 해결하기 위해 제어 반전을 구현하는 소프트웨어 설계 패턴입니다. 인젝션 (injection)은 종속 객체를 사용하는 종속 객체로 의존성을 전달하는 것입니다. 이를 통해 클라이언트의 동작과 클라이언트의 종속성을 분리 할 수 있으므로 응용 프로그램을 느슨하게 결합 할 수 있습니다.
위의 정의와 혼동해서는 안됩니다. 의존성 삽입은 객체에 인스턴스 변수를주는 것을 의미합니다.
간단하지만 많은 이점을 제공합니다.
- 코드 테스트가 더 쉽습니다 (단위 테스트 및 UI 테스트와 같은 자동화 된 테스트 사용).
- 프로토콜 지향 프로그래밍과 함께 사용하면 특정 클래스의 구현을 쉽게 변경할 수 있습니다. 리팩터링하기 쉽습니다.
- 코드를 모듈화하고 재사용 가능하게 만듭니다.
응용 프로그램에서 의존성 주입 (DI)을 구현하는 데 가장 일반적으로 사용되는 세 가지 방법이 있습니다.
- 이니셜 라이저 주입
- 특성 주입
- 타사 DI 프레임 워크 (예 : Swinject, Cleanse, Dip 또는 Typhoon) 사용
Dependency Injection에 대한 기사에 대한 링크 가있는 흥미로운 기사 가 있으므로 DI 및 Inversion of Control 원칙을 더 깊이 파고 싶다면 체크해보십시오.
평균적인 iOS 개발자를위한 매일의 작업 인 View Controller에서 DI를 사용하는 방법을 보여 드리겠습니다.
DI가없는 예
LoginViewController 와 TimelineViewController 라는 두 개의 View Controller가 있습니다. LoginViewController는 로그인에 사용되며 성공한 후에는 TimelineViewController로 전환됩니다. 두 뷰 컨트롤러 모두 FirebaseNetworkService에 종속됩니다.
LoginViewController
class LoginViewController: UIViewController {
var networkService = FirebaseNetworkService()
override func viewDidLoad() {
super.viewDidLoad()
}
}
TimelineViewController
class TimelineViewController: UIViewController {
var networkService = FirebaseNetworkService()
override func viewDidLoad() {
super.viewDidLoad()
}
@IBAction func logoutButtonPressed(_ sender: UIButton) {
networkService.logutCurrentUser()
}
}
FirebaseNetworkService
class FirebaseNetworkService {
func loginUser(username: String, passwordHash: String) {
// Implementation not important for this example
}
func logutCurrentUser() {
// Implementation not important for this example
}
}
이 예제는 매우 간단하지만, 10 개 또는 15 개의 다른 뷰 컨트롤러가 있고 그 중 일부는 FirebaseNetworkService에 종속되어 있다고 가정 해 봅시다. 어느 순간에 회사의 사내 백엔드 서비스를 통해 Firebase를 백엔드 서비스로 변경하려고합니다. 이를 수행하려면 모든 View Controller를 거쳐 CompanyNetworkService로 FirebaseNetworkService를 변경해야합니다. CompanyNetworkService의 일부 메소드가 변경된 경우 많은 작업이 필요합니다.
유닛과 UI 테스팅은이 예제의 범위는 아니지만 밀접하게 결합 된 종속성을 가진 단위 테스트 뷰 컨트롤러를 원한다면 그렇게하는 것이 정말 힘들 것이다.
이 예제를 다시 작성하고 View Controller에 Network Service를 주입 해 봅시다.
의존성 주입을 사용한 예
Dependency Injection을 최대한 활용하려면 프로토콜에서 네트워크 서비스의 기능을 정의하십시오. 이런 방식으로, 네트워크 서비스에 의존하는 View Controller는 실제 네트워크 서비스 구현에 대해 알 필요조차 없습니다.
protocol NetworkService {
func loginUser(username: String, passwordHash: String)
func logutCurrentUser()
}
NetworkService 프로토콜의 구현을 추가합니다.
class FirebaseNetworkServiceImpl: NetworkService {
func loginUser(username: String, passwordHash: String) {
// Firebase implementation
}
func logutCurrentUser() {
// Firebase implementation
}
}
LoginViewController와 TimelineViewController를 FirebaseNetworkService 대신 새로운 NetworkService 프로토콜을 사용하도록 변경합시다.
LoginViewController
class LoginViewController: UIViewController {
// No need to initialize it here since an implementation
// of the NetworkService protocol will be injected
var networkService: NetworkService?
override func viewDidLoad() {
super.viewDidLoad()
}
}
TimelineViewController
class TimelineViewController: UIViewController {
var networkService: NetworkService?
override func viewDidLoad() {
super.viewDidLoad()
}
@IBAction func logoutButtonPressed(_ sender: UIButton) {
networkService?.logutCurrentUser()
}
}
이제, 문제는 : LoginViewController와 TimelineViewController에 올바른 NetworkService 구현을 삽입하려면 어떻게해야할까요?
LoginViewController는 시작 뷰 컨트롤러이므로 응용 프로그램이 시작될 때마다 표시되므로 AppDelegate에 모든 종속성을 주입 할 수 있습니다.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// This logic will be different based on your project's structure or whether
// you have a navigation controller or tab bar controller for your starting view controller
if let loginVC = window?.rootViewController as? LoginViewController {
loginVC.networkService = FirebaseNetworkServiceImpl()
}
return true
}
AppDelegate에서 첫 번째보기 컨트롤러 (LoginViewController)에 대한 참조를 가져 와서 속성 주입 메소드를 사용하여 NetworkService 구현을 주입합니다.
이제, 다음 작업은 NetworkService 구현을 TimelineViewController에 삽입하는 것입니다. 가장 쉬운 방법은 LoginViewController가 TimlineViewController로 전환 할 때 그렇게하는 것입니다.
LoginViewController의 prepareForSegue 메소드에 주입 코드를 추가합니다 (다른 접근 방법을 사용하여보기 컨트롤러를 탐색하고 거기에 주입 코드를 배치하는 경우).
우리의 LoginViewController 클래스는 다음과 같이 보입니다 :
class LoginViewController: UIViewController {
// No need to initialize it here since an implementation
// of the NetworkService protocol will be injected
var networkService: NetworkService?
override func viewDidLoad() {
super.viewDidLoad()
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "TimelineViewController" {
if let timelineVC = segue.destination as? TimelineViewController {
// Injecting the NetworkService implementation
timelineVC.networkService = networkService
}
}
}
}
우리는 끝났으며 그렇게 쉽습니다.
이제 FireService의 NetworkService 구현을 맞춤형 회사의 백엔드 구현으로 전환하려고합니다. 우리가해야 할 일은 다음과 같습니다.
새 NetworkService 구현 클래스 추가 :
class CompanyNetworkServiceImpl: NetworkService {
func loginUser(username: String, passwordHash: String) {
// Company API implementation
}
func logutCurrentUser() {
// Company API implementation
}
}
AppDelegate에서 새로운 구현으로 FirebaseNetworkServiceImpl을 전환하십시오 :
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// This logic will be different based on your project's structure or whether
// you have a navigation controller or tab bar controller for your starting view controller
if let loginVC = window?.rootViewController as? LoginViewController {
loginVC.networkService = CompanyNetworkServiceImpl()
}
return true
}
즉, 우리는 LoginViewController 또는 TimelineViewController를 건드리지 않고도 NetworkService 프로토콜의 전체적인 구현을 전환했습니다.
이것은 간단한 예이므로 현재 모든 이점을 볼 수는 없지만 프로젝트에서 DI를 사용하려고하면 이점을 볼 수 있으며 항상 종속성 삽입을 사용합니다.
의존성 주입 유형
이 예제는 Swift에서 DI (Dependency Injection) 디자인 패턴을 사용하여 다음과 같은 방법을 사용하는 방법을 보여줍니다.
- Initializer Injection (적절한 용어는 Constructor Injection이지만, Swift는 Initializer를 가지고 있기 때문에 initializer injection이라고 부름)
- 속성 주입
- 방법 주입
DI가없는 예제 설정
protocol Engine {
func startEngine()
func stopEngine()
}
class TrainEngine: Engine {
func startEngine() {
print("Engine started")
}
func stopEngine() {
print("Engine stopped")
}
}
protocol TrainCar {
var numberOfSeats: Int { get }
func attachCar(attach: Bool)
}
class RestaurantCar: TrainCar {
var numberOfSeats: Int {
get {
return 30
}
}
func attachCar(attach: Bool) {
print("Attach car")
}
}
class PassengerCar: TrainCar {
var numberOfSeats: Int {
get {
return 50
}
}
func attachCar(attach: Bool) {
print("Attach car")
}
}
class Train {
let engine: Engine?
var mainCar: TrainCar?
}
Initializer Dependency Injection
이름에서 알 수 있듯이 모든 종속성은 클래스 이니셜 라이저를 통해 주입됩니다. 이니셜 라이저를 통해 종속성을 주입하려면 이니셜 라이저를 Train
클래스에 추가합니다.
Train 클래스는 이제 다음과 같이 보입니다.
class Train {
let engine: Engine?
var mainCar: TrainCar?
init(engine: Engine) {
self.engine = engine
}
}
Train 클래스의 인스턴스를 만들려면 이니셜 라이저를 사용하여 특정 엔진 구현을 주입합니다.
let train = Train(engine: TrainEngine())
참고 : 이니셜 라이저 주입과 속성 주입의 주된 이점은 변수를 개인 변수로 설정하거나 let
키워드로 상수로 만들 수 있다는 것입니다 (예에서와 같이). 이렇게하면 아무도 액세스하거나 변경할 수 없습니다.
속성 의존성 삽입
속성을 사용하는 DI는 이니셜 라이저를 사용하는 것보다 훨씬 간단합니다. 프로퍼티 DI를 사용하여 이미 생성 한 train 객체에 PassengerCar 종속성을 주입합시다 :
train.mainCar = PassengerCar()
그게 전부 야. 우리 기차의 mainCar
는 이제 PassengerCar
인스턴스입니다.
방법 의존성 주입
이 유형의 종속성 삽입은 이전 객체와 약간 다르지만 객체 전체에 영향을 미치지는 않지만 하나의 특정 메소드의 범위에서만 사용되는 종속성을 주입합니다. 종속성이 단일 메소드에서만 사용되는 경우 전체 객체를 종속 메소드에 종속시키는 것이 좋지 않습니다. Train 클래스에 새 메서드를 추가해 보겠습니다.
func reparkCar(trainCar: TrainCar) {
trainCar.attachCar(attach: true)
engine?.startEngine()
engine?.stopEngine()
trainCar.attachCar(attach: false)
}
이제 새로운 Train 클래스 메서드를 호출하면 메서드 종속성 삽입을 사용하여 TrainCar
를 주입합니다.
train.reparkCar(trainCar: RestaurantCar())