블로그 글들 이리저리 긁어와서 내가 이해하기 쉬운 순서대로 재구성!
모듈 간의 종속성이 있는 경우, 서로를 바라보는 의존성 그래프가 그려집니다.
이를 즉 모듈 간의 순환 종속성 관계(Circular Dependency)를 가진다고 하는데요, 적절한 모듈화도 좋지만, 순환 종속성이 생기면 모듈화를 섣불리 하기 어렵습니다. ([iOS][Swift] 모듈간의 관계를 Dependency Injection Container으로 풀어보자)
우리는 이러한 문제점을 어떻게 해결 할 수 있을까요?
바로 경험상 의존성 주입을 통해서 해결해야한다는 것을 알고 있습니다 (경험해보지 못하셨다면 유감!)
각 모듈에서는 자신이 필요한 기능을 정의해둔 protocol 을 정의해두고 사용합니다. 그리고 메인 프로젝트에서는 각 모듈을 알고 있으므로 실제 concrete 한 구현체는 이곳에서 주입을 해주거죠!
의존성 주입을 해줄때는 이렇게 보통 밖에서 인스턴스를 만들어서 주입해줍니다. 하지만 밖에서 인스턴스를 만들어서 주입해주는 곳은 앱에서 여러군데 입니다. 즉 인스턴스를 만드는 위치가 분산되어서 관리포인트가 많아지게 됩니다!
그렇다면
🤔 : 흠.. 상자 (📦) 안에
- 내가 사용할 모든 인스턴스를 미리 다 만들어서 등록해두고,
- 필요한 시점에 이 상자 안에 등록해뒀던 인스턴스를 달라고 하면
인스턴스 초기화는 이 상자 안에서 한번만 해주면 되겠네?! 라고 생각할 수 있습니다. ([DI] DI Container, IOC Container 개념과 예제)
오호.. 상자의 필요성이 조금 다가옵니다. 이 상자 (📦) 의 필요성을 좀 더 이해하기위해, 상자가 없을 때의 다른 상황을 살펴보겠습니다.
상자가 없다면 우리는 아래와 같이 앱 target 프로젝트에서 직접 Storage 라는 인스턴스를 주입할텐데요,
let manager = FilesystemManager(storage: Storage())
🤔 : FilesystemManager 에서는 어차피 protocol 로 인자를 받고 있을때고, concrete 한 구현체인 Storage 는 모르고 있으니까 상관없는거 아냐??
저도 그런것 같았는데..!! 앱 taget 에서 Storage의 생성과 FileSystemManager의 관계 설정을 해주고 있는 상황이 IoC 에는 위배된다고 하네요.
IoC 란 Inversion of Control 의 약자로, 기존 구조적 설계와 비교해 프레임워크가 제어를 나누어 가져가되어 의존 관계가 방향이 달라지게 되는 것을 제어가 반전,역전되었다고 합니다.
어쨌든 IoC에 의하면, 앱 taget 은 Storage를 알 필요가 없는 상황이고, 그냥 아까 말한 상자(📦)에서 객체를 관리하고 생성을 책임지고, 의존성을 관리하는 역할을 하면 되는거죠! ([iOS] Dependency Injection, (IoC, DI, DIP))
그리고 우리는 이 상자를 "DI Container" 혹은 "IoC Container" 라고 부르기로 합니다.
좋아여 그럼 여기까지
- 모듈간의 순환 종속성의 문제점을 해결하기 위해 의존성 주입이 필요하고,
- 의존성 주입의 관리 포인트를 줄이고, 의존 관계의 역전을 위해 DI / IoC Container 의 개념이 도입이 되었다! 를 알아봤습니다.
그럼 실제로 한번 구현을 해볼까요?
🤔 : ..........에??? 제가요.....?
뭐 여러 블로그 글들을 참고해서 차근차근 따라가보면 야 너두 할 수 있어! 겠지만.... 의존성을 관리할 컨테이너를 직접 구현해야하고, 여기서 객체를 사용하기 위해서는 컨테이너를 생성할 때 모든 객체 생성자를 넣어줘야하고.. 등등의 불편함이 있겠죠? ([Swift] Needle DI Tool - 의존성 라이브러리)
그래서 등장했습니다! DI Container 도구!! 바로 Swinject 나 Needle 같은 친구들이져!
iOS 진영에서 주로 사용하는 프레임워크로 Swinject 가 있습니다. 다만 얘는 컴파일 시점에 안전성을 확인하기 어렵다는 단점이 있는데요, 런타임에 dependency 를 등록하기 때문에 resolve 시점에 값이 없을 수도 있습니다. (iOS) Needle 로 의존성 주입하기, Swift Dependency Injection)
또한 의존성 수가 점점 많아질 수록, 수동으로 연결해야하는 수많은 객체가 생깁니다. 귀찮기도 할뿐더러 더더욱 안전성을 보장하기 어렵습니다.
그에 반해 Needle 의 차밍 포인트는 아래와 같습니다. (모듈화하고 Needle 적용해보기)
- 컴파일 타임에서 잘못된 DI 계층을 지적해주기 때문에 컴파일 시점의 안정성 보장
- 매번 새로운 객체를 추가할 때마다 register 해주는 코드를 작성할 필요 없이 자동으로 생성해줌
- 컴파일과 동시에 계층적으로 그려진 DI 코드를 자동으로 생성해줌
그럼 이제 Needle 을 본격적으로 알아보러 가볼까요? (iOS) Needle 로 의존성 주입하기)
Needle 에서 각 의존성의 범위는 Component
로 정의하고, 그에 대한 의존성은 protocol
로 캡슐화됩니다. 그리고 이 둘을 제네릭을 사용하여 연결합니다.
🤔 : 뭐라고요.....?
ㅎㅎ 코드로 바로 가죠!
먼저 필요한 의존성을 프로토콜로 정의합니다.
protocol MyDependency: Dependency {
var chocolate: Food { get }
var milk: Food { get }
}
그러니까 여기서는 chocolate 이랑 milk 를 상위로부터 의존성을 주입 받고 싶은 값이라는거겠져?
이렇게 정의한 의존성 프로토콜을 활용해 컴포넌트를 정의합니다.
class MyComponent: Component<MyDependency> {
}
그러면 이 안에서는 dependency.chocolate
, dependency.milk
처럼 값에 접근할 수 있게 됩니다.
만약 이번에는 hotChocolate 을 주입받고 싶고 싶은 애 (MyChildComponent) 가 있다?! 하면 똑같이 dependency 를 정의해주고, 컴포넌트로 연결해서 사용하면 되는데요
protocol MyChildDependency: Dependency {
var hotChocolate: Drink { get }
}
class MyChildComponent: Component<MyChildDependency> {
var veryHotChocolate: Drink {
return VeryHotChocolate(dependency.hotChocolate)
}
}
그럼 여기서 실제로 이 Component 의 의존성(MyDependency)은 어디서 획득하냐!! 라는 의문이 들 수 있습니다.
그건 바로 상위 Scope 에서 정의를 해줍니다. 상위 Scope란, 해당 컴포넌트 생성에 사용하는 parent 를 의미합니다.
아까 예시로 들면 요렇게 실제로 MyChildComponent 를 생성하는 곳, 즉 parent scope 인 MyComponent 가 parent 가 되고, 이곳에서 child 에 필요한 의존성을 정의해두는거져
class MyComponent: Component<MyDependency> {
// 새로운 객체인 hotChocolate을 의존성 그래프에 추가합니다.
// 하위 Scope들에서 Dependency 프로토콜을 통해 이를 획득할 수 있습니다.
var hotChocolate: Drink {
return HotChocolate(dependency.chocolate, dependency.milk)
}
// 자식 Scope는 항상 부모 Scope에 의해 인스턴스화됩니다.
var myChildComponent: MyChildComponent {
return MyChildComponent(parent: self)
}
}
🤔 음.. 대충 감은 잡은거 같은데.. Component 를 실제로 어떻게 이용하는데?? 실제 예시를 들어봐라!
먼저 최상위 컴포넌트를 정의합니다. 상위 Scope 이 없는 BootstrapComponent 를 활용합니다.
final class RootComponent: BootstrapComponent {}
이에 RootViewController 와 필요한 의존성들을 정의합니다. 로그인 화면과 로그아웃된 화면이 필요하기에 각각을 컴포넌트로 정의합니다.
RootComponent 예시
// RootComponent.swift
final class RootComponent: BootstrapComponent {
var playersStream: PlayersStream {
return mutablePlayersStream
}
// 해당 스코프에 객체가 하나로 유지되어야 하면 shared 를 활용해요
// RootComponent 에서 활용하면 싱글톤 패턴으로 활용 가능해요
var mutablePlayersStream: MutablePlayersStream {
return shared { PlayersStreamImpl() }
}
var rootViewController: UIViewController {
return RootViewController(
loggedOutBuilder: loggedOutComponent,
loggedInBuilder: loggedInComponent
)
}
var loggedOutComponent: LoggedOutComponent {
return LoggedOutComponent(parent: self)
}
var loggedInComponent: LoggedInComponent {
return LoggedInComponent(parent: self)
}
}
각 서브 컴포넌트 예시
예를 들어 로그 아웃 화면이 필요하기 때문에 별도의 Component 로 정의를 하고, 여기서 필요한 의존성을 주입받아서 loggedOutViewController 를 생성해줄 수 있습니다.
protocol LoggedOutDependency: Dependency {
var mutablePlayersStream: MutablePlayersStream { get }
}
final class LoggedOutComponent: Component<LoggedOutDependency>, LoggedOutBuilder {
var loggedOutViewController: UIViewController {
return LoggedOutViewController(
mutablePlayersStream: mutablePlayersStream
)
}
}
// ViewController 를 지연 생성하기 위해 프로토콜과 computed property 활용
protocol LoggedOutBuilder {
var loggedOutViewController: UIViewController { get }
}
(뭔가 볼수록 RIBs 같기도 하고..)
그럼 우리 프로젝트에서 적용을 해보겠습니다.
Needle 은 NeedleFoundation 프레임워크와 executable code generator 로 구성됩니다. Needle을 DI 시스템으로 사용하려면 두 부분 모두를 Swift 프로젝트에 통합해야 합니다.
1 . NeedleFoundation
framework 설치하기
NeedleFoundation
프레임워크를 Swift 프로젝트와 통합하려면 표준 Swift Package Manager 패키지 정의 프로세스를 통해 Needle을 의존성에 추가합니다.
dependencies: [
.package(url: "https://github.com/uber/needle.git", .upToNextMajor(from: "VERSION_NUMBER")),
],
targets: [
.target(
name: "YOUR_MODULE",
dependencies: [
"NeedleFoundation",
]),
],
- code generator 설치하기
brew install needle
needle code generator 는 개발자가 작성한 코드를 구문 분석해 Swift 소스 코드를 생성하는 커맨드라인 유틸리티입니다. 생성된 코드는 개발자가 작성하는 다양한 Component
서브클래스를 연결합니다. DI 그래프 구조에 따라 generator 는 각 Component
를 연결합니다. 생성된 코드는 앱에서 컴파일되며 완전한 DI 그래프를 제공합니다. needle code generator 의 대략적인 동작방식은 iOS) Needle 로 의존성 주입하기 을 참고해주세요
이렇게 설치한 code generator 를 실행할 수 있도록 build phase 에 스크립트를 작성해줘야하는데요, 저는 블로그에서 본 대로 제일 간단히 써봤습니다
if which needle; then
SOURCEKIT_LOGGING=0 && needle generate $SRCROOT/Sources/NeedleGenerated.swift $SRCROOT/.. / else echo "warning: Needle not installed, download from https: //github.com/uber/needle using Homebrew"
fi
이제 코드를 작성하고 런했을때 결과는!?!
ㅎㅎㅎ.. 역시 니들 스크립트 실행할때부터 에러 터집니다.
Showing All Messages
SourceParsingFramework/FileEnumerator.swift:161: Fatal error: Failed to traverse file:///Library/Application%20Support/Apple/ParentalControls/Users with error Error Domain=NSCocoaErrorDomain Code=257 "The file “Users” couldn’t be opened because you don’t have permission to view it." UserInfo={NSURL=file:///Library/Application%20Support/Apple/ParentalControls/Users, NSFilePath=/Library/Application Support/Apple/ParentalControls/Users, NSUnderlyingError=0x600001bb4360 {Error Domain=NSPOSIXErrorDomain Code=13 "Permission denied"}}.
에러 로그를 보면.. 권한 실패요..?? Users 폴더에 읽기 권한 설정해줬는데도 해당 에러가 떴습니다.
좀 더 구글링해서 The file couldn’t be opened because you don’t have permission to view it. 보고 엑코 자체에 전체 디스크 권한 주려고 했는데.. 에? 왜 베타밖에 안떠요..?
Xcode 가 세개있는데, beta 한개밖에 안뜨길래,, 한개만 설정되는건가??? 해서 저 목록에서 베타를 삭제하고 다시 Xcode-13 을 클릭해도,, 베타가 뜹니다......? 엑코 괴담인가여..?
beta 지우고 테스트해보고 싶다가도.. 다시 설치하려면 너무 한나절이라.. 이렇게 하면 될거라는 일단 믿음으로 스킵하겠읍니다...?
여기까지 얼레벌레 needle 찍먹하기 끝!