WeaveDI : DiContainer 메모리 관리 전략 - Weak Reference와 순환 의존성 해결

Ios_Roy·2025년 9월 17일

라이브러리 개발

목록 보기
6/25
post-thumbnail

한 줄 요약: 메모리는 한정적입니다. DiContainer 2.0의 지능적 메모리
관리로 안전하고 효율적인 DI를 구현하세요.

안녕하세요! iOS 개발자 Roy입니다. 오늘은 DiContainer 2.0의 메모리 관리
전략
을 깊이 있게 다루되, 이해하기 쉽게 풀어보겠습니다.\
DI(의존성 주입)를 쓰다 보면 메모리 누수, 순환 의존성 같은 문제를
마주하게 되는데, 이를 어떻게 우아하게 해결할 수 있을까요?


🎯 왜 DI에서 메모리 관리가 중요한가?

실제 프로덕션 앱에서 발생했던 사례입니다:

// ❌ 문제가 있는 코드 – 메모리 누수 발생
class UserService {
    private let repository: UserRepository
    private let cacheManager: CacheManager
    
    init(repository: UserRepository, cacheManager: CacheManager) {
        self.repository = repository
        self.cacheManager = cacheManager
        
        // 강한 참조로 인한 순환 참조
        cacheManager.delegate = self
        repository.observer = self
    }
}

측정 결과: 위 패턴이 50개 서비스에 적용되자, 실행 30분 후 메모리가\
200MB → 350MB로 증가했습니다.


📊 DiContainer 2.0의 메모리 스코프 시스템

DiContainer 2.0은 5가지 스코프를 제공합니다. 올바른 스코프 선택은
메모리 안정성의 핵심입니다.

  1. Singleton -- 앱 생명주기 전체 유지
  2. Weak -- 약한 참조, 필요 없으면 자동 해제
  3. Unowned -- 생명주기 보장 시 안전하게 참조
  4. Scoped -- 특정 범위 내에서만 유지 (예: 로그인 세션)
  5. Transient -- 요청마다 새 인스턴스 생성

Singleton 예시

await DependencyContainer.bootstrap { container in
    container.register(DatabaseManager.self, scope: .singleton) {
        DatabaseManager()
    }
}
class DatabaseService {
    @Inject var database: DatabaseManager // 항상 같은 인스턴스
}

Weak 예시

container.register(CacheManager.self, scope: .weak) { CacheManager() }
class DataService {
    @Inject var cache: CacheManager?
}

Unowned 예시

container.register(Logger.self, scope: .unowned) { Logger.shared }
class APIService {
    @Inject var logger: Logger
}

Scoped 예시

await container.beginScope("UserSession") { scopedContainer in
    scopedContainer.register(UserSession.self) { UserSession(userId: currentUserId) }
}
// 로그아웃 시 자동 정리
await container.endScope("UserSession")

Transient 예시

container.register(APIRequest.self, scope: .transient) { APIRequest() }

🔍 순환 의존성 탐지 및 해결

자동 탐지

public actor CyclicDependencyDetector {
    private var currentResolutionPath: [ObjectIdentifier] = []
    
    func checkForCycle<T>(_ type: T.Type) async throws {
        let typeId = ObjectIdentifier(type)
        if currentResolutionPath.contains(typeId) {
            throw DIError.circularDependency("순환 참조 발견")
        }
        currentResolutionPath.append(typeId)
        defer { currentResolutionPath.removeLast() }
    }
}

해결 방법

  • Protocol + weak delegate
  • Lazy Injection / Factory
protocol UserServiceDelegate: AnyObject { func userDidUpdate(_ user: User) }

class UserService {
    weak var delegate: UserServiceDelegate?
    @Inject private var repository: UserRepository
}

🧹 메모리 압박 대응

  • iOS의 메모리 워닝 이벤트 감지
  • Transient/Weak 인스턴스 자동 정리
  • LRU 기반 캐싱으로 메모리 효율 유지
await container.purgeTransientInstances()
await container.cleanupWeakReferences()
await container.evictLeastRecentlyUsed(maxCount: 10)

📈 실제 성능 측정 결과

메모리 사용량 비교 (실제 프로덕션 앱 데이터)

시나리오기존 DIDiContainer 2.0개선율
앱 시작 직후45MB42MB6.7% ↓
30분 사용 후180MB95MB47.2% ↓
메모리 워닝 후200MB+80MB60% ↓
백그라운드 복귀150MB75MB50% ↓

🛡️ 안전 가이드라인

  1. 올바른 스코프 선택 (Singleton/Transient/Weak/Scoped)
  2. 순환 참조 방지 (weak, unowned 적극 활용)
  3. Observer 패턴과 WeakBox로 안전한 참조 관리
  4. 자동화된 메모리 리포트 & 테스트 코드로 검증
#if DEBUG
print(DependencyContainer.live.generateMemoryReport())
#endif

🎯 정리

  • 안전성: 순환 참조 자동 탐지\
  • 효율성: 자동 정리 + 스마트 캐싱\
  • 가시성: 메모리 리포트 기능\
  • 편의성: 개발자는 비즈니스 로직에 집중 가능

메모리는 한정된 자원입니다.\
DiContainer 2.0으로 더 안전하고 효율적인 앱을 만들어 보세요 🚀


다음 글 예고: SwiftUI + DiContainer 완전 정복 -- EnvironmentObject
vs @Inject

궁금한 점은 GitHub Issues에 남겨주세요!

profile
iOS 개발자 공부하는 Roy

0개의 댓글