iyOmSd/Title: Swift

[Swift] Needle DI Tool - 의존성 라이브러리

냄수 2022. 9. 10. 17:37
반응형

DI 도구를 왜써야하죠...?

물론 도구를 사용하지않고도 할 수 있지만 의존성을 관리하는데 도움이되고 간편하게 사용할 수 있기 때문이죠

도구를 사용하지않고 생성자에서 직접 객체를 주입받는다고했을때

도구를 사용하지않는다면 의존성을 관리할 컨테이너를 직접구현해서 사용하거나

객체를 사용하기위해 생성할 때 모든 생성자를 모두 넣어줘야하고 그때 그때 생성해야하는 불편함이 있겟죠

 

 

특히 이번에 해볼 Needle은 우버에서 만들어서 사용하고 있는 의존성 관리 도구죠

계층구조로 작성하도록 유도하고 컴파일시점에서 DI를 확인하는

이점을 가지고있어요

다른 의존성 도구들은 런타임에서 확인하기떄문에 개발자가 실수를 하면 실행해서야 알수 있게되는거죠

또한 상위객체의 의존성을 주입할때 따로 코드를 정의하지않아도 자동으로 생성되는 편리함이있죠

A객체에서 a를 사용하고있을때

B객체에서 a를 사용하고싶을때 주입하는 코드를 따로 작성하지않고 이 변수를 사용한다고 정의만해주면 알아서 코드가 생성되서

a를 B가 생성될대 넘겨줘요 아주 간편하죠

 

더 자세한 설명과 소개는 깃허브에 한글로도 잘 설명되어있어요 게시글 끝에 참고하세요

 

사용법

이제 간단하게 사용법을 알아볼게요

우선 깔아야해요!

터미널로 들어가서

brew install needle

를 통해 니들을 셋팅해줍니다

 

needle generate (파일생성경로) (분석할 경로)

를 통해서 코드를 분석하고 노드사이의 의존성을 연결하고 객체를 생성합니다

 

필수 파라미터로

파일생성경로, 분석할 경로 가 있구요

옵션으로는 다양하게 많아요 

깃허브에도 잘 써잇지만 몇개만보자면

 

--sources-list-format newline

newline - 각행이 구문분석할 swift소스파일에 대한 단일 경로라고 가정함 - default

minescaping - 작은 따옴표로 이스케이프되도록 지정하고 필요하지않는경우는 따옴표로 묶지않음

 

--exclude-suffixes Tests

구문분석을위해 무시할 파일이름 접미사

위와같은 명령어인 경우 파일확장자를 제외한 "Tests"로 끝나는 모든 파일 무시

 

--exclude-paths

해당경로에 포함된 파일을 무시함

 

이정도가 있겠네요 여기까지 사용법을 훑어봤으니

 

코드로 구현해볼게요

 

간단한 예제를 만들어볼거에요

처음화면에 사람과 로봇을 선택하는 버튼이있고

각버튼을누르면 버튼에 대응하는 텍스트를 띄우도록 말이죠

Needel을 사용하지않고 먼저 만들어보면

 

일반구현

뷰모델에는 간단한 텍스트만 저장하도록 만들었구요

protocol ViewModel {
    var text: String { get }
}

final class RobotViewModel: ViewModel {
    let text: String
    
    init(text: String) {
        self.text = text
    }
}

final class PeopleViewModel: ViewModel {
    let text: String
    
    init(text: String) {
        self.text = text
    }
}

 

뷰컨트롤러 코드입니다

// needle사용안하고 일반적인 DI
final class NormalViewController: UIViewController {
    private let questionLabel: UILabel = UILabel()
    private let stackView: UIStackView = UIStackView()
    private let peopleButton: UIButton = UIButton(configuration: .filled())
    private let robotButton: UIButton = UIButton(configuration: .filled())
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setup()
    }
    
    private func setup() {
        setLayout()
        setUI()
    }
    
    private func setLayout() {
        questionLabel.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(questionLabel)
        questionLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        questionLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 120).isActive = true
        
        view.addSubview(stackView)
        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.centerXAnchor.constraint(equalTo: questionLabel.centerXAnchor).isActive = true
        stackView.topAnchor.constraint(equalTo: questionLabel.bottomAnchor, constant: 50).isActive = true
        stackView.widthAnchor.constraint(equalToConstant: 300).isActive = true
        
        stackView.addArrangedSubview(peopleButton)
        stackView.addArrangedSubview(robotButton)
    }
    
    private func setUI() {
        questionLabel.text = "당신은 사람입니까?"
        stackView.spacing = 20
        peopleButton.setTitle("사람입니다", for: .normal)
        let peopleAction = UIAction { _ in
            self.tapPeople()
        }
        peopleButton.addAction(peopleAction, for: .touchUpInside)
        
        robotButton.setTitle("로_봇-입_니-다", for: .normal)
        let robotAction = UIAction { _ in
            self.tapRobot()
        }
        robotButton.addAction(robotAction, for: .touchUpInside)
    }
    
    private func tapPeople() {
        let viewModel = PeopleViewModel(text: "사람 입니다 🙆")
        let viewController = NormalSecondViewController(viewModel: viewModel)
        present(viewController, animated: true)
    }
    
    private func tapRobot() {
        let viewModel = RobotViewModel(text: "로봇 입니다 🤖")
        let viewController = NormalSecondViewController(viewModel: viewModel)
        present(viewController, animated: true)
    }
}

