Skip to content

WeaveDI 모범 사례

프로덕션 애플리케이션에서 WeaveDI를 효과적으로 사용하기 위한 권장 패턴과 사례입니다.

Property Wrapper 선택

대부분의 경우 @Injected 사용 (v3.2.0+)

swift
// ✅ 권장: 타입 안전, TCA 스타일
@Injected(\.userService) var userService
@Injected(\.apiClient) var apiClient

이유:

  • KeyPath를 통한 컴파일 타임 타입 안전성
  • 기본적으로 non-optional (liveValue/testValue 폴백)
  • withInjectedValues를 통한 향상된 테스트 지원
  • TCA 호환 API

새 인스턴스가 필요한 경우 @Factory 사용

swift
// ✅ 좋음: 무상태 작업
@Factory var pdfGenerator: PDFGenerator
@Factory var reportBuilder: ReportBuilder
@Factory var dateFormatter: DateFormatter

사용 시기:

  • 무상태 서비스 (PDF 생성기, 포맷터, 파서)
  • 각 작업마다 독립적인 상태가 필요한 경우
  • 독립적인 인스턴스로 동시 처리
  • 새로운 인스턴스가 필요한 빌더 패턴

예제:

swift
class DocumentService {
    @Factory var pdfGenerator: PDFGenerator

    func generateReports(data: [ReportData]) async {
        await withTaskGroup(of: PDF.self) { group in
            for item in data {
                group.addTask {
                    // 각 작업마다 새 생성기 획득 - 상태 충돌 없음
                    let generator = self.pdfGenerator
                    return generator.generate(item)
                }
            }
        }
    }
}

@Inject/@SafeInject 사용 피하기 (v3.2.0부터 Deprecated)

swift
// ❌ 피하기: Deprecated
@Inject var service: UserService?
@SafeInject var api: APIClient?

// ✅ 대신 사용:
@Injected(\.service) var service
@Injected(\.api) var api

의존성 구성

기능별로 의존성 그룹화

swift
// ✅ 좋음: 기능 기반 구성
// File: DI/UserFeatureDependencies.swift
extension InjectedValues {
    // 사용자 기능 의존성
    var userService: UserService {
        get { self[UserServiceKey.self] }
        set { self[UserServiceKey.self] = newValue }
    }

    var userRepository: UserRepository {
        get { self[UserRepositoryKey.self] }
        set { self[UserRepositoryKey.self] = newValue }
    }
}

// File: DI/AuthFeatureDependencies.swift
extension InjectedValues {
    // 인증 기능 의존성
    var authService: AuthService {
        get { self[AuthServiceKey.self] }
        set { self[AuthServiceKey.self] = newValue }
    }

    var tokenManager: TokenManager {
        get { self[TokenManagerKey.self] }
        set { self[TokenManagerKey.self] = newValue }
    }
}

장점:

  • 명확한 기능 경계
  • 관련 의존성을 쉽게 찾을 수 있음
  • 기능 제거가 쉬움
  • 더 나은 코드 구성

InjectedKey 정의 중앙 집중화

swift
// ✅ 좋음: 한 곳에 모든 키
// File: DI/InjectedKeys.swift
struct UserServiceKey: InjectedKey {
    static var liveValue: UserService = UserServiceImpl()
    static var testValue: UserService = MockUserService()
}

struct APIClientKey: InjectedKey {
    static var liveValue: APIClient = URLSessionAPIClient()
    static var testValue: APIClient = MockAPIClient()
}

// 그런 다음 기능 파일에서 InjectedValues 확장

스코프 관리

적절한 스코프 사용

swift
// ✅ 앱 전체 서비스에 싱글톤
struct LoggerKey: InjectedKey {
    static var liveValue: Logger = ConsoleLogger()  // 공유 인스턴스
}

// ✅ 기능별 서비스에 스코프 지정
await WeaveDI.Container.bootstrap { container in
    container.register(SessionService.self, scope: .session) {
        SessionServiceImpl()
    }
}

스코프 가이드라인:

스코프사용 사례예제
Singleton앱 전체 서비스Logger, Analytics, Config
Session사용자 세션 서비스Auth token, User preferences
Request요청당 서비스네트워크 호출당 API 클라이언트
Transient새 인스턴스Formatters, Builders

예제: 다중 스코프 아키텍처

swift
// 앱 전체 싱글톤
struct AnalyticsKey: InjectedKey {
    static var liveValue: Analytics = FirebaseAnalytics()
}

// 세션 스코프 서비스
class SessionManager {
    @Injected(\.authToken) var authToken  // 세션마다 변경
    @Injected(\.analytics) var analytics  // 공유 싱글톤

    func login(credentials: Credentials) async {
        // authToken은 세션 특정
        // analytics는 앱 전체
    }
}

