WeaveDI 2.0 설계 철학과 결정의 순간들 - 완전한 이야기

Ios_Roy·2025년 9월 15일

라이브러리 개발

목록 보기
1/25
post-thumbnail

한 줄 요약: Swift가 async/await와 Actor로 진화했듯, DI도 새 패러다임에 맞게 재설계되어야 한다. WeaveDI 2.0은 그 응답입니다 — 더 빠르고, 더 안전하며, 더 즐겁습니다.

🌅 프롤로그 — 왜 또 다른 DI 라이브러리인가?

2024년 당시 생태계에는 Swinject, Needle, Factory 등 훌륭한 DI 라이브러리들이 이미 있었습니다. 그런데도 새로운 라이브러리를 만들기로 했습니다.

실제 개발하면서 겪은 문제들:

  • 매번 의존성 주입할 때마다 10줄 이상 작성해야 함
  • ! 연산자 때문에 예상치 못한 크래시 발생
  • 앱이 시작될 때 눈에 띄게 느려짐
  • async/await 사용할 때 뭔가 어색한 느낌

이유는 간단했습니다. 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의 한계를 마주하다 → 2.0의 출발

실제 프로덕션 앱에 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)!
    }
}

2.0의 핵심 목표

  1. Swift Concurrency First — Actor/async를 1급으로 대우
  2. DX 혁신 — 보일러플레이트 최소화, 의도 기반 API
  3. 성능 최우선 — Actor hop 최적화, zero-cost abstraction 추구
  4. 예측 가능성 — "No Surprises", 타입 안전성 강화
  5. 미래 대비 — Swift 6, Macro, Typed throws, borrowing/consuming

📚 Chapter 1 — Swift Concurrency와의 만남

결정 1: Actor를 First-Class Citizen으로

기존 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와 함께 쓸 때 뭔가 부자연스러운 느낌

결정 2: Zero‑Cost Abstraction

호출 오버헤드와 불필요한 동적 분기 제거를 위해 인라이닝/메모리 레이아웃 최적화를 적극 활용했습니다.

@inlinable
public func resolve<T>(_ type: T.Type) -> T? { /* inline-friendly path */ }

@frozen
public struct ContainerKey {
    let identifier: ObjectIdentifier
    let scope: Scope
}

📐 Chapter 2 — API 설계, "간단한 것은 간단하게"

결정 3: 점진적 학습 곡선 (Progressive Disclosure)

"초보자도 쉽게, 고수도 만족스럽게"

  • 🟢 초보자: @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만 알면 되고, 나중에 필요할 때 고급 기능을 하나씩 배워갈 수 있어요.

결정 4: 친절한 에러 처리 — "빨리 실패하되, 친절하게"

에러 상황에 따라 다르게 대응합니다:

  • 개발할 때 꼭 필요한 것이 없으면 → 즉시 크래시 + 해결 방법 알려줌
  • 있어도 되고 없어도 되는 것 → nil 반환해서 개발자가 처리
  • 복구 가능한 상황 → 기본값이나 대체재 사용
// 이런 친절한 에러 메시지를 보여줍니다
🔴 DI Resolution Failed

Type: UserService
Reason: 등록되지 않은 타입입니다

💡 해결 방법:
'UserServiceImpl'을 등록하려고 하셨나요?
아니면 부트스트랩에 이 코드를 추가해보세요:
container.register(UserService.self) { UserServiceImpl() }

📍 호출 경로:
- MyViewController.init()
- MyViewModel.init()

📚 자세한 문서: https://github.com/Roy-wonji/WeaveDI

이렇게 하면 에러가 발생해도 "아, 이렇게 하면 되겠구나" 하고 바로 해결할 수 있어요.

🏗 Chapter 3 — 아키텍처 패턴의 진화

결정 5: BootstrapCoordinator (Actor) 도입

여러 모듈의 초기화 순서/순환 의존성을 중앙에서 관리합니다.

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)
    }
}

결정 6: Module System

대규모 앱에서 논리적 그룹화와 독립 테스트가 필수입니다.

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() } }
        }
    }
}

🔬 Chapter 4 — 성능 최적화의 깊은 이야기

결정 7: 메모리/스코프 전략

  • 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()
    }
}

결정 8: 2‑Tier 캐싱 (L1/L2)

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개의 서비스를 준비해야 했는데, 기존 방식보다 체감상 훨씬 빨라진 것을 확인했습니다. 특히 앱을 켰을 때 "뭔가 더 빠르네?"라는 느낌을 받을 정도로 개선되었습니다.

🎨 Chapter 5 — Developer Experience(DX)의 집착

결정 9: 디버깅을 "보는" 경험으로

// Graphviz 내보내기 예시
digraph Dependencies {
  rankdir=LR;
  node [shape=box];
  "ViewController" -> "UserService";
  "UserService"   -> "Repository";
  "Repository"    -> "Database";
  "Repository"    -> "NetworkClient";
}
  • Visual Dependency Graph: 복잡한 의존성 한눈에 파악
  • 실시간 모니터링(DEBUG 빌드): SwiftUI 디버그 뷰
  • 스마트 에러 메시지: 오타/유사 타입 추천, 해결책 제시