final class NormalSecondViewController: UIViewController {
    private var titleLabel: UILabel = UILabel()
    private let viewModel: ViewModel
    
    init(viewModel: ViewModel) {
        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()
        view.backgroundColor = .systemBackground
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(titleLabel)
        titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        titleLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 120).isActive = true
        titleLabel.font = .systemFont(ofSize: 40)
        titleLabel.textAlignment = .center
        
        titleLabel.text = viewModel.text
    }
}

 

 

private func tapPeople() {
    let viewModel = PeopleViewModel(text: "사람 입니다 🙆")
    let viewController = NormalSecondViewController(viewModel: viewModel)
    present(viewController, animated: true)
}

private func tapRobot() {
    let viewModel = RobotViewModel(text: "로봇 입니다 🤖")
    let viewController = NormalSecondViewController(viewModel: viewModel)
    present(viewController, animated: true)
}

각 버튼이 눌렸을때 사람뷰모델, 로봇뷰모델을 생성해서

NormalSecondViewController에 넘겨주고

final class NormalSecondViewController: UIViewController {
    private let viewModel: ViewModel
    
    init(viewModel: ViewModel) {
        self.viewModel = viewModel
        super.init(nibName: nil, bundle: nil)
    }
}

NormalSecondViewController는 ViewModel프로토콜을 준수만한다면 받아서

텍스트를 보여주도록 설계되어있죠

 

Needle 타입

이 과정을 Needle을 이용해서 해볼게요

코드를 구현하기앞서 알아야할 타입이있어요

Needle에는 타입이 크게 3가지가 있어요

Component

BootstrapComponent

Dependency

 

하나씩 보면

 

Component

의존성 Scope를 정의해요

각 의존성 범위는 Component로 정의되고 그 의존성은 protocol로 캡슐화되고

제네릭을 이용하여 Component - Dependency 둘이 연결이되요

필요한 하위 Component를 만들어서 트리구조처럼 작성할 수 있죠

final class PeopleComponent: Component<PeopleDependency> { ... } // 사용시

 

 

BootstrapComponent

이름에서도 볼 수 있듯이 뭔가 빠르게 만들것 같죠?

상위Dependency 가 없는경우 간단하게 사용할 수 있는 타입이에요

Component<EmptyDependency>와 같아요

open class BootstrapComponent: Component<EmptyDependency> { ... } // 정의
final class RootComponent: BootstrapComponent { ... } // 사용시

 

Dependency

상위 component로 부터 가져올 의존성을 정의해요

final class RootComponent: BootstrapComponent {
    var prefixTitle: String { "당신은..." }
    var someProperty: String { "어떤 프로퍼티" }
}

// 사람인경우는 2개모두 의존성
protocol PeopleDependency: Dependency {
    var prefixTitle: String { get }
    var someProperty: String { get }
}

// 사람인경우는 1개 변수만 의존성
protocol RobotDependency: Dependency {
    var prefixTitle: String { get }
}

// ❌ 변수 이름이 달라서 에러발생
protocol RobotDependency: Dependency {
    var prefixTitle123: String { get }
}

이때 상위의 프로퍼티와 이름을 다르게 지정한다면 에러가 발생해요

prefixTitle123으로 지정하면 발생하는 에러

Dependency로 정의한 프로퍼티들은

dependency프로퍼티를 이용해서 접근할 수 있어요

dependency.prefixTitle 이런식으로말이죠

 

Needle 사용구현

처음으로

루트 컴포넌트를 만들거에요