성능 최적화

의존성 수 최소화

swift
// ❌ 나쁨: 너무 많은 의존성
class ViewModel {
    @Injected(\.service1) var service1
    @Injected(\.service2) var service2
    @Injected(\.service3) var service3
    @Injected(\.service4) var service4
    @Injected(\.service5) var service5  // 너무 많음!
}

// ✅ 좋음: 서비스 조합
class ViewModel {
    @Injected(\.userFacade) var userFacade  // Facade 패턴
}

// Facade가 관련 서비스 결합
class UserFacade {
    @Injected(\.userService) var userService
    @Injected(\.authService) var authService
    @Injected(\.profileService) var profileService

    func performUserAction() {
        // 여러 서비스 조정
    }
}

무거운 의존성의 지연 로딩

swift
// ✅ 좋음: 지연 초기화
struct DatabaseKey: InjectedKey {
    static var liveValue: Database {
        // 비용이 많이 드는 초기화 지연
        Database.shared
    }
}

// 필요할 때만 접근
class DataService {
    @Injected(\.database) var database

    func saveData() {
        // Database는 처음 접근할 때만 초기화됨
        database.save()
    }
}

동시 작업에 @Factory 사용

swift
// ✅ 좋음: @Factory로 병렬 처리
class ImageProcessor {
    @Factory var imageFilter: ImageFilter

    func processImages(_ images: [UIImage]) async -> [UIImage] {
        await withTaskGroup(of: UIImage.self) { group in
            for image in images {
                group.addTask {
                    // 각 이미지마다 새 필터 - 스레드 충돌 없음
                    let filter = self.imageFilter
                    return filter.apply(to: image)
                }
            }

            var results: [UIImage] = []
            for await result in group {
                results.append(result)
            }
            return results
        }
    }
}

테스트 전략

테스트에 withInjectedValues 사용

swift
// ✅ 좋음: 스코프 지정된 의존성 오버라이드
func testUserLogin() async {
    await withInjectedValues { values in
        values.authService = MockAuthService()
        values.userService = MockUserService()
    } operation: {
        let viewModel = LoginViewModel()
        await viewModel.login(credentials: testCredentials)

        XCTAssertTrue(viewModel.isLoggedIn)
    }
}

장점:

  • 테스트 후 자동 정리
  • 전역 상태 오염 없음
  • 타입 안전 값 할당
  • async/await와 호환

InjectedKey에서 테스트 값 정의

swift
// ✅ 좋음: 내장 테스트 값
struct UserServiceKey: InjectedKey {
    static var liveValue: UserService = UserServiceImpl()
    static var testValue: UserService = MockUserService()  // 사전 정의된 모의 객체
}

// 테스트 컨텍스트에서 자동으로 testValue 사용
func testWithDefaults() async {
    await withInjectedValues { values in
        // testValue 자동 사용
    } operation: {
        // 테스트 코드
    }
}

테스트 헬퍼 생성

swift
// ✅ 좋음: 재사용 가능한 테스트 설정
extension XCTestCase {
    func withTestDependencies(
        userService: UserService = MockUserService(),
        apiClient: APIClient = MockAPIClient(),
        operation: () async throws -> Void
    ) async rethrows {
        await withInjectedValues { values in
            values.userService = userService
            values.apiClient = apiClient
        } operation: {
            try await operation()
        }
    }
}

// 테스트에서 사용
func testExample() async throws {
    await withTestDependencies {
        // 표준 모의 객체로 테스트
    }
}

에러 처리

누락된 의존성을 우아하게 처리

swift
// ✅ 좋음: 폴백 값
struct LoggerKey: InjectedKey {
    static var liveValue: Logger = ConsoleLogger()
    static var testValue: Logger = NoOpLogger()  // 테스트에서 조용함
}

// 서비스는 구성되지 않아도 항상 로거를 가짐
class Service {
    @Injected(\.logger) var logger  // nil이 아님

    func performAction() {
        logger.log("Action performed")  // 안전하게 호출
    }
}

시작 시 중요한 의존성 검증

swift
// ✅ 좋음: 조기 검증
@main
struct MyApp: App {
    init() {
        validateDependencies()
        setupDependencies()
    }

    func validateDependencies() {
        // 중요한 의존성 존재 확인
        precondition(
            type(of: InjectedValues.current.apiClient) != Never.self,
            "API Client must be configured"
        )
    }

    func setupDependencies() {
        // 의존성 구성
    }
}

의미 있는 에러 메시지 제공

