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 }
}
이때 상위의 프로퍼티와 이름을 다르게 지정한다면 에러가 발생해요
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
'iyOmSd > Title: Swift' 카테고리의 다른 글
[Swift] Xcode Cloud(CI/CD) + Tuist(프로젝트관리툴) + dSYMs 업로드까지 자동화 배포하기 (feat. 스크립트쉘) (0) | 2022.11.11 |
---|---|
[Swift] Xcode Cloud CI/CD (0) | 2022.11.01 |
[Swift] Tuist 모듈화 툴 (1) | 2022.08.05 |
[Swift] Combine의기본 Publisher이란? (0) | 2022.06.13 |
[Swift] Xcode 템플릿 만들기 (0) | 2022.04.26 |