루트는 상위컴포넌트가 없으니까 의존성주입받을 필요가없겟죠?

Bootstrap을 준수해줍니다

 

루트에서 사람인지 로봇인지 선택하기떄문에

루트에서 사람, 로봇컴포넌트를 만들어서 전달해줄거에요

final class RootComponent: BootstrapComponent {
    var prefixTitle: String { "당신은..." }
    var someProperty: String { "어떤 프로퍼티" }
    
    var rootViewController: UIViewController {
        ViewController(
            robot: robotComponent,
            people: peopleComponent
        )
    }
    
    var robotComponent: RobotComponent {
        RobotComponent(parent: self)
    }
    
    var peopleComponent: PeopleComponent {
        PeopleComponent(parent: self)
    }
}

 

그 다음으로는

원하는 텍스트를 넣어서 뷰모델을 만들고 뷰컨트롤러에 주입시켜야하죠

 

사람 컴포넌트에서는

사람입니다를 보여줄 뷰컨트롤러를 띄울거에요

따라서 뷰모델에 사람입니다를 정의하도록할게요

protocol PeopleDependency: Dependency {
    var prefixTitle: String { get }
    var someProperty: String { get }
}

final class PeopleComponent: Component<PeopleDependency> {
    var peopleViewContoller: UIViewController {
        SecondViewController(viewModel: viewModel)
    }
    
    var viewModel: ViewModel {
        PeopleViewModel(text: "\(dependency.prefixTitle)사람 입니다 🙆")
    }
}

부모컴포넌트에 있는 prefixTitle을 사용하기위해 Dependency를 정의했어요

ViewModel을 정의할때 필요한 정보를 넣어주고

뷰모델을 주입시켜서 뷰컨트롤러를 생성하는 코드까지 작성했어요

 

다음으로는 로봇컴포넌트

protocol RobotDependency: Dependency {
    var prefixTitle: String { get }
}

final class RobotComponent: Component<RobotDependency> {
    var robotViewContoller: UIViewController {
        SecondViewController(viewModel: viewModel)
    }
    
    var viewModel: ViewModel {
        mutableViewModel
    }
}

extension RobotComponent {
    var mutableViewModel: ViewModel {
        shared { RobotViewModel(text: "\(dependency.prefixTitle)로봇 입니다 🤖") }
    }
}

기능은같은데

부모컴포넌트에서 하나의 프로퍼티만 의존성을 가져왔고

마지막에

shared를 사용했죠

컴포넌트 프로퍼티를 보면 모두 연산프로퍼티처럼 사용했죠

클린하게 컴포넌트를 사용하기위해 접근시 새로운 객체를 만들어서 전달해주는 방식이지만

shared는 싱글턴처럼 사용할 수 있도록 해줘요

해당 컴포넌트가 살아있는동안 현재 scope와 모든하위 scope에서 단일 인스턴스를 공유할 수 있어요

 

컴포넌트를 다만들었으니

뷰컨트롤러 코드를 구현해볼까요?

위에서 스타일과 탭클릭 이벤트연결은 다동일하므로 제거하고 

새로생긴 Needle위주로 코드에요

final class ViewController: UIViewController {
    let robotComponent: RobotComponent
    let peopleComponent: PeopleComponent
    
    init(robot: RobotComponent,
         people: PeopleComponent) {
        robotComponent = robot
        peopleComponent = people
        super.init(nibName: nil, bundle: nil)
    }
    
    private func tapPeople() {
        present(peopleComponent.peopleViewContoller, animated: true)
    }

    private func tapRobot() {
        present(robotComponent.robotViewContoller, animated: true)
    }
}

final class SecondViewController: UIViewController {
    private var titleLabel: UILabel = UILabel()
    private let viewModel: ViewModel?
    
    init(viewModel: ViewModel) {
        self.viewModel = viewModel
        super.init(nibName: nil, bundle: nil)
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
       
        titleLabel.text = viewModel?.text
    }
}

뷰컨트롤러를 생성할때 robot, people을 주입받고

viewController를 각 컴포넌트에서 생성해주고

SecondViewController에선 어떤 객체가오던 뷰모델의 text만 사용하는 모습을 볼 수 있죠

또한 prefixTitle도 붙였기때문에 앞과는 조금다른 UI가 나올거에요 (앞에 `당신은...` 이 추가되겟죠?)

 

아마 빌드가 안되는데 어떻게 했지? 할수 있어요

안돼는게 맞아요