swift
// ✅ 좋음: 설명적 에러
struct APIClientKey: InjectedKey {
    static var liveValue: APIClient {
        guard let baseURL = ProcessInfo.processInfo.environment["API_BASE_URL"] else {
            fatalError("""
                ❌ API_BASE_URL 환경 변수가 설정되지 않았습니다

                스킴의 환경 변수에서 API_BASE_URL을 구성하십시오:
                1. Edit Scheme → Run → Arguments → Environment Variables
                2. 추가: API_BASE_URL = https://api.example.com
                """)
        }
        return URLSessionAPIClient(baseURL: baseURL)
    }
}

아키텍처 패턴

프로토콜 기반 설계 사용

swift
// ✅ 좋음: 추상화를 위한 프로토콜
protocol UserService {
    func fetchUser(id: String) async throws -> User
    func updateUser(_ user: User) async throws
}

// 여러 구현 가능
class ProductionUserService: UserService { /* ... */ }
class MockUserService: UserService { /* ... */ }
class CachedUserService: UserService { /* ... */ }

// 한 번 정의하고 구현 교체
struct UserServiceKey: InjectedKey {
    static var liveValue: UserService = ProductionUserService()
    static var testValue: UserService = MockUserService()
}

의존성 계층화

swift
// ✅ 좋음: 명확한 계층
// 레이어 1: 인프라 (하단)
extension InjectedValues {
    var networkClient: NetworkClient { /* ... */ }
    var database: Database { /* ... */ }
    var logger: Logger { /* ... */ }
}

// 레이어 2: 데이터/Repository
extension InjectedValues {
    var userRepository: UserRepository { /* ... */ }
    var productRepository: ProductRepository { /* ... */ }
}

// 레이어 3: 도메인/비즈니스 로직
extension InjectedValues {
    var userService: UserService { /* ... */ }
    var orderService: OrderService { /* ... */ }
}

// 레이어 4: 프레젠테이션
// ViewModel은 레이어 3의 서비스를 주입

순환 의존성 피하기

swift
// ❌ 나쁨: 순환 의존성
class ServiceA {
    @Injected(\.serviceB) var serviceB
}

class ServiceB {
    @Injected(\.serviceA) var serviceA  // 순환!
}

// ✅ 좋음: 추상화 도입
protocol EventBus {
    func publish(_ event: Event)
}

class ServiceA {
    @Injected(\.eventBus) var eventBus  // 둘 다 추상화에 의존
}

class ServiceB {
    @Injected(\.eventBus) var eventBus  // 순환 의존성 없음
}

상속보다 조합 사용

swift
// ❌ 나쁨: 상속 기반
class BaseService {
    @Injected(\.logger) var logger
}

class UserService: BaseService {
    // logger 상속
}

// ✅ 좋음: 조합 기반
class UserService {
    @Injected(\.logger) var logger  // 명시적
    @Injected(\.database) var database

    // 명확하고 자체 포함
}

코드 구성 체크리스트

  • [ ] 새 코드에 @Injected 사용 (v3.2.0+)
  • [ ] 기능/모듈별로 의존성 그룹화
  • [ ] 명확한 의존성 계층 정의
  • [ ] 클래스당 의존성 최소화 (< 5개)
  • [ ] 추상화를 위한 프로토콜 사용
  • [ ] InjectedKey에서 liveValue와 testValue 모두 제공
  • [ ] 시작 시 중요한 의존성 검증
  • [ ] 의존성 관계 문서화
  • [ ] 순환 의존성 피하기
  • [ ] 서비스에 적절한 스코프 사용

피해야 할 안티패턴

❌ 서비스 로케이터 패턴

swift
// ❌ 나쁨: 수동 서비스 위치
class ViewModel {
    func loadData() {
        let service = InjectedValues.current.userService  // 나쁨!
        // 서비스 사용
    }
}

// ✅ 좋음: 의존성 주입
class ViewModel {
    @Injected(\.userService) var userService

    func loadData() {
        // userService 사용
    }
}

❌ 전역 싱글톤

swift
// ❌ 나쁨: 전역 싱글톤
class APIClient {
    static let shared = APIClient()
}

// ✅ 좋음: DI 관리
struct APIClientKey: InjectedKey {
    static var liveValue: APIClient = APIClient()
}

// 필요한 곳에 주입
@Injected(\.apiClient) var apiClient

❌ 기본값이 있는 생성자 주입

swift
// ❌ 나쁨: 숨겨진 의존성
class UserService {
    init(
        apiClient: APIClient = InjectedValues.current.apiClient,  // 나쁨!
        database: Database = InjectedValues.current.database
    ) { }
}

// ✅ 좋음: 명시적 의존성 주입
class UserService {
    @Injected(\.apiClient) var apiClient
    @Injected(\.database) var database

    init() { }  // 깔끔한 초기화
}

다음 단계

MIT 라이선스 하에 릴리스됨.