Dependency Injection

JIN·2023년 2월 17일
0
post-custom-banner

Dependency Injection

DI는 의존성을 클래스에 주입 시키는 것이고, 의존성 분리의 조건을 만족해야한다.

  1. 객체 생성
  2. 의존성 객체 주입 *스스로가 만드는것이 아니라 제어권을 위임하여 만들어놓은 객체를 주입*
  3. 의존성 객체 메소드 호출

swift- DI(의존성 주입, Dependency Injection)

의존성이란?

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 을 의미한다.

  • DIP 원칙이란
    • 의존 관계를 맺을 땐, 변화하기 쉬운 것보단 변화하기 어려운 것에 의존해야 한다는 원칙.
    • 여기서 변화하기 어려운 것이란 추상 클래스나 인터페이스를 말하고 변화하기 쉬운 것은 구체화된 클래스를 의미.
    • 따라서 DIP를 만족한다는 것은 구체적인 클래스가 아닌 인터페이스 또는 추상 클래스와 관계를 맺는다는 것을 의미.

추상화

객체 지향 프로그래밍은 프로그래밍 시 필요한 데이터들을 추상화 시킨다. 추상화 된 객체는 상태(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() {
    }
}

의존성 주입!

의존성 주입을 하는 이유는 무엇일까?

  • Unit Test가 용이해진다.
  • 코드의 재활용성을 높여준다.
  • 객체 간의 의존성(종속성)을 줄이거나 없엘 수 있다.
  • 객체 간의 결합도이 낮추면서 유연한 코드를 작성할 수 있다.

그렇다면 내부에서 만든 객체를 외부에서 넣어서 의존성을 주입해보자.
하지만 전에 의존 관계 역전 법칙을 알아야 한다.

의존 관계 역전 법칙

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)”

DI (Dependency Injection) 방법

1. Constructor Injection (생성자 주입)

  • 생성자를 이용한 의존성 주입
  • BViewController는 BViewModel을 필요로 한다. 따라서 이 객체를 init에서 주입받아 설정해준다.
  • 생성자를 사용해서 외부로부터 필요한 의존성을 주입받는다.
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()
    }
    
}
  • ios에서는 view Life cycle과 관련된 메서드 이전에 실행되어 뷰를 그리기 이전에 미리 필요한 설정을 해줄수 있으므로 초기에 꼭 필요한 데이터나 객체가 있을때 사용된다.
init(viewModel: BViewModel, content: String?) {
        self.viewModel = viewModel
        self.content = content ?? ""
        super.init(nibName: nil, bundle: nil)
    }
  • 그러나 default가 있는 값이나 nil등의 선택사항의 경우에는 다음의 코드보다는 Property Injection을 사용한다. 생성시 굳이 필요한 데이터가 아니기 때문이다. 또한 순환참조의 경우 Constructor Injection을 사용하기 힘들다.
  • ios는 순환참조가 발생하지 않도록 조심해야 한다.
class A {
    let b: B
    init(b: B) {
        self.b = b
    }
}

class B {
    let a: A
    init(a: A) {
        self.a = a
    }
}

2. Property Injection (프로퍼티 주입)

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가 있기 때문이다.

외부에서 얼마든지 그 값을 바꿀 수 있고 원하지 않게 참조하여 값을 수정해버리면 꼬인다.

3. Method Injection(메서드 주입)

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과 동일한 단점을 가지고 있다. 외부에서 얼마든지 접근하여 변경이 가능하다.

4. Interface 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를 생성하고 주입하는 기능이 필요하게 될 것이다.

IOC 프레임워크 : Swinject

  • Swinject : Swift 에서 DI (의존성 주입)을 위한 프레임워크. 객체 자체가 아니라 프레임워크에 의해 객체의 의존성이 주입되도록 한다.

Swinject 실습

https://github.com/Swinject/Swinject

https://doodledevnote.tistory.com/30

  • In the pattern, Swinject helps your app split into loosely-coupled components, which can be developed, tested and maintained more easily.

    DI 패턴에서 Swinject는 앱이 느슨하게 결합된 구성 요소로 분할될 수 있도록 도와주며, 이는 보다 쉽게 개발, 테스트 및 유지 관리할 수 있습니다.

1. AppDelegate.swift에 Container를 생성한다. 그리고 Container에 프로토콜과 클래스를 등록한다.

  • 의존성 주입은 클래스의 외부에서 한다. 따라서 ViewController 클래스의 외부인 AppDelegate에 Swinject Container를 선언해준다.
    • register: Container에 사용할 프로토콜 등록
    • resolve: 클래스 사용
//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
        }()
				...
}

2. SceneDelegate.swift에서 등록했던 ViewController를 사용한다.

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()
    }

3. ViewController.swift에서 등록했던 Animal을 사용한다.

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!)")
    }
}

같은 프로토콜을 준수하는 클래스가 여러개 일 경우

  • Animal 프로토콜을 준수하는 Cat, Dog 클래스가 있을 때의 예시
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!)
    }
}

이슈 : Swinject 라이브러리 import 후 build 안되는 이슈 발생

원인

구글링을 통해 확인한 결론은 외부라이브러리 사용 없이 프로젝트를 생성하고 사용할 경우에는 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를 지키기 위한 디자인 패턴

profile
배우고 적용하고 개선하기
post-custom-banner

0개의 댓글