결정 10: 자동완성 최적화

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 }
}

🔐 Chapter 6 — 안전성과 신뢰성

결정 11: Thread Safety 보장

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
    }
}

결정 12: 타입 안전성 극대화

  • Phantom type으로 스코프 구분
  • 제네릭 제약으로 "해결 가능한 의존성"만 등록 허용
  • Result Builder로 등록 실수 컴파일 단계에서 차단
@resultBuilder
struct DependencyBuilder {
    static func buildBlock<T>(_ components: Registration<T>...) -> [Registration<T>] {
        components
    }
}

🌍 Chapter 7 — 생태계 통합

SwiftUI 통합

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 / TCA / Plugin

// 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)
}

🧰 UnifiedDI — 하나의 진입점

여러 프로젝트 피드백: "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

수치는 프로젝트/환경에 따라 달라질 수 있습니다.

🧭 설계 원칙 — 우리가 끝까지 붙잡은 다섯 가지

  1. Performance First — 성능은 기능이다.
  2. Developer Happiness — DX는 UX만큼 중요하다.
  3. No Surprises — 예측 가능한 동작, 숨은 마법은 배제한다.
  4. Progressive Complexity — 단순한 것은 단순하게, 복잡한 것은 가능하게.
  5. Future‑Proof — 오늘의 코드가 내일도 동작하게.

🗺 로드맵 — Swift 6 & Macro 시대

  • Typed throws 지원
  • Borrowing/Consuming 파라미터 최적화
  • Macro 기반 @Injectable 코드 생성
@attached(member) @attached(conformance)
macro Injectable() = #externalMacro(module: "WeaveDIMacros", type: "InjectableMacro")

@Injectable
final class UserService {
    // DI용 보일러플레이트 자동 생성 (미래 지향)
}

🧪 5분만에 시작하기

설치부터 사용까지 정말 간단합니다. 한번 따라해보세요!

1단계: 설치

// Package.swift에 추가
dependencies: [
    .package(url: "https://github.com/Roy-wonji/WeaveDI", from: "2.0.0")
]

2단계: 설정 (단 5줄!)

// AppDelegate 또는 App.swift에서
await WeaveDI.Container.bootstrap { c in
    c.register(UserRepository.self) { UserRepositoryImpl() }
    c.register(UserService.self) { UserService(repo: c.resolveRequired(UserRepository.self)) }
}

3단계: 사용하기

// 이게 전부입니다!
final class MyViewModel {
    @Inject var service: UserService
    
    func loadUser() async {
        let user = await service.getCurrentUser()
        // 바로 사용 가능!
    }
}

SwiftUI에서도 당연히 됩니다

struct ContentView: View {
    @InjectedObject var viewModel: UserViewModel
    
    var body: some View {
        Text("Hello, \(viewModel.userName)!")
    }
}

정말 이게 전부예요! 복잡한 설정이나 추가 코드가 필요 없습니다.

💬 마치며 — 개발자의 진솔한 이야기

이 라이브러리를 만들면서 가장 고민했던 것은 "정말 필요한 기능인가?"였습니다.

단순히 "있으면 좋겠다"가 아니라 "없으면 개발이 힘들다" 수준의 기능들만 남겼습니다. 그 결과 개발자가 자연스럽게 올바른 방향으로 코딩할 수 있는 라이브러리가 되었다고 생각합니다.

앞으로의 계획

DiContainer 2.0은 끝이 아니라 시작입니다. Swift가 발전하는 만큼 함께 발전할 예정이에요:

  • Swift 6 완전 호환
  • Macro를 활용한 더 간단한 사용법
  • 웹 기반 의존성 시각화 도구
  • 더 스마트한 에러 메시지

Swift 개발자들의 일상이 조금 더 즐거워졌으면 좋겠습니다. 🚀

함께 만들어가요!

혼자 만드는 것보다 함께 만드는 것이 더 좋은 결과를 낳는다고 믿습니다. 여러분의 피드백과 아이디어를 기다리고 있어요!

🔗 더 알아보기 & 함께하기

📚 자료

💬 커뮤니티

  • 이슈 제보: 버그를 찾으셨나요? 언제든 알려주세요
  • 기능 제안: "이런 기능 있으면 좋겠어요!" 환영합니다
  • 경험 공유: 실제 프로젝트 적용 후기나 노하우 공유해주세요

특히 Swift Concurrency 최적화 사례나 대규모 앱 적용 경험을 공유해주시면 다른 개발자들에게도 큰 도움이 될 것 같아요!

🎉 기여하기

코드 기여도 물론 좋지만, 이런 것들도 큰 도움이 됩니다:

  • 오타나 문서 개선
  • 예제 코드 추가
  • 사용 후기나 블로그 포스팅
  • 주변 개발자들에게 소개

함께 더 나은 Swift 생태계를 만들어가요! 🚀

profile
iOS 개발자 공부하는 Roy

0개의 댓글