DI는 의존성을 클래스에 주입 시키는 것이고, 의존성 분리의 조건을 만족해야한다.
class Zoo {
let animal: Bear
init(){
self.bear = Bear()
}
}
class Bear {
func startCliming(){
print("bear :: Climing!!!")
}
}
객체 지향 프로그래밍에서 Dependency, 의존성은 서로 다른 객체 사이에 의존 관계가 있다는 것을 말한다. 즉, 의존하는 객체가 수정되면, 다른 객체도 영향을 받는다는 것이다.
Zoo 클래스는 animal 변수가 필요하다. 이때 animal 타입은 Bear이다. Zoo의 생성자는 Zoo의 인스턴스가 생성될때 Bear 인스턴스를 생성한다. Zoo 객체를 생성하고 사용하기 위해서는 animal 객체가 필요하다.
이때 Zoo 는 Bear에 의존한다고 한다. Bear의 변경 사항이 Zoo에도 영향이 가기 때문이다.
Injection, 주입은 외부에서 객체를 생성해서 넣는 것을 의미한다.
의존성을 외부로부터 전달받아 생성자에서 할당해준다. 의존성 주입에서 초점은 외부로부터 객체를 전달받아 필요한 객체를 참조하는 방식이다.
class Zoo {
let animal: Bear
init(bear: Bear) {
self.bear = bear
}
}
class Zoo {
let animal: Bear
init(animal: Bear) {
self.animal = animal
}
func openZoo() {
animal.startCliming()
}
}
class Bear {
func startCliming() { }
}
class Duck {
func startSwim() { }
}
여기서 Zoo의 animal 타입이 Duck으로 바뀌면 의존성으로 인해 코드를 바꾸어야 한다.
일반적으로 의존성을 주입시킨 것 만으로 DI 라고 부르지는 않는다. 의존성 분리의 조건을 만족시켜야 DI 라고 한다.그리고 의존성 분리는 "의존 역전의 원칙"을 기반으로 분리를 실행한다.
SOLID 5원칙 중 Dependency Inversion Principle 을 의미한다.
객체 지향 프로그래밍은 프로그래밍 시 필요한 데이터들을 추상화 시킨다. 추상화 된 객체는 상태(property)와 행위 (method)를 가지게 된다. 이때 이 객체들은 서로 유기적인 상호작용을 하며 로직을 형성하게 된다.
protocol Animal {
func startAct()
}
class Zoo {
let animal: Animal
init(animal: Animal){
self.animal = animal
}
func openZoo(){
animal.startAct()
}
}
class Bear: Animal {
func startAct() {
}
}
class Duck: Animal {
func startAct() {
}
}
의존성 주입!
의존성 주입을 하는 이유는 무엇일까?
그렇다면 내부에서 만든 객체를 외부에서 넣어서 의존성을 주입해보자.
하지만 전에 의존 관계 역전 법칙을 알아야 한다.
DIP, 의존 관계 역전 법칙은 객체 지향 프로그래밍 설계의 다섯가지 기본 원칙(SOLID) 중 하나이다.
추상화 된 것은 구체적인 것에 의존하면 안되고 구체적인 것이 추상화된 것에 의존 해야한다.
즉, 구체적인 객체는 추상화된 객체에 의존 해야 한다는 것이 핵심이다.
Swift에서 추상화된 객체는 Protocol이 있다.
우리는 이 Protocol을 활용해서 의존성 주입을 구현하려고 한다.
// sayHi를 강제하는 토크 프로토콜
protocol Talk {
func sayHi()
}
//베프, 올드프렌드 토크 클래스들은 토크 프로토콜을 임플리먼트 하기 때문에 sayHi 메소드 정의가 강제됨
class BestFriendTalk: Talk {
func sayHi() {
print("오호! 오늘도 빡코딩 하는중?!")
}
}
class OldFriendTalk : Talk {
func sayHi(){
print("어이! 올만이여~")
}
}
//친구 클래스
class Friend {
// 멤버 변수로써 토크를 가진다
// 외부에서도 주입이 가능하도록 프라이빗으로 안함
var talk: Talk?
init(){}
//생성자 메서드를 통해 토크 주입가능
init(talk: Talk){
self.talk = talk
}
//의존성 주입이 완료된 토크로 말한다
func sayHello(){
talk?.sayHi()
}
//의존성 주입 메소드
func setTalk(talk: Talk){
self.talk = talk
}
}
//친구 인스턴스 생성시에 토크를 주입시킴
let bfTalk = BestFriendTalk()
let myBestFriend = Friend(talk: bfTalk)
myBestFriend.sayHello()
//친구 인스턴스를 만들고 변수에 주입함
let myOldFriendTalk = OldFriendTalk()
let myOldFriend = Friend()
myOldFriend.talk = myOldFriendTalk
myOldFriend.sayHello()
//의존성 주입은 크게 3가지 방법
// 1. 생성자 메서드 주입
// 2. 멤버 변수 주입
// 3. 주입 메소드를 따로 만들어서 주입
클래스의 제어 주체가 모두 프로토콜에게 있게 된다. 프로토콜에게 의존의 방향이 역전 되었다고 한다.
“IOC(Inversion of Control)”
struct AViewModel { }
struct BViewModel { }
class BViewController: UIViewController {
let viewModel: BViewModel
init(viewModel: BViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
init(viewModel: BViewModel, content: String?) {
self.viewModel = viewModel
self.content = content ?? ""
super.init(nibName: nil, bundle: nil)
}
class A {
let b: B
init(b: B) {
self.b = b
}
}
class B {
let a: A
init(a: A) {
self.a = a
}
}
init이 아닌 property를 통해서 의존성을 주입받는다. 왜 그럴까?
객체와 의존성 생성 시점을 생각해보자. 객체를 먼저 생성하고 나중에 의존성이 생성되는 상황에서는 생성자 주입을 이용하게 되면 객체를 먼저 생성할 수 없다. 의존성까지 함께 넘겨주어야 하니 의존성 객체가 생성되기 전까지 객체를 생성할 수 없다.
struct Lecture { }
class TimeTableViewController: UIViewController {
var lecture: Lecture?
override func viewDidLoad() {
super.viewDidLoad()
}
}
class MainViewController: UIViewController {
func moveToTiemTable() {
let timeTableViewController = TimeTableViewController()
timeTableViewController.lecture = Lecture()
// 생략
}
}
또한 선택사항의 의존성인 경우 init에서 생성하는 것보다 외부에서 프로퍼티를 통해 받는 방법을 사용할 수 있다.
@objc func fetchLecture() {
tiemTableViewController.lecture = Lecture()
}
하지만, 캡슐화가 깨지는 단점이 존재한다. public으로 공유되는 property가 있기 때문이다.
외부에서 얼마든지 그 값을 바꿀 수 있고 원하지 않게 참조하여 값을 수정해버리면 꼬인다.
UITableViewCell의 속성들을 넘겨서 한번에 UI를 업데이트 할 때 많이 쓰는 방법이다. public method 즉, 외부에서 접근할 수 있는 메소드를 가지고 dependency 를 주입하는 방법입니다.
class TableViewCell: UITableViewCell {
@IBOutlet weak var titleLabel: UILabel!
// MARK: -
override func awakeFromNib() {
super.awakeFromNib()
selectionStyle = .none
}
func configuration(title: String) {
self.titleLabel.text = title
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as? TableViewCell else {
return UITableViewCell()
}
cell.configuration(title: "\(Cell(rawValue: indexPath.row) ?? .total)")
return cell
}
cell을 dequeue할때 configuration 메소드를 호출하여 의존성을 주입해준다. 이러한 방법은 위에서 살펴 봤던 property injection과 동일한 단점을 가지고 있다. 외부에서 얼마든지 접근하여 변경이 가능하다.
추후 포스팅에서 소개해 드릴 프레임워크와 앱 아키텍처에서도 사용되는 방법이다.
객체지향 프로그래밍에서 추상화라는 개념이 있습니다. Swift 에서는 클래스를 정의하는 추상화 방법에 Protocol이 있다. Interface를 사용하게 되면 긴밀하게 연결되어 있는 의존성이 약해져 모듈과 모듈 사이의 결합이 줄어 든다.
protocol Networking {
func fetchData()
}
class UserNetworkManager: Networking {
func fetchData() {
print("user data fetched")
}
}
class PostNetworkManager: Networking {
func fetchData() {
print("post data fetched")
}
}
1)
class MainViewController {
let networkManager: Networking
init(networkManaer: Networking) {
self.networkManager = networkManaer
}
}
2)
protocol Service {
func setNetworkManager(networkManager: Networking)
}
class MainViewController: Service {
var networkManager: Networking?
func setNetworkManager(networkManager: Networking) {
self.networkManager = networkManager
}
}
Service 프로토콜을 따르는 클래스는 네트워킹을 필요로하는 클래스이고 Service에서 정의한 함수인 setNetworkManager를 구현하게 된다.
외부에서 Service를 따르는 MainViewController를 구현하고 이 객체의 networkManager를 설정해 주기 위해 인터페이스에서 정의된 함수를 사용하여 의존성을 설정해준다.
이때 networkManager 또한 프로토콜인 Networking으로 설정하여 필요한 기능과 데이터를 명시하고 외부로 부터 인터페이스를 사용하여 객체를 주입 받게 된다.
인터페이스에 의해 각 의존관계를 유연하게 만들 수 있고 결합도 줄일 수 있으며 재사용과 테스트 측면에도 용이하다.
각각의 dependency 마다 함수를 직접 작성해주어야 했다. 이전 포스팅 예제는 아주 간단하고 규모가 작았기 때문에 큰 문제는 없었지만, 앱이 점점더 성장하면 성장하게 될 수록, 코드는 금방 복잡해지게 될 것이다. 나중에는 수백의 dependencies를 갖게될지도 모르고, 그 dependency를 생성하고 주입하는 기능이 필요하게 될 것이다.
https://github.com/Swinject/Swinject
https://doodledevnote.tistory.com/30
DI 패턴에서 Swinject는 앱이 느슨하게 결합된 구성 요소로 분할될 수 있도록 도와주며, 이는 보다 쉽게 개발, 테스트 및 유지 관리할 수 있습니다.
//AppDelegate.swift
import UIKit
import CoreData
import Swinject
protocol Animal {
var name: String? {get}
}
class Cat: Animal {
let name: String?
init(name: String?){
self.name = name
}
}
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
let container: Container = {
let container = Container()
// Cat 이라는 클래스에 Animal 프로토콜에게 제어권을 넘기는 동시에,
// 의존성을 주입한다.
container.register(Animal.self) { _ in Cat(name: "Mimi") }
// ViewController 클래스를 등록한다.
container.register(ViewController.self) { resolver in
let controller = ViewController()
controller.animal = resolver.resolve(Animal.self)
return controller
}
return container
}()
...
}
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
let appDelegate = UIApplication.shared.delegate as! AppDelegate
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let scene = (scene as? UIWindowScene) else { return }
self.window = UIWindow(windowScene: scene)
self.window?.rootViewController = appDelegate.container.resolve(ViewController.self)
self.window?.makeKeyAndVisible()
}
import UIKit
class ViewController: UIViewController {
var animal: Animal?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
print("Animal name is \(animal!.name!)")
}
}
import UIKit
import Swinject
import Foundation
protocol Animal {
var name: String? { get }
var cry: String? { get }
}
class Cat: Animal {
let name: String?
let cry: String?
init(name: String?) {
self.name = name
self.cry = "Meow"
}
}
class Dog: Animal {
let name: String?
let cry: String?
init(name: String?) {
self.name = name
self.cry = "Bark"
}
}
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
let container: Container = {
let container = Container()
container.register(Cat.self) { _ in Cat(name: "Mimi")}
container.register(Dog.self) { _ in Dog(name: "Popi")}
container.register(ViewController.self) { resolver in
let controller = ViewController()
controller.cat = resolver.resolve(Cat.self)
controller.dog = resolver.resolve(Dog.self)
return controller
}
return container
}()
...
}
import UIKit
class ViewController: UIViewController {
var cat: Cat?
var dog: Dog?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
print("Cat name is \(cat!.name!)")
print(cat!.cry!)
print("Dog name is \(dog!.name!)")
print(dog!.cry!)
}
}
구글링을 통해 확인한 결론은 외부라이브러리 사용 없이 프로젝트를 생성하고 사용할 경우에는 xcodeproj로 실행해도 되지만, 외부라이브러리를 사용했을 경우에는 xcworkspace를로 프로젝트를 열어야 합니다.
xcodeproj는 프로젝트는 프로젝트 설정파일들이 들어있는 디렉토리이고
xcworkspace는 workspace와 프로젝트들에 대한 설명하는 파일이 들어 있는 디렉토리 입니다.
그래서 cocoapods 등을 사용하여 외부 라이브러리 등을 내 프로젝트에 추가한 경우에 xcworkspace에서 내 프로젝트와 외부 라이브러리를 연결해주는 역할을 하게됩니다. 그래서 xcodeproj로 실행했을 때 AFNetworking/AFNetworking.h file not found가 발생하지만, xcworkspace로 프로젝트를 실행할 경우 정상 실행 되었습니다.
~.xcodeproj 파일 대신에 새로 생성된 ~.xcworkspace 라는 파일로 프로젝트를 열어준다.
참고 링크
https://jeongupark-study-house.tistory.com/160
https://minhyeokism.tistory.com/80
의존성 주입 Dependency Injection
클래스 A가 클래스 B를 사용하기 때문에 A는 B에 의존적
IOC 의존성 역전 중간에 매개체를 두고 사용 매개체에게 제어의 흐름을 줌
IOC Container 제어권을 관리, 인스턴스 생성, 메모리 해제
Dependency Injection
필요한 모듈 등록
사용처에서 직접 생성하는게 아니라 필요할때 IOC가 필요할때 의존성있는 모듈을 주입해주는 방식이다.
의존하는 모듈의 생성, 해제 , 주입등의 일련의 과정을 프레임워크가 하면서 의존성이 줄어들게 된다.
IOC 제어의 역전 /DIP 의존 방향의 역전 /객체, 프레임워크 /인터페이스로 구현 상속받는 모든것이 들어와야함
고수준 , 저수준 모듈이 의존 / 추상화에 의존해야하지 구체화에 의존하면 안된다. /필요한 의존성을 모두 포함하는 생성자를 만들고 얘를 주입하는 방법 / Interface 주입 /의존성 분리
IOC는 개념 DI 어떻게 구현할지 - 구체적인 행위
필드 주입 / setter - final 필드를 만들 수 없다. / 생성자주입 객체의 최초 생성시점에 주입해준다.
필드 final 의존성 주입 최조 1회 NPE, 불변
순환참조 문제를 해결할 할 수 있다.
IOC 원칙 DI IOC를 지키기 위한 디자인 패턴