오늘은 Dependency Injection을 위한 Container를 구현해보자라는 주제를 가지고 온 킴스캐슬입니다
지금부터 Dependency Injection은 DI라고 말하겠습니다(단어가 너무 길어요...)
요즘 이전에 진행했던 프로젝트를 리팩토링하고 있는데요
DI로 네트워크 레이어를 분리하고 객체간의 결합도를 줄이려는 방향으로 개발을 진행하고 있습니다
이번 포스팅은 의존성주입(DI)에 관한 이야기를 하는 포스팅이 아니기때문에 DI에 관한 개념은 생략하겠습니다
제가 적은 DI관련 포스팅입니당
Dependency Injection(DI) - 이론편
Dependency Injection(DI) - 실전편
그리고 오늘 포스팅은 어쩌면 실패의 기록이라고 이야기할 수 있을거같아요
어떤 과정을 거치면서 어떤 문제를 만났고 어떤 시도를 했으며 어떤 결론을 내렸는지에 대한 이야기가 될거같습니다
그럼 시작해보겠습니다
사건의 시작은 단순히 레이어를 DI로 분리할때 시작되었습니다
프로젝트를 진행할때 나름 코드를 신경쓴다고하고 작성했지만 사실 당시에는 의존성주입을 전혀 신경쓰지 않았었습니다
그러다 보니 모든 객체가 내부에서 또다른 객체를 생성하는 말그대로 객체간의 의존성이 강하게 있는 그런 코드들이었습니다
ViewController안에 Service객체가 있고 Service객체 안에 APICaller객체가 있는 큰 틀의 구성이었는데
심지어 마이페이지 같은 경우는 마이페이지 Service객체와 로그인 Service객체 두가지 Serivce객체를 들고있는 구성이었습니다
그러다보니 각 네트워크 객체를 분리해서 프로토콜로 만들어서 외부에서 주입을 하는 작업또한 꽤나 공수가 들었습니다
의존성 주입을 통해 코드를 작성하면 ViewController를 만들때마다 ViewController가 들고있는 객체를 외부에서 주입해야하고 그 객체를 생성할때 그 객체가 들고있는 객체를 주입해야하는 무한지옥에 빠지게됩니다
근데 저희가 모든 에러처리를 각 레이어에서 throw로 던지고 그걸 전부 ViewController로 받는단말이죠?
extension ChallengeViewController: ViewControllerServiceable {
func handleError(_ error: NetworkError) {
switch error {
case .urlEncodingError:
LHToast.show(message: "url인코딩에러")
case .jsonDecodingError:
LHToast.show(message: "챌린지Decode에러")
case .badCasting:
LHToast.show(message: "배드캐스팅")
case .fetchImageError:
LHToast.show(message: "챌린지 이미지 패치 에러")
case .unAuthorizedError:
LHToast.show(message: "챌린지 Auth 에러")
case .clientError(_, let message):
LHToast.show(message: message)
case .serverError:
LHToast.show(message: "서버문제!")
}
}
}
이런식으로 말이죠...
사실 이 예시는 단순히 에러를 토스트메세지로 보여주기만 하면 되는데 이뷰를 제외하고 거의 대부분의 뷰가 특정에러를 처리할때 다른 ViewController로 rootViewController를 바꾸거나 navigation push를 합니다
그러면 아래와 같은 코드를 적어줘야합니다
/// 객체주입의 무한 굴레....
let VC = SplashViewController(authService: AuthService(api: AuthAPI(apiService: APIService())))
ViewControllerUtil.setRootViewController(window: window, viewController: VC, withAnimation: false)
뷰컨안에 서비스 서비스안에 서비스두개 각 서비스에 또다른 서비스를 만들어서 넣어줘야하는 딱 보기만해도 이렇게 하면안될거같은데...
라는 생각이 절로드는 코드를 작성해야합니다
더 큰 문제는 각 뷰컨에서 저런 에러처리를 하고 있으니까
하나만 바뀌어도 모든 뷰컨에서 ViewController객체를 생성하는 코드를 고쳐줘야합니다
혹여나 어떤 서비스 객체가 다른 객체를 가지게된다면...?
다시 모든 뷰컨에 가서 코드를 일일히 다 고쳐줘야합니다
이러한 불편함을 해결하려면 어떤 방식을 통해서 ViewController를 생성하게되면 해당 ViewController에 필요한 객체들을 주입해준상태로 생성시켜주면 되는거겠죠
이렇게 불편한 일을 10번정도 반복하다보니 이대로는 안되겠다싶어서 방법을 수소문하게 되었고 DI Container
방식에 대해 알게 되었습니다
제가 좀 전에 말했던
어떤 방식
을 통해서 ViewController를 생성하게되면 해당 ViewController에 필요한 객체들을 주입해준상태로 생성시켜주면 되는거겠죠
이 말에서 어떤 방식
을 담당하고 있는 친구가 DI Container
라는 친구입니다
DI Container에 대해서 간단하게 설명하면
어떤 객체를 만들어서 Container에 등록해놓으면(특정 key값을 가지고) 그 객체를 사용하고 싶은 곳에서 key를 가지고 만들어진 객체를 사용할 수 있게 되는 메커니즘이라고 생각하시면 지금부터 제가 말씀드리는 예시를 이해하시는데는 충분할거라고 생각됩니다
우선 key를 가지고 객체를 저장하는거기때문에 Container는 dictionary가 필요할겁니다
그리고 객체를 등록하는 메서드와 꺼내는 메서드가 있으면 간단한 Container를 만들 수 있습니다
아주 간단하게 String타입의 key를 가지고 객체를 저장할 수 있는 dictionary입니다
private var services: [String: Any] = [:]
Any를 채택한 이유는 사실 어떤 타입이 들어올줄 모르잖아요? 그러니까 어떤 타입이라도 될수있는 Any를 채택헀습니다
/// 쓰기(Write)
func register<T>(type: T.Type, component: AnyObject) {
let key = "\(type)"
services[key] = component
}
실제로 container에 등록하는 메서드는 타입자체를 string으로 변환해서 key로 가지고 componet를 value로 가져서 dictionary에 등록하게 로직을 구성했습니다
왜 굳이 타입자체를 받아서 string으로 변환하나요?
사실 이렇게 한 이유는 우리가 어쨋든 DI를 위한 container를 만드는거잖아요? 그리고 우리가 어떤 변수에 객체를 외부에서 주입할때 protocol타입으로 변수를 선언하고 거기에 특정 객체를 넣어주니까
protocol타입을 key로 가지고 protocol타입의 객체를 container에 넣어놓으면 나중에 어떤 속성을 프로토콜 타입으로 선언해놓은 상태에서 꺼낸다면 그 프로토콜 타입의 객체가 나오니까 DI를 위해서는 이런 방식으로 구현하는것이 좋겠다고 생각했습니다
/// 읽기(Read)
func resolve<T>(type: T.Type) -> T {
let key = "\(type)"
return services[key] as! T
}
/// 변수의 타입이 지정되어있으면 알아서 key로만들어서 resolve해줌
/// property wrapped를 사용하기 위한 메서드
func resolve<T>() -> T {
let key = "\(T.self)"
return services[key] as! T
}
Container의 값을 가져오는 메서드는 두가지로 구성을 했습니다 하나는 직접 타입을 넣어줘서(타입이 key가 되니까요) 객체를 가져오는 방식과 저장속성의 타입이 명시되어있는 상태에서 resolve메서드만 호출한다면 타입자체가 key가 되어서(타입예측이라고 하는거같아요) 객체를 꺼내주는 방식의 메서드 입니다
위에 메서드는 이해가 되시겠지만 아래메서드는 약간 헷갈릴수있어서 예시를 하나 가져왔습니다
예를 들어서 Animal이라는 프로토콜 타입으로 Cat()이라는 객체를 저장해놓은 상태라고 해보겠습니다
/// 1번방식
let returnValue = container.resolve(type: Animal)
/// returnValue에는 Cat()이 들어감
/// 2번방식
let returnValue: Animal = container.resolve()
/// resolve가 return해야할 generic T가 Animal이라는걸 알고 Animal을 key로만들어 Cat을 return해줌
/// returnValue에는 Cat()이 들어감
그래서 전체적인 Container객체는 이렇게 생겼습니다
final class LHDIContainer: LHDIContainerProcotol {
static let shared = LHDIContainer()
private init() {}
private var services: [String: AnyObject] = [:]
func register<T>(type: T.Type, component: AnyObject) {
let key = "\(type)"
services[key] = component
}
func resolve<T>(type: T.Type) -> T {
let key = "\(type)"
return services[key] as! T
}
func resolve<T>() -> T {
let key = "\(T.self)"
return services[key] as! T
}
}
그러면 실제로 사용해보겠습니다
우선 우리는 DI를 해야할 객체를 모두 등록을 해주겠습니다
당연히 key는 protocol이 될거고(DI에서의 추상체) value는 해당 protocol을 채택하고 있는 객체를 return해주게 됩니다
AppDelegate에서 사용해야할 DI객체를 모두 register해줬습니다
각 객체를 register할때 이미 등록된 객체가 필요하다면 resolve해서 객체 주입을 해주고 등록을 해줍니다 그리고 마지막 레이어인 ViewController에서는 외부에서 객체가 주입된 service객체를 하나만 넣어주고 등록을 해주면 됩니다
이전 코드에서는 SplashViewController객체를 하나 생성하기 위해서는 외부에서 3개를 넣어줘야했습니다 MyPageViewController의 경우엔 뷰컨트롤러 객체하나의 생성을위해 5개의 객체를 생성해야했습니다
이전에도 이야기햇지만 한번정도는 그러려니해도 15개의 뷰컨에서 이러려고하면 정말 힘이 많이 듭니다...
그리고 이후에 프로젝트를 하면서 뷰컨이 계속 생긴다면...? 생길때마다 만드는것도 일이지만 나중에 뷰컨이 30개가 되고나서 네트워크 구조가 바뀌는 바람에 이 모든 뷰컨에서 주입객체를 바꿔야하는 일이생긴다면...
정말 상상만 해도 끔찍합니다...
하지만 Container를 사용해서 외부에서 객체가 주입된 DI가 된 ViewController를 꺼내서 쓰기만 한다면 정말 간결한 코드가 됩니다
LHDIContainer.shared.resolve(type: SplashViewController.self)
외부에서 필요한 모든 객체가 주입된 SplashViewController를 이렇게 간단한 코드로 생성해낼수있습니다
swift에서 대표적인 DI 라이브러리라고 하면 Swinject가 먼저 떠오르시는 분들이 많을거같아요
제가 아직 실력이 부족해서 라이브러리의 코드를 전부 이해하진 못했지만 이렇게 저렇게 보기도 해보고 구글링을 열심히 해보니 Swinject라는 라이브러리도 이런식으로 DI를 관리할 수있다고 합니다
그러던 와중에 조금 신선한 글을 보게되었는데요
https://medium.com/@juliusfischer/dependency-injection-in-ios-with-swinject-f5ffb019006d
swinject는 DI를 위한 tool이 아니다
라는 주장이 담긴 글이었습니다
Swinject는 DI가 아닌 단순한 service locator이다라는 이야기를 하는데 번역기를 돌려보면 이런 문단이 있습니다
Swinject는 의존성 주입 프레임워크가 아닙니다. 충격을 줘서 미안합니다. Swinject는 단순한 서비스 로케이터일 뿐 실제 종속성 주입 프레임워크는 없으며 컴파일 시간 동안 이미 종속성을 주입합니다. 당신이 어떻게 생각하고 있는지 알고 있어요. 속았다고 느끼시는군요. 하지만 Swinject는 일반적으로 실제 DI 프레임워크와 유사한 작업을 수행합니다.
Service locator는 누군가가 필요로 하는 객체를 제공해주는 역할을 수행하는 객체라고 합니다 swinject나 우리가 지금까지 만들었던 container객체 모두 사실은 단순히 어떤 객체를 register하고 resolve하는 역할을 합니다
하지만 우리는 이러한 역할을 하는 도구를 DI를 위해 사용하려하는겁니다
swinject를 사용한 모든 프로젝트가 DI를 했다고 볼 수 없는 이유는 swinject가 정의만 놓고 보면 Service Locator에 가깝기때문입니다
그래서 swinject를 사용한 프로젝트라고 하더라도 이 라이브러리를 DI를 위해 사용했는지를 명확하게 확인해야합니다 단순히 reigster하고 resolve만 해서 사용한 경우는 DI를 위해 활용했다고 보기 어려울 수 있다고 생각합니다
단순히 register하고 resolve한다라는 말의 의미는
어떤 객체를 객체자체의타입(클래스)으로 register하고 객체자체의타입으로 resolve한다면 객체를 객체자체의 타입으로 넣고 사용하려는것이기때문에 단순히 객체를 외부에서 주입해주는것에 불과하게됩니다
swift에서는 어떤 객체를 단순히 initalize를 통해 외부에서 주입시켜주는 행위를 DI라고 하지 않습니다 주입받는 객체와 주입하는 객체가 변하기 어려운, 다시말해서 추상화된 객체에 의존성을 가지고있어야합니다 좀더 쉬운말로 이야기하면 프로토콜에 의존하고 있는 상태에서 외부에서 객체를 주입해줘야 DI라고 할 수 있습니다(
swift에서는요)
그러면 어떻게 이 도구들을 사용해야 DI로써 활용할 수 있을까요?
제생각에는 프로토콜과의 연관성을 가지고 register하고 init을 통해 외부에서 해당 객체를 주입하는 과정이 있어야만 DI로 활용한 swinject프로젝트라고 이야기할 수 있다고 생각합니다
/// 쓰기(Write)
func register<T>(type: T.Type, component: AnyObject) {
let key = "\(type)"
services[key] = component
}
register에 객체를 AnyObject로 설정해둔 이유가 Container를 단순 service locator가 아닌 DI의 도구로 사용하고 싶었기 때문입니다
그렇기 때문에 어떤 객체를 프로토콜 타입으로 받기위해서는 AnyObject타입으로 dictionary에 들어가야했습니다(AnyObject자체가 클래스타입임을 뜻하니까요)
이렇게 Container를 사용하는 방식이 큰 틀에서는 swinject랑 다르지 않고 실제로 swinject코드를봐도 register를하게되면 dictionary에 저장하는 방식이라는걸 알 수 있습니다
지금부터는 custom한 container를 조금더 개선시켰던 과정에 대해서 말씀드리겠습니다
swift 5.몇 버전에서부터 property wrapped라는 새로운 타입이 도입되었습니다 사실 property wrapped의 개념에 대해서 소개하는건 이번 포스팅의 주제와 맞지않을거같아 자세한 내용은 생략하고 간단하게 소개만 하고 넘어가겠습니다
프로퍼티를 감싸 특별한 타입으로 만들어주는 도구
이렇게 표현할 수있습니다 주로 어떤 특정한 로직을 여러 곳에서 적용시켜야할때 같은로직들을 매번 명시해주는것이 아니라 새로운 타입으로 만들어서 한번의 로직 명시로 여러곳에서 사용할수있게해줍니다
(지금부터 말씀드릴 property wrapped는 어려운내용은 아니지만 헷갈리신다면 공부하고 오시는걸 추천드립니다!)
제가생각했을때 지금 container에서 중복되는 로직은 어떤 변수의 타입이 선언되어있는 경우에 resolve를 하는 경우였습니다
class ViewController: UIViewController {
let service: ServiceProtocol
init(service: ServiceProtocol) {
self.service = service
}
}
보통 DI를 위해서 객체에 변수를 선언해주고 어떤 타입인지를 명시해줍니다 그리고 initalize에서 비로소 해당 타입의 객체를 주입받게됩니다
결국 DI를 위한 모든 객체는 저렇게 타입으로 선언해놓고 init에 맞는 타입의 객체를 외부에서 주입시켜줘야한다는 로직이 매번 반복되고 그 반복되는 로직을 반복해서 적어줘야합니다
그래서 property wrapped를 활용해 새로운 타입을 만들어줬습니다
@propertyWrapper
class DependencyPropertyWrapper<T> {
var wrappedValue: T
init() {
self.wrappedValue = LHDIContainer.shared.resolve()
}
}
이렇게 선언을 하면 DependencyPropertyWrapper라는 PropertyWrapper는 생성되는 시점에 자기자신의 타입 T를 key로하는 객체를 container에서 가져와서 주입시켜주게됩니다
그래서 이 property wrapper를 실제로 적용해보면
final class AuthMyPageServiceWrapper: AuthServiceProtocol, MyPageServiceProtocol {
@DependencyPropertyWrapper private var authAPIService: AuthAPIProtocol
@DependencyPropertyWrapper private var mypageAPIService: MyPageAPIProtocol
// init(authAPIService: AuthAPIProtocol, mypageAPIService: MyPageAPIProtocol) {
// self.authAPIService = authAPIService
// self.mypageAPIService = mypageAPIService
// }
이렇게 initalize없이도 외부에서 각 변수의 프로토콜 타입에 맞는 객체를 주입받을 수 있게됩니다
그리고 initalize를 통해서 객체를 생성해도 되지 않으니까 코드의 모양도 약간은 달라집니다
private func registerDIContainer() {
let container = LHDIContainer.shared
container.register(type: Requestable.self, component: APIService())
container.register(type: AuthAPIProtocol.self, component: AuthAPI())
container.register(type: MyPageAPIProtocol.self, component: MyPageAPI())
container.register(type: MyPageServiceProtocol.self, component: AuthMyPageServiceWrapper())
container.register(type: AuthServiceProtocol.self, component: AuthMyPageServiceWrapper())
///ViewController
container.register(type: SplashViewController.self, component: SplashViewController())
container.register(type: MyPageViewController.self, component: MyPageViewController())
}
secene delegate에서 register하는 객체의 initalize에 다른 객체를 넣어줄 필요가 없어집니다
왜냐면 각 객체가 property wrapper를 들고있고 객체가 생성되면 기본적으로 저장속성이 초기화가 되기때무네 property wrapper의 init 메서드가 불리고 매번 저장속성의 protocl을 key로하는 resolve 메서드가 실행되 initalize에 객체를넣어주지 않고도 객체를 주입받을 수 있게됩니다
이런식으로 사용하면 기존처럼 하나하나 객체를 매번생성해서 주입시켜야하는 방식에 비해 훨씬 간결한 방식으로 DI를 적용할 수 있습니다
그러면 어떤 분은 이렇게 말씀하실수도 있습니다
이렇게 container객체를 만들어서 사용하면 굳이 swinject를 사용할 필요가 없는거 아닐까요?
그래서 이제부턴 제가 container를 만들면서 느꼈던 점과 시도했지만 실패했던 부분을 말씀드려보겠습니다
코드를 작성하다 보니 문득 이런생각이 들었습니다
결국 perperty wrapper가 되는 프로토콜이 정해져있네?
그렇다고 하면 모든 프로토콜이 property wrapper가 될 수 있다면 우리는 아무래도 인간이다보니 헷갈려서 잘못 채택할 수있다고 생각을 했습니다
property wrapper를 선언한 변수는 이렇게 생겼었는데
@DependencyPropertyWrapper private var authAPIService: AuthAPIProtocol
@DependencyPropertyWrapper private var mypageAPIService: MyPageAPIProtocol
결국 property wrapper가 된 변수의 타입은 protocol이었단 말이죠?
그래서 모든 프로토콜앞에 @DependencyPropertyWrapper
를 붙일수있다는건 결국 init되는 시점에 타입자체를 가지고 value를 return 받는데 애초에 그렇게 하면 실수로 register안한 protocol타입에 달수도 있는거고 그러면 문제가 발생할테니(dictionary에서 value를 뽑아서 force unwrapping해줬음) 제약조건을 달아서 문제가 발생할 가능성을 없애고 싶었습니다
그래서 처음에 사용했던 방식이
protocol Dependency {}
@propertyWrapper
class DependencyPropertyWrapper<T: Dependency> {
var wrappedValue: T
init() {
self.wrappedValue = LHDIContainer.shared.resolve()
}
}
property wrapper에 있는 generic에 Dependency라는 제약조건을 다는것이었습니다
Dependency를 채택한 프로토콜타입(T)만 Property wrapper가 될 수있겠군...
하지만 이렇게 하고
protocol AuthAPIProtocol: Dependency {
func reissueToken(token: Token) async throws -> Token?
func login(type: LoginType, kakaoToken: String) async throws
func signUp(type: LoginType, onboardingModel: UserOnboardingModel) async throws
@discardableResult func logout(token: UserDefaultToken) async throws -> String?
func resignUser() async throws
}
이런식으로 모든 외부에서 주입해줘야하는 객체가 바라보고있는 프로토콜에 Dependency를 채택해줬는데 에러가 발생합니다
분명히 머리속에서 작동하는 방식으로는 논리적으로 틀린부분이 없는데 컴파일이 에러를 던져버립니다...
우선 에러를 천천히 보면 처음부터 이상한녀석이 나옵니다
변수는 분명히 Requestable인데 any Requestable라고 합니다
이부분에 대한 저의 생각을 정리해보면 구체적인타입(애플에선 콘크리트타입이라고 하는거같습니다)를 타입으로 가지는 변수가 아니라 추상객체를 타입으로 가지는 변수이기때문에 저 프로토콜을 채택하는 객체가 어떤 객체가 올지 모릅니다(Animal프로토콜 타입의 변수라면 Cat이 올지 Dog가 올지 모른다는겁니다) 그런 의미에서 Any라는 키워드가 붙은게 아닐까라는생각을 해봤습니다
그리고 분명히 Requestable은 Dependency라는 프로토콜을 채택하고 있는데 Dependency를 conform할 수 없다고 합니다
사실 이부분이 가장 애매한 부분이었는데 여러 글들을 본 결과
변수자체가 콘크리트타입
으로 선언되어있지 않아서 에러가 발생한게 아닐까라는 나름의 결론을 내리게 되었습니다
제네릭 T가 Dependency를 채택해야한다는 로직에서 T가 단순 타입이라고 생각해서 추상화된 타입도 범주안에 들어간다고 생각했는데 property wrapper입장에선 Dependency프로토콜을 채택하는 콘크리트타입을 받았다고 생각했는데 실제로 들어온 객체(클래스)는 Dependency를 채택하고 있지 않으니 확인할수없다고 에러를 던지는게 아닐까 라는 생각을 해봤습니다(실제로 dependency는 프로토콜이 채택하고 있으니까요!)
그렇게 되면 결국 property wrapper는 프로토콜타입으로 객체를 받기때문에 property wrapper의 generic에는 제한조건을 달 수가 없게됩니다
결국 그러면 우리가 DI로 사용하려는 프로토콜자체를 Dependency를 채택한 객체만 채택하면 어떨까 라는 아이디어가 떠올랐습니다
뭔가 찜찜한 방법이고 좋은 방법이 아니라는 생각이 뼛속깊이 느껴지지만 적어도 적용하려했던 제한조건은 걸수있는 방법이긴합니다...(해보고 너무 아닌거같아서 지웠습니다)
결국 Dependency라는 제약조건을 걸기위해서는 콘크리트타입인 객체에 걸어줘야한다는 사실을 알게되었습니다
저희가 DI Container를 싱글톤으로 만들어서 사용하고 있는데요
같이 프로젝트를 하는 팀원이 이런 이야기를 합니다
dictionary에 register된 순간부터 쭉 객체에 대한 강한참조를 가지고 있는거 아니야?
처음에는 그럴수도 있긴하네 라고 생각을 하고 구글링을 해보니 실제로도 이런 이유때문에 들어가는 변수를 weak으로 가지는 객체로 한번 감싸는 방식으로 강한참조 문제를 해결하는 포스팅을 심심치 않게 볼 수 있었습니다
강한참조가 생기면 언제든지 순환참조가 발생할 수 도 있는거고 그에따른 메모리 leak이 발생하는 우리가 예상치못한 side effect가 발생할 수있기 때문에 한번 해결을 해보고자 우리가 dictionary에 넣으려는 변수를 weak 저장속성으로 가지고 있는 객체를 한번 만들어봤습니다
struct Weak {
weak var value: Any?
init(value: Any) {
self.value = value
}
}
당연히 value로 들어가는 객체가 Any여서 weak로 선언을 해줬는데
처음에는 AnyObject를 생각하지 못해서 Any로 선언해줬었다가 추후에 AnyObect로 변경했습니다
이번에도 친절한 엑코가 에러를 던져줍니다
읽어보면 weak는 반드시 non class bound에서 apply되면 안된다고 하네요. 결국 이 에러는 class를 따르지 않는 프로토콜을 weak 변수로 선언해 사용할 때 발생하는 에러입니다
이거에 대해서도 한번 고민을 해봤는데요 결국 weak라는 키워드는 Referece Count를 관리해주는 녀석입니다 이걸 이용해서 RC를 관리해 순환참조를 막는데 사용되는 녀석입니다. 하지만 any는 reference type도 value type도 될수있는 녀석이기에 타입에 민감한(타입마다 해야할일이 명확한) swift에서는 Any가 value type이 될가능성이 있기에 weak를 선언하지 못하게 한게 아닐까요?
그래서 weak를 선언하려면 any가 아니라 referenct type이어야해서 문득 떠오른 키워드가 AnyObject였습니다
위에서 AnyObject를 사용하면 안되는 이유에 대해서 설명을 드렸는데 이 이유를 알기전에 시도했던 방식입니다, 실제로도 weak자체로 돌아가지만 AnyObject가 프로토콜 타입으로 타입캐스팅이 안되서 문제가 발생합니다)
그래서 우선 AnyObject로 바꿔서 Weak한 dictionary를 만들었습니다
그런데 자꾸 register를 한 친구를 dictionary에서 꺼내면 nil이 반환됩니다
대체 왜일까를 고민하던와중 결국 지금 상황에서 container에서 객체를 꺼내는 녀석은 property wrapper라는사실을 알게되었습니다
property wrapper
와 init() {print("객체생성")}
에 bp를 찍어보니 init보다 property wrapper가 먼저 불려서 객체를 먼저 property wrapper가 가지게 됩니다
그렇게 되면 property wrapper를 init으로 부터 생성되는 객체가 미리 참조하고 있어야 referece count가 증가해 메모리에서 자동으로 해제되지 않는데 객체가 생성되기도 전에 property wrapper에 의해 등록된 객체가 먼저 resolve되어 변수에 들어가고 심지어 그 객체가 weak로 선언되어서 rc가 증가하지 않아서 바로nil로 메모리에서 해제되는게 아닌가 라는 생각이 들었습니다
실제로 register되자마자 바로 dictionary를 찍어보면 weak로 선언된 value가 nil이되어있음을 확인 할 수 있습니다
실제로 weak때문에 rc가 count가 되지 않아서인지 정확하게 알수없지만 여러가지 증거들을 토대로 내린 나름의 결론정도라고 생각해주시면 감사하겠습니다 (_ _)
제 생각엔 이런 실행의 결과로 인해 property wrapper로 인해 생성된 객체가 바로 메모리에서 할당해제가 된게 아닐까라는 조심스러운 의견을 내어봅니다
결론부터 말씀드리면 dictionary의 강한참조 문제를 해결하지 못했고 dependency 프로토콜을 이용한 제약조건도 상상처럼 쉽게 되지 않았습니다
그렇게 삽질을 하고 나니까 이래서 swinject를 쓰는건가?
라는 생각이 문득 들었습니다. 정확히는 모르겠지만 swinject에서 dictionary가 weak으로 선언되어있는 부분을 보니 swinject를 만든 개발자분도 이런 dictionary의 string 참조를 고려한게 아닐까라는 생각도 들었습니다
swinject내부의 정말 큰틀의 로직은 이러할거다라는 많은 글들을 보고 직접 구현해본 나사가 많이 빠진 custom container를 구현했고 어떻게 개선했고 개선하는 과정에서 어떤 문제점을 발견했고 어떻게 원인을 분석했는지에 대한 이야기를 구구절절 나열해봤습니다
라이브러리를 단순히 사용하기보다는 어떤 원리로 이 라이브러리가 구성되어있고 실제로 내부 로직을 라이브러리없이 구현해보고 사용하다보니 이런이런점을 고려해야하고 이런 문제가 있다라는 생각이들고 고민을 해본뒤에야 그 문제들을 해결해주는 라이브러리를 선택해서 사용하는 일련의 과정이 저에게있어서는 기술스택을 쌓아가는 유의미한 과정이라고 생각을 했습니다
비록 삽질일기지만 이렇게 해본 경험을 바탕으로 swinject를 실제로 사용해보거나 간단한 프로젝트(제가 발견한 문제점이 발생하지 않는 정도)에서는 굳이 라이브러리를 사용하지 않더라도 DI를 위한 container를 적용해보면 좋을거같습니다
긴글이었고 아마 정확하지 않은 부분도 많을거같습니다
제 논리에 문제가 있거나 수정해야할 부분 제가 잘못알고있는 부분들을 꼭 알려주시면 감사하겠습니다!
그럼 20000!
유익하게 잘 읽었습니다.