아직 needle을 생성하지 않았기 때문이죠!

 

이제 위에서봣던 명령어를 이용해서 생성해줘야해요

저는 단순하게 구조를 만들지않고 바로 프로젝트폴더에 생성할거에요

 

터미널을키고 프로젝트 경로에가서

needle generate ./test_needle/NeedleGenerated.swift ./test_needle

혹은

Xcode run script를 설정해줄 수 있어요

Project -> Build Phases -> + -> New Run Script Phase

 

환경변수를 이용한 경로로 적용해도 좋아요

 

 

이렇게하면 파인더에 NeedleGenerated.swift 가생성되는걸 볼 수 있고

해당파일을 프로젝트에 추가시켜야해요

그리고

scene 혹은 appdelegate에가서

registerProviderFactories() 를 실행해주면!

자동으로 코드가 생성됩니다!

 

그다음으로  컴포넌트를 생성하고 window를 갈아끼워줘야겟죠

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.

    registerProviderFactories()
    
    let root = RootComponent()
    let rootVC = root.rootViewController
    rootVC.view.backgroundColor = .systemBackground
    window?.rootViewController = rootVC
    window?.makeKeyAndVisible()
    return true
}

 

컴포넌트와 디펜던시를 수정하고

빌드를 하면 실패하게되고

프로퍼티를 수정했기때문에 다른 객체들도 수정해야하는걸 알려줘서 개발자의 실수를 방지하는게 좋은것같아요

 

수정시

Needle생성된 파일을 삭제하고

registerProviderFactories를 잠시주석처리하고

빌드하면 다시 새롱누 Needle파일이 생성될거에요

그리고 다시 registerProviderFactories주석을 제거하고

실행시키면 정상적으로 동작해요!

 

 

 

 

Needle뷰컨트롤러 전체코드입니다

final class ViewController: UIViewController {
    private let questionLabel: UILabel = UILabel()
    private let stackView: UIStackView = UIStackView()
    private let peopleButton: UIButton = UIButton(configuration: .filled())
    private let robotButton: UIButton = UIButton(configuration: .filled())
    
    let robotComponent: RobotComponent
    let peopleComponent: PeopleComponent
    
    init(robot: RobotComponent,
         people: PeopleComponent) {
        robotComponent = robot
        peopleComponent = people
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setup()
    }
    
    private func setup() {
        setLayout()
        setUI()
    }
    
    private func setLayout() {
        questionLabel.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(questionLabel)
        questionLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        questionLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 120).isActive = true
        
        view.addSubview(stackView)
        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.centerXAnchor.constraint(equalTo: questionLabel.centerXAnchor).isActive = true
        stackView.topAnchor.constraint(equalTo: questionLabel.bottomAnchor, constant: 50).isActive = true
        stackView.widthAnchor.constraint(equalToConstant: 300).isActive = true
        
        stackView.addArrangedSubview(peopleButton)
        stackView.addArrangedSubview(robotButton)
    }
    
    private func setUI() {
        questionLabel.text = "당신은 사람입니까?"
        stackView.spacing = 20
        peopleButton.setTitle("사람입니다", for: .normal)
        let peopleAction = UIAction { _ in
            self.tapPeople()
        }
        peopleButton.addAction(peopleAction, for: .touchUpInside)
        
        robotButton.setTitle("로_봇-입_니-다", for: .normal)
        let robotAction = UIAction { _ in
            self.tapRobot()
        }
        robotButton.addAction(robotAction, for: .touchUpInside)
    }
    
    private func tapPeople() {
        present(peopleComponent.peopleViewContoller, animated: true)
    }

    private func tapRobot() {
        present(robotComponent.robotViewContoller, animated: true)
    }
}

final class SecondViewController: UIViewController {
    private var titleLabel: UILabel = UILabel()
    private let viewModel: ViewModel?
    
    init(viewModel: ViewModel) {
        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()
        view.backgroundColor = .systemBackground
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(titleLabel)
        titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        titleLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 120).isActive = true
        titleLabel.font = .systemFont(ofSize: 40)
        titleLabel.textAlignment = .center
        
        titleLabel.text = viewModel?.text
    }
}

 

 

Needle 문서링크입니다 :)

https://github.com/uber/needle/tree/master/Documents/ko_KR

 

GitHub - uber/needle: Compile-time safe Swift dependency injection framework

Compile-time safe Swift dependency injection framework - GitHub - uber/needle: Compile-time safe Swift dependency injection framework

github.com

 

 

 

 

 

반응형