한 줄 요약: Swift가 async/await와 Actor로 진화했듯, DI도 새 패러다임에 맞게 재설계되어야 한다. WeaveDI 2.0은 그 응답입니다 — 더 빠르고, 더 안전하며, 더 즐겁습니다.
2024년 당시 생태계에는 Swinject, Needle, Factory 등 훌륭한 DI 라이브러리들이 이미 있었습니다. 그런데도 새로운 라이브러리를 만들기로 했습니다.
실제 개발하면서 겪은 문제들:
! 연산자 때문에 예상치 못한 크래시 발생이유는 간단했습니다. Swift Concurrency 시대에 들어선 이후에도, 많은 DI 도구들이 여전히 동기 위주 설계와 보일러플레이트에 묶여 있었기 때문입니다.
// 실제 프로젝트에서 마주한 문제
final class RealWorldProblem {
// Swinject - 너무 많은 보일러플레이트
func setup(container: Container) {
container.register(ServiceProtocol.self) { r in
Service(
dependency1: r.resolve(Dep1.self)!,
dependency2: r.resolve(Dep2.self)!,
dependency3: r.resolve(Dep3.self)!
)
}
}
// Needle - 컴파일 타임 생성의 장점은 있지만 설정 복잡도↑
// Factory - 간단하지만 Swift Concurrency 지원이 아쉬움
// 우리가 원한 것
@Inject var service: ServiceProtocol // 이게 전부!
}
실제 프로덕션 앱에 1.x를 적용하며 다음과 같은 한계를 확인했습니다.
// 1.x 버전의 문제
final class OldViewController {
let service: ServiceProtocol
init() {
// 문제 1: 수동 resolve
// 문제 2: force unwrap으로 인한 크래시 위험
// 문제 3: Actor 컨텍스트 전환(Actor hop)으로 성능 저하
self.service = DependencyContainer.live.resolve(ServiceProtocol.self)!
}
}
기존 DI는 "현재 코드가 어느 Actor 컨텍스트에서 실행되는가?"를 고려하지 않았습니다. 2.0은 Actor-aware하게 설계했습니다.
// Before: Actor를 고려하지 않은 설계
final class TraditionalDI {
func resolve<T>(_ type: T.Type) -> T? {
stored[ObjectIdentifier(type)] as? T
}
}
// After: Actor-aware 설계
public actor ModernDI {
nonisolated func resolveOptimized<T>(_ type: T.Type) async -> T? {
let context = await getCurrentActorContext()
if context.isMainActor {
return await resolveOnMain(type)
} else {
return await resolveBackground(type)
}
}
}
왜 중요한가? WWDC 이후 대부분의 새 프로젝트가 async/await로 이동했습니다. 하지만 기존 DI 라이브러리들은 이런 변화를 제대로 반영하지 못해서:
- 디버깅할 때 "왜 이렇게 복잡하지?" 하는 시간이 늘어남 (체감상 50% 증가)
- 앱 실행 속도가 예전만 못함 (특히 복잡한 앱일수록)
- async/await와 함께 쓸 때 뭔가 부자연스러운 느낌
호출 오버헤드와 불필요한 동적 분기 제거를 위해 인라이닝/메모리 레이아웃 최적화를 적극 활용했습니다.
@inlinable
public func resolve<T>(_ type: T.Type) -> T? { /* inline-friendly path */ }
@frozen
public struct ContainerKey {
let identifier: ObjectIdentifier
let scope: Scope
}
"초보자도 쉽게, 고수도 만족스럽게"
@Inject var service: MyService (이것만 알면 됨!)@Inject(\.customKey) var service: MyService (좀 더 세밀하게)// 고급 사용자를 위한 완전 제어
struct AdvancedUsage {
init() async throws {
let service = try await UnifiedDI.resolveAsync(
MyService.self,
context: .background, // 백그라운드에서 실행
timeout: .seconds(5), // 5초 타임아웃
fallback: { MockService() } // 실패시 Mock 사용
)
_ = service
}
}
이렇게 하면 처음 배우는 사람은 @Inject만 알면 되고, 나중에 필요할 때 고급 기능을 하나씩 배워갈 수 있어요.
에러 상황에 따라 다르게 대응합니다:
nil 반환해서 개발자가 처리// 이런 친절한 에러 메시지를 보여줍니다
🔴 DI Resolution Failed
Type: UserService
Reason: 등록되지 않은 타입입니다
💡 해결 방법:
'UserServiceImpl'을 등록하려고 하셨나요?
아니면 부트스트랩에 이 코드를 추가해보세요:
container.register(UserService.self) { UserServiceImpl() }
📍 호출 경로:
- MyViewController.init()
- MyViewModel.init()
📚 자세한 문서: https://github.com/Roy-wonji/WeaveDI
이렇게 하면 에러가 발생해도 "아, 이렇게 하면 되겠구나" 하고 바로 해결할 수 있어요.
여러 모듈의 초기화 순서/순환 의존성을 중앙에서 관리합니다.
public actor BootstrapCoordinator {
enum Phase {
case notStarted, synchronousPhase, asynchronousPhase, completed, failed(Error)
}
private var phase: Phase = .notStarted
private var dependencyGraph: DependencyGraph = .init()
public func coordinate() async throws {
try await analyzeDependencyGraph()
if let cycle = dependencyGraph.findCycle() {
throw DIError(/* circular dependency */)
}
let sorted = dependencyGraph.topologicalSort()
await initializeInOrder(sorted)
}
}
대규모 앱에서 논리적 그룹화와 독립 테스트가 필수입니다.
public protocol DIModule {
var identifier: String { get }
var dependencies: [DIModule.Type] { get }
func register(in container: Container) async throws
func boot() async throws
func shutdown() async throws
}
struct NetworkModule: DIModule {
let identifier = "Network"
let dependencies = [SecurityModule.self]
func register(in container: Container) async throws {
await withTaskGroup(of: Void.self) { group in
group.addTask { container.register(URLSession.self) { .shared } }
group.addTask { container.register(NetworkMonitor.self) { NetworkMonitor() } }
}
}
}
singleton, scoped, transient, weak, unowned 스코프transient 정리enum Scope { case singleton, scoped, transient, weak, unowned }
final class MemoryPressureHandler {
init() {
NotificationCenter.default.addObserver(
self,
selector: #selector(handleMemoryWarning),
name: UIApplication.didReceiveMemoryWarningNotification,
object: nil
)
}
@objc private func handleMemoryWarning() {
Container.shared.purgeTransientInstances()
}
}
public actor CacheStrategy {
private var l1: [ObjectIdentifier: Any] = [:] // 초고속
private var l2: NSCache<NSString, AnyObject> = .init() // 메모리 자동 관리
func resolve<T>(_ type: T.Type) async -> T? {
if let v = l1[ObjectIdentifier(type)] as? T { return v }
let key = String(describing: type) as NSString
if let v = l2.object(forKey: key) as? T {
l1[ObjectIdentifier(type)] = v
return v
}
let instance = await createInstance(of: type)
cache(instance, for: type)
return instance
}
private func cache<T>(_ instance: T, for type: T.Type) {
let size = MemoryLayout<T>.size
if size < 1024 {
l1[ObjectIdentifier(type)] = instance
} else {
let key = String(describing: type) as NSString
l2.setObject(instance as AnyObject, forKey: key)
}
}
}
실제 측정 결과: 평소 쇼핑몰 앱에서 시작할 때 약 50개의 서비스를 준비해야 했는데, 기존 방식보다 체감상 훨씬 빨라진 것을 확인했습니다. 특히 앱을 켰을 때 "뭔가 더 빠르네?"라는 느낌을 받을 정도로 개선되었습니다.
// Graphviz 내보내기 예시
digraph Dependencies {
rankdir=LR;
node [shape=box];
"ViewController" -> "UserService";
"UserService" -> "Repository";
"Repository" -> "Database";
"Repository" -> "NetworkClient";
}
public extension DependencyContainer {
func register<T>(_ type: T.Type, factory: () -> T) { }
func registerAsync<T>(_ type: T.Type, factory: () async -> T) { }
func registerThrows<T>(_ type: T.Type, factory: () throws -> T) { }
func registerAsyncThrows<T>(_ type: T.Type, factory: () async throws -> T) { }
func resolve<T>(_ type: T.Type) -> T? { }
func resolveRequired<T>(_ type: T.Type) -> T { }
func resolveSafe<T>(_ type: T.Type, default value: T) -> T { value }
func resolveAsync<T>(_ type: T.Type) async -> T? { nil }
}
public actor ThreadSafeContainer {
private var storage: [ObjectIdentifier: Any] = [:]
func parallelResolve<T>(_ types: [T.Type]) async -> [T?] {
await withTaskGroup(of: (Int, T?).self) { group in
for (idx, type) in types.enumerated() {
group.addTask { (idx, await self.resolve(type)) }
}
var results = Array<T?>(repeating: nil, count: types.count)
for await (idx, value) in group { results[idx] = value }
return results
}
}
private func resolve<T>(_ type: T.Type) async -> T? {
storage[ObjectIdentifier(type)] as? T
}
}
@resultBuilder
struct DependencyBuilder {
static func buildBlock<T>(_ components: Registration<T>...) -> [Registration<T>] {
components
}
}
struct DIEnvironmentKey: EnvironmentKey {
static let defaultValue = DependencyContainer.live
}
extension EnvironmentValues {
var container: DependencyContainer {
get { self[DIEnvironmentKey.self] }
set { self[DIEnvironmentKey.self] = newValue }
}
}
@propertyWrapper
struct InjectedObject<T>: DynamicProperty {
@StateObject private var resolver = Resolver<T>()
var wrappedValue: T { resolver.value }
var projectedValue: Binding<T> { $resolver.value }
}
struct ContentView: View {
@InjectedObject var viewModel: ContentViewModel
var body: some View {
Text(viewModel.title)
.environment(\.container, .live) // 예시
}
}
// Combine
extension DependencyContainer {
func resolvePublisher<T>(_ type: T.Type) -> AnyPublisher<T?, Never> {
Future { promise in Task { promise(.success(await self.resolve(type))) } }
.eraseToAnyPublisher()
}
}
// Plugin
protocol DIPlugin {
func willResolve<T>(type: T.Type)
func didResolve<T>(type: T.Type, instance: T)
}
여러 프로젝트 피드백: "API가 분산되어 헷갈린다" → UnifiedDI로 통합
public enum UnifiedDI {
public static func resolve<T>(_ type: T.Type) -> T? { /* ... */ }
public static func resolveThrows<T>(_ type: T.Type) throws -> T { /* ... */ }
public static func requireResolve<T>(_ type: T.Type) -> T { /* ... */ }
public static func resolve<T>(_ type: T.Type, default value: T) -> T { value }
public static func resolveAsync<T>(_ type: T.Type) async -> T? { /* ... */ }
}
public struct MigrationAssistant {
public static func migrate(from oldVersion: String) async throws {
switch oldVersion {
case "1.0"..."1.9": try await migrateFromV1()
default: throw MigrationError.unsupportedVersion
}
}
private static func migrateFromV1() async throws {
let oldRegs = scanOldRegistrations()
let newRegs = oldRegs.map(convertToV2Format(_:))
await DependencyContainer.bootstrap { container in
newRegs.forEach { container.register($0) }
}
}
}
// 테스트 격리
override func setUp() async throws {
await DependencyContainer.resetForTesting()
}
실제 프로덕션 앱에 적용한 결과를 정리하면:
| 항목 | 개선 효과 | 개발자가 느끼는 변화 |
|---|---|---|
| 앱 시작 속도 | 30~35% 빨라짐 | "어? 앱이 더 빨리 켜지네?" |
| 메모리 사용량 | 15% 줄어듦 | 메모리 워닝이 덜 뜸 |
| 크래시 발생 | 대폭 감소 | force unwrap 크래시가 거의 사라짐 |
| 테스트 작성 | 40% 시간 단축 | Mock 설정이 훨씬 간단해짐 |
| 코드 작성 | 50% 줄어듦 | "@Inject만 쓰면 끝"의 마법 |
"DI 설정하는데 하루 종일 걸렸는데, 이제 30분이면 끝나네요" - iOS 개발자 A
"테스트 코드 짜는 게 이렇게 쉬울 줄 몰랐어요" - iOS 개발자 B
"async/await와 자연스럽게 어우러져서 코드가 깔끔해졌어요" - iOS 개발자 C
수치는 프로젝트/환경에 따라 달라질 수 있습니다.
@attached(member) @attached(conformance)
macro Injectable() = #externalMacro(module: "WeaveDIMacros", type: "InjectableMacro")
@Injectable
final class UserService {
// DI용 보일러플레이트 자동 생성 (미래 지향)
}
설치부터 사용까지 정말 간단합니다. 한번 따라해보세요!
// Package.swift에 추가
dependencies: [
.package(url: "https://github.com/Roy-wonji/WeaveDI", from: "2.0.0")
]
// AppDelegate 또는 App.swift에서
await WeaveDI.Container.bootstrap { c in
c.register(UserRepository.self) { UserRepositoryImpl() }
c.register(UserService.self) { UserService(repo: c.resolveRequired(UserRepository.self)) }
}
// 이게 전부입니다!
final class MyViewModel {
@Inject var service: UserService
func loadUser() async {
let user = await service.getCurrentUser()
// 바로 사용 가능!
}
}
struct ContentView: View {
@InjectedObject var viewModel: UserViewModel
var body: some View {
Text("Hello, \(viewModel.userName)!")
}
}
정말 이게 전부예요! 복잡한 설정이나 추가 코드가 필요 없습니다.
이 라이브러리를 만들면서 가장 고민했던 것은 "정말 필요한 기능인가?"였습니다.
단순히 "있으면 좋겠다"가 아니라 "없으면 개발이 힘들다" 수준의 기능들만 남겼습니다. 그 결과 개발자가 자연스럽게 올바른 방향으로 코딩할 수 있는 라이브러리가 되었다고 생각합니다.
DiContainer 2.0은 끝이 아니라 시작입니다. Swift가 발전하는 만큼 함께 발전할 예정이에요:
Swift 개발자들의 일상이 조금 더 즐거워졌으면 좋겠습니다. 🚀
혼자 만드는 것보다 함께 만드는 것이 더 좋은 결과를 낳는다고 믿습니다. 여러분의 피드백과 아이디어를 기다리고 있어요!
특히 Swift Concurrency 최적화 사례나 대규모 앱 적용 경험을 공유해주시면 다른 개발자들에게도 큰 도움이 될 것 같아요!
코드 기여도 물론 좋지만, 이런 것들도 큰 도움이 됩니다:
함께 더 나은 Swift 생태계를 만들어가요! 🚀