[Swift] Pure DI practice

이 전 글에서 Pure DI 에 대한 설명을 간략하게 해보았습니다. 이번 포스팅에서는 Swift에서 어떻게하면 Framework 사용 없이 의존성 주입을 해볼 수 있을지 아주 간략하게 정리해보도록 하겠습니다.

프로젝트 구성

download code

의존성 주입

코드를 살펴보면 두 개의 뷰 컨트롤러 모두 StringGenerator 클래스에 의존성이 있습니다. 만약 StringGenerator 대신에 다른 클래스를 사용하고 싶다면 뷰 컨트롤러들까지 모두 수정을 해주어야 합니다. 따라서 StringGenerator를 프로토콜로 간접화시키고 의존성을 주입하도록 수정해줍니다.

먼저 StringGenerator를 프로토콜을 이용해 간접화시켜보도록 하겠습니다. 그리고 추가적인 StringGenerator를 만들어줍니다.

StringGenerator.swift

protocol StringGeneratorProtocol {
func generate() -> String
}
class StringGenerator:StringGeneratorProtocol {
func generate() -> String {
return "Hello world"
}
}
class StringGenerator2:StringGeneratorProtocol {
let text:String

init(text:String) {
self.text = text
}

func generate() -> String {
return "Hello \(text)"
}
}

다른 뷰 컨트롤러 부분들을 수정해줍니다.

FirstViewController

import UIKitclass FirstViewController: UIViewController {

// MARK: Properties
// 타입을 StringGenerator에서 StringGeneratorProtocol로 변경해줍니다.
private let stringGenerator:StringGeneratorProtocol

private lazy var stringLabel:UILabel = {
let label = UILabel()
return label
}()

private lazy var redirectButton:UIButton = {
let bt = UIButton(type: UIButton.ButtonType.system)
bt.setTitle("두번째 뷰", for: UIControl.State.normal)
bt.addTarget(self, action: #selector(redirectButtonTapped), for: UIControl.Event.touchUpInside)
return bt
}()
// MARK: Lifecycles
// 생성자 추가
init(generator:StringGeneratorProtocol) {
self.stringGenerator = generator

super.init(nibName: nil, bundle: nil)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
configureUI()
drawView()
}

// MARK: Helpers
func drawView() {
self.stringLabel.text = stringGenerator.generate()
}

func configureUI() {
view.backgroundColor = .systemBackground

view.addSubview(stringLabel)
stringLabel.translatesAutoresizingMaskIntoConstraints = false
stringLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
stringLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 100).isActive = true

view.addSubview(redirectButton)
redirectButton.translatesAutoresizingMaskIntoConstraints = false
redirectButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
redirectButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -100).isActive = true
}

func presentSecondViewController() {

// 두번째 뷰 컨트롤러에 의존성 주입을 해주다 보니 첫번째 뷰 컨트롤러에서 의존성이 생겼습니다.
let generator:StringGeneratorProtocol = StringGenerator2(text: "Donggyu")
let secondView = SecondViewController(generator: generator)
self.present(secondView, animated: true, completion: nil)
}

// MARK: Selectors
@objc func redirectButtonTapped() {
presentSecondViewController()
}
}

SecondViewController

//
// SecondViewController.swift
// PureDISwift
//
// Created by 신동규 on 2021/01/19.
//
import UIKitclass SecondViewController: UIViewController {// MARK: Properties
// 타입을 StringGenerator에서 StringGeneratorProtocol로 변경해줍니다.
private let stringGenerator:StringGeneratorProtocol

private lazy var stringLabel:UILabel = {
let label = UILabel()
return label
}()

// MARK: Lifecycles
// 생성자 추가
init(generator:StringGeneratorProtocol) {
self.stringGenerator = generator

super.init(nibName: nil, bundle: nil)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
configureUI()
drawView()
}

// MARK: Helpers
func drawView() {
self.stringLabel.text = stringGenerator.generate()
}

func configureUI() {
view.backgroundColor = .systemBackground
view.addSubview(stringLabel)
stringLabel.translatesAutoresizingMaskIntoConstraints = false
stringLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
stringLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 100).isActive = true
}
}

Delegate 도 수정해줍니다.

SceneDelegate

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

guard let scene = (scene as? UIWindowScene) else { return }
self.window = UIWindow(windowScene: scene)
// 첫번째 뷰 컨트롤러에 의존성을 주입하다보니, Delegate 에서 의존성이 생겼습니다.
let generator = StringGenerator()
self.window?.rootViewController = FirstViewController(generator: generator)
self.window?.makeKeyAndVisible()
}

의존성 주입을 했지만 StringGenerator를 교체하려면 델리게이트와 두개의 뷰 컨트롤러를 수정해야 합니다. 추후에 프로젝트의 규모가 방대해지게 된다면 빼먹는 실수를 하기 쉽습니다. 그렇다면 의존성을 관리하는 파일을 하나 만들어서 한군데에서 해결할 수 있도록 수정해보도록 하겠습니다.

AppDependency

struct AppDependency {
let firstViewControllerDependency: FirstViewController.Dependency
}
// 첫번째 뷰컨트롤러의 의존성
extension FirstViewController {
struct Dependency {
// 클로저를 사용해 게으른 생성이 가능합니다
let generator:() -> StringGeneratorProtocol

// 첫번째 뷰컨에서 두번째 뷰컨트오 바로 이어지기 때문에 두번째 뷰컨의 의존성도 같이 넣어줍니다.
let secondViewControllerDependency: SecondViewController.Dependency
}
}
// 두번째 뷰컨트롤러의 의존성
extension SecondViewController {
struct Dependency {
let generator:() -> StringGeneratorProtocol
}
}

의존성을 담아줄 구조체를 생성하였습니다. 이제 앱 전체의 의존성을 관리해줄 resolve 메서드를 구현합니다.

static func resolve() -> AppDependency {
// 두번째 뷰컨트롤러의 의존성
let secondViewDependency: SecondViewController.Dependency = .init(generator: { StringGenerator() })
// 첫번째 뷰컨트롤러의 의존성
let firstViewDependency: FirstViewController.Dependency = .init(generator: { StringGenerator2(text: "Donggyu") }, secondViewControllerDependency: secondViewDependency)

return AppDependency(firstViewControllerDependency: firstViewDependency)
}

이제 컨트롤러부분부터 델리게이트까지 차례대로 수정해줍니다.

두번째 뷰 컨트롤러는 수정할 부분이 없습니다.

FirstViewController

// MARK: Properties

private let stringGenerator:StringGeneratorProtocol
private let secondViewDependency: SecondViewController.Dependency
.
.
.
init(generator:StringGeneratorProtocol, secondViewDependency: SecondViewController.Dependency) {
self.stringGenerator = generator
self.secondViewDependency = secondViewDependency
super.init(nibName: nil, bundle: nil)
}
.
.
.
func presentSecondViewController() { let secondView = SecondViewController(generator: secondViewDependency.generator())
self.present(secondView, animated: true, completion: nil)
}

Delegate

class SceneDelegate: UIResponder, UIWindowSceneDelegate {var window: UIWindow?

// 델리게이트의 의존성을 받습니다.
let appDependency: AppDependency = .resolve()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

guard let scene = (scene as? UIWindowScene) else { return }
self.window = UIWindow(windowScene: scene)

// 앱 디펜던시에 있는 퍼스트뷰컨의 의존성을 받아서 생성자 주입을 해줍니다.
let firstViewDependency = appDependency.firstViewControllerDependency
let firstViewController = FirstViewController(generator: firstViewDependency.generator(), secondViewDependency: firstViewDependency.secondViewControllerDependency
)
self.window?.rootViewController = firstViewController
self.window?.makeKeyAndVisible()
}
.
.
.

이렇게 해서 뷰 컨트롤러들의 generator를 변경하고 싶다면 앱 디펜던시의 resolve 부분만 수정해주시면 됩니다.

Born to be solo

Born to be solo