문제상황
첫번째 문제상황
프로젝트가 점점 비대해지면서(?) 사실 뷰컨은 아직까진 두개밖에 없지만.. 의존성 주입방법에 대해서 고민하고 찾아볼 수 밖에 없게 되었습니다. 이유인 즉슨, 현재는 SceneDelegate에서 뷰컨트롤러에 의존성을 주입해주고 있는데 뷰컨간의 이동이 생기게되면서 SceneDelegate 하나에서 의존성을 모두 주입할 수가 없는 노릇이었습니다. 예를들어, 만약 SceneDelegate에서 모든 의존성을 주입해주게 된다면 아래와 같은 상황이 발생하게 될겁니다.
물론, DIP를 사용해서 의존성을 역전시켜서 분리했지만 의존성 분리를 위해 ViewController들이 필요없는 ViewModel들을 갖고 있어야 된다면 DIP를 사용했다고 하더라도 의존성이 잘 분리되었다고 말할 수 있을까? 라는 생각이 들었습니다.
해결 과정
의존성을 주입해주는 방법중에 DI Container라는 방법을 찾았습니다.
제가 이해한 DI Container의 핵심 원리는 다음과 같습니다.
- 싱글톤을 사용해서 각 의존성을 주입하고 싶은 객체들의 identifier가 달라지는것을 방지해서 잘못된 참조가 발생하는것을 방지합니다.
- DIContainer에 등록할 때, 인스턴스를 저장해주고 꺼내서 사용할 때 해당 프로토콜로 캐스팅해주기 때문에 DIP도 만족할 수 있습니다.
제가 작성한 DI Container는 아래와 같습니다.
import Foundation
final class DIContainer {
static let shared = DIContainer()
private var dependencies: [String: Any] = [:]
private init() {}
func register<T>(
instance: T
) {
dependencies["\\(T.self)"] = instance
}
func resolve<T>(instanceName: String) -> T {
guard let instance = dependencies[instanceName] as? T else { preconditionFailure("인스턴스 등록 안됨.") }
return instance
}
}
///사용 예시
///SceneDelegate.swift
let issuesRepository = IssuesRepository(networkManager: NetworkManager.shared)
DIContainer.shared.register(
instance: IssueCreateViewModel(repository: issuesRepository)
)
DIContainer.shared.register(
instance: IssueListViewModel(
repository: issuesRepository,
issueCreateViewModel: DIContainer.shared.resolve(instanceName: Constants.InstanceName.issueCreateViewModel)
)
)
프로토콜을 활용해서 DIContainer를 사용하면서 DIP가 가능하게 만들었습니다. DIContainer를 활용해서 resolve할 때, 리턴타입이 DI받을 객체의 매개변수(DI할 인스턴스)에 프로토콜 타입으로 추론되므로 DIP가 가능해지게 됩니다.
그림을 잘 그린건지는 모르겠습니다..ㅎㅎ
트러블 슈팅
첫번째 트러블
아래는 맨 처음 만들었던 DIContainer 코드입니다.
final class DIContainer {
let shared = DIContainer()
private var dependencies: [String: Any] = [:]
private init() {}
func register<T>(
type: T.Type,
instance: Any
) {
dependencies["\\(type)"] = instance
}
func resolve<T>(
type: T.Type
) -> T {
guard let instance = dependencies["\\(T.self)"] as? T else { preconditionFailure("인스턴스 등록 안됨.") }
return instance
}
}
키 값을 프로토콜 타입 문자열로 정의했었는데, 프로토콜 타입 문자열로 키값을 정의하게되면 프로토콜 다형성을 사용하기 위해 (A & B & C)protocol 타입의 인스턴스를 dependencies에 저장해서 넣게되면 키 값 자체가 “(A & B & C)” 라서 dependencies[”C”]의 기능만 꺼내와서 사용하는게 불가능했습니다. 따라서 프로토콜 다형성을 사용하기 위해 키 값을 인스턴스 이름으로 정의하였습니다.
- 인스턴스의 타입을 DIContainer를 꺼내쓸 때마다 입력하는것(특정 기능을 위한 프로토콜을 꺼내서 사용하기 위해서 해당 프로토콜을 채택하는 객체의 이름도 알아야하는것)에 대한 문제?
- 다형성을 구현하는건 컴파일 타임과 런타임의 타입을 불일치 시키는것이기 때문에 컴파일타임에는 개발자가 블랙박스 상태로 해당 프로토콜의 기능을 사용할 수 있어야된다고 생각합니다. 키값을 설정하는데 더 괜찮은 방법이 있을지 궁금합니다..
두번째 트러블
웃픈 사실을 하나 발견하였습니다. 결국 아래코드처럼 작성하면 instance자체를 DIContainer가 resolve해주는건 똑같기 때문에 결합도가 낮춰지진 않습니다. 따라서 resolve인자로 프로토콜의 타입도 받아서 DIContainer내부에서 프로토콜로 캐스팅해서 리턴해주겠습니다. 라고 생각했는데,
private let issueCreateViewController = IssueCreateViewController(viewModel: DIContainer.shared.resolve(instanceName: Constants.InstanceName.issueCreateViewModel))
type에 자동으로 인자로 받고싶은 프로토콜의 타입이 들어가는것을 봤습니다.
아래는 resolve의 코드인데 제네릭 함수를 사용할 때, 리턴타입이 타입추론에 의해서 현재 들어갈 인자값의 타입을 추론합니다! 따라서, 위의 코드에서도 타입추론에 의해 알아서 타입캐스팅이 되서 들어가므로 IssueCreateViewModel() 자체를 리턴하는것이 아니고 인자로받을 프로토콜 타입이 추론되고 타입캐스팅되서 다형성을 유지할 수 있게됩니다!
func resolve<T>(instanceName: String, type: T.Type) -> T {
guard let instance = dependencies[instanceName] as? T else { preconditionFailure("인스턴스 등록 안됨.") }
return instance
}
참고
혼자서 공부하다가 작성한 글이라서 오개념이 존재할 수 있습니다😅. 혹시 잘못된 사실이 있다면 언제든지 댓글 남겨주세요!
'IOS > 실험' 카테고리의 다른 글
hitTest (0) | 2024.01.12 |
---|---|
mutating이 성능에 끼치는 영향 분석 및 Copy-On-Write와의 관계 (0) | 2023.12.21 |
RunLoop.main vs DispatchQueue.main (2) | 2023.10.16 |
String의 joined(separator:) DeepDive (2) | 2023.10.14 |
댓글