Dependency Injection: 정의, iOS 및 Android에서의 의존성 주입

저자: IT Sectr 게시일: 2026-02-18 읽는 시간: 9 분

의존성 주입(DI)은 객체가 의존성을 직접 생성하지 않고 외부에서 받는 기술입니다. DI는 IoC(제어의 역전) 원칙의 구현체이며 Dagger, Hilt 및 Swinject의 기초입니다. 의존성 주입은 코드 결합도를 줄이고, 테스트를 단순화하며, 아키텍처를 유연하게 만듭니다. Android에서는 Google의 Dagger Hilt가 DI 표준이고, iOS에서는 Swinject 또는 수동 주입이 사용됩니다. 자세한 내용은 Android DI 가이드를 참조하세요.

핵심 요점

  • 의존성 주입 — 의존성이 객체에 외부에서 전달되며, 내부에서 생성되지 않음
  • 제어의 역전 — DI는 IoC 원칙을 구현하며, 제어 흐름이 컨테이너로 이전됨
  • Dagger Hilt — Google의 Dagger 기반 Android용 DI 표준
  • Swinject — iOS 및 Swift를 위한 인기 있는 DI 프레임워크
  • 결합도 감소 — 클래스가 구체적인 구현이 아닌 추상화에 의존

Dependency Injection이란: 본질 및 DI 유형

의존성 주입은 객체가 생성자, 세터 또는 인터페이스를 통해 의존성(서비스, 리포지토리, 구성)을 받고, new로 직접 생성하지 않는 기술입니다. DI의 목표는 클래스 간의 결합도를 줄이는 것입니다. 클래스가 의존성을 직접 생성하면 특정 구현에 강하게 결합되어 테스트와 수정이 어려워집니다. DI를 사용하면 클래스는 추상화(프로토콜/인터페이스)에 대해 작업하고, 구체적인 구현은 외부에서 제공됩니다.

세 가지 주입 방식 — 생성자 주입(init/constructor 통해), 세터 주입(속성/setter 통해), 인터페이스 주입(인터페이스 메서드를 통해). 생성자 주입이 선호되는 방식입니다. 의존성이 시그니처에 명확하게 표시되고 객체가 항상 유효한 상태로 생성됩니다. 세터 주입은 기본값이 있는 선택적 의존성에 사용됩니다. 인터페이스 주입은 드물며 주로 DI 컨테이너에 사용됩니다.

DI 유형방식사용 시기예시
생성자초기화 매개변수필수 의존성init(service: ServiceProtocol)
속성클래스 속성선택적 의존성var service: ServiceProtocol?
메서드메서드 매개변수임시 의존성func doWork(with service: Service)

DI 컨테이너 — 의존성의 생성과 생명주기를 관리하는 라이브러리입니다. 컨테이너는 유형 등록(각 추상 유형을 구체적인 구현에 매핑)과 해결된 의존성을 가진 객체를 생성하는 팩토리를 포함합니다. Android에서는 Dagger/Hilt, iOS에서는 Swinject, Needle, Dip이 사용됩니다. 컨테이너는 범위(Scope)를 관리할 수 있습니다: 싱글톤(애플리케이션당 하나의 인스턴스), 기능 범위(화면별) 또는 각 요청 시 새 객체.

Dagger Hilt: 코드 생성을 통한 Android용 DI

Dagger Hilt는 Google의 Dagger를 래핑한 Android용 표준 DI 라이브러리입니다. Hilt는 Dagger를 단순화합니다: 수동 컴포넌트 생성을 제거하고 @HiltAndroidApp, @AndroidEntryPoint 및 @Module을 추가합니다. Hilt는 Android 생명주기와 통합되어 ViewModel, Activity, Fragment, Service, BroadcastReceiver가 어노테이션을 통해 의존성을 받을 수 있습니다. 코드 생성은 컴파일 타임에 이루어지며 Dagger가 컴포넌트 구현을 생성하여 런타임 오버헤드가 없습니다.

kotlin
// 애플리케이션 클래스
@HiltAndroidApp
class MyApp : Application()

// 모듈 — 의존성 생성 방법을 정의
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Provides
    @Singleton
    fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder().build()

    @Provides
    @Singleton
    fun provideApiService(client: OkHttpClient): ApiService {
        return Retrofit.Builder()
            .baseUrl("https://api.example.com")
            .client(client)
            .build()
            .create(ApiService::class.java)
    }
}

// ViewModel이 생성자를 통해 의존성 수신
@HiltViewModel
class MainViewModel @Inject constructor(
    private val apiService: ApiService
) : ViewModel() {
    private val _state = MutableStateFlow(MainState.Loading)
    val state: StateFlow<MainState> = _state.asStateFlow()

    fun loadData() {
        viewModelScope.launch {
            _state.value = MainState.Success(apiService.getData())
        }
    }
}

// Activity — @AndroidEntryPoint가 DI 활성화
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    private val viewModel: MainViewModel by viewModels()
}

Dagger 컴포넌트 및 범위 — @Singleton(전체 애플리케이션), @ActivityScoped(Activity별), @FragmentScoped(Fragment별), @ViewModelScoped(ViewModel별). 범위 선택이 객체의 수명을 결정합니다. @Singleton — 프로세스당 하나의 인스턴스, OkHttpClient 및 데이터베이스에 적합합니다. @ActivityScoped — Activity가 살아있는 동안 객체가 유지되며 화면 수준 의존성에 사용됩니다. @ViewModelScoped — Hilt 2.45+의 새로운 기능으로, ViewModel이 살아있는 동안 객체가 유지되어 코루틴 범위에 편리합니다.

Swinject: Swift를 사용한 iOS용 DI

Swinject는 iOS용 인기 있는 오픈 소스 DI 프레임워크입니다. Swinject는 Container, Assemblies 및 다양한 범위를 제공합니다. Dagger와 달리 Swinject는 런타임에 작동하며 의존성이 코드 생성 없이 동적으로 해결됩니다. 이로 인해 Swinject는 설정이 쉽지만 디버깅이 더 어렵습니다: 해결되지 않은 의존성 오류는 런타임에만 나타납니다. Swinject는 생성자 주입, 속성 주입 및 메서드 주입을 지원합니다.

swift
import Swinject

// Assembly — 등록 그룹
class NetworkAssembly: Assembly {
    func assemble(container: Container) {
        container.register(NetworkServiceProtocol.self) { _ in
            NetworkService()
        }.inObjectScope(.container) // singleton

        container.register(UserRepositoryProtocol.self) { r in
            UserRepository(
                networkService: r.resolve(NetworkServiceProtocol.self)!
            )
        }
    }
}

// ViewModel이 생성자 주입을 통해
class ProfileViewModel: ObservableObject {
    private let repository: UserRepositoryProtocol

    init(repository: UserRepositoryProtocol) {
        self.repository = repository
    }

    @Published var user: User?
    func loadUser() {
        repository.fetchUser { [weak self] user in
            self?.user = user
        }
    }
}

// AppDelegate 또는 App에서 DI 설정
let assembler = Assembler([NetworkAssembly(), ServiceAssembly()])
let viewModel = assembler.resolver.resolve(ProfileViewModel.self)!

// UIKit ViewController를 위한 속성 주입
container.register(UserViewController.self) { r in
    let vc = UserViewController()
    vc.viewModel = r.resolve(ProfileViewModel.self)
    return vc
}

Swinject 범위 — .transient(매번 새 객체), .container(컨테이너당 싱글톤), .graph(기본값 — 하나의 의존성 그래프 내에서 객체 공유). iOS 애플리케이션의 경우 .container와 .transient로 충분합니다. Swinject는 또한 Assembler를 지원하여 모듈형 아키텍처를 위한 Assembly 그룹화를 제공합니다. 테스트를 위해 Assembly를 MockAssembly로 대체하여 프로덕션 코드 변경 없이 의존성 대체가 가능합니다.

Service Locator 및 수동 주입과 DI 비교

DI vs Service Locator — 두 패턴 모두 의존성 관리 문제를 해결하지만 방식이 다릅니다. DI는 객체에 의존성을 주입합니다. Service Locator는 전역 레지스트리를 제공하며 객체가 스스로 의존성을 요청합니다. DI는 생성자(또는 세터)를 통해 의존성을 명시적으로 선언합니다. Service Locator는 의존성을 숨기며 메서드 내부에서 요청되어 시그니처의 정보성이 낮아집니다. DI는 테스트가 더 쉽습니다: 생성자에 모크를 전달하기만 하면 됩니다. Service Locator는 각 테스트마다 전역 레지스트리 설정이 필요합니다.

특성의존성 주입Service Locator수동 주입
의존성 명시성생성자 내메서드 본문에 숨김명시적
테스트생성자에 모크Locator 설정생성자에 모크
설정 복잡성DI 컨테이너 필요전역 레지스트리수동 생성
런타임 오버헤드Dagger — 컴파일 타임런타임 조회없음

DI vs 수동 주입 — DI 컨테이너 없이 의존성은 팩토리나 AppDelegate에서 수동으로 생성됩니다. 5-10개 클래스의 경우 수동 주입이 더 간단하며 Dagger나 Swinject를 배울 필요가 없습니다. 50개 이상의 클래스에서는 수동 주입이 문제가 됩니다: 5-6개 매개변수가 있는 생성자, 복잡한 생성 순서, 코드 중복. DI 컨테이너는 이러한 프로세스를 자동화하고 명확한 생명주기 관리를 제공합니다. 컨테이너 없는 수동 주입은 소규모 프로젝트와 프로토타입에 적합합니다.

의존성 주입 모범 사례

생성자 주입 — 표준. 필수 의존성에는 항상 생성자 주입을 사용하세요. 이렇게 하면 의존성이 명확해지고 객체가 항상 작동 가능한 상태가 됩니다. 세터 주입은 선택적 의존성에만 사용합니다(예: delegate 또는 listener). 인터페이스 주입은 자체 DI 라이브러리를 작성하는 경우가 아니라면 사용하지 마세요. 생성자 주입은 객체가 유효한 상태로 생성됨을 보장하는 유일한 방법입니다.

하나의 클래스 — 하나의 책임. 클래스 생성자에 5개 이상의 매개변수가 필요한 경우, 클래스가 단일 책임 원칙을 위반하고 있을 가능성이 높습니다. 더 적은 의존성을 가진 여러 클래스로 분할하세요. 징후: 6개의 다른 서비스를 가진 ServiceManager 클래스를 작성하고 있다면 God Object 안티패턴입니다. 비즈니스 로직을 Use Cases(Interactors)로 추출하고 각각 1-2개의 의존성을 갖도록 하세요.

kotlin
// ❌ 나쁨: 6개 의존성 — God Object
class ProfileViewModel @Inject constructor(
    private val api: ApiService,
    private val db: Database,
    private val analytics: Analytics,
    private val prefs: Preferences,
    private val location: LocationProvider,
    private val notification: NotificationManager
)

// ✅ 좋음: 1-2개 의존성을 가진 Use Cases
class ProfileViewModel @Inject constructor(
    private val loadProfileUseCase: LoadProfileUseCase,
    private val trackAnalyticsUseCase: TrackAnalyticsUseCase
)

범위와 생명주기 — 각 의존성에 적합한 범위를 선택하세요. 싱글톤: OkHttpClient, 데이터베이스, SharedPreferences. 기능 범위: 리포지토리, Use Cases(상태가 없는 경우). Transient: Value Objects, DateFormatter, 파서. 범위 지정 오류는 일반적인 문제입니다: 화면 상태를 저장하는 싱글톤은 메모리 누수를 유발합니다. Android에서는 Hilt의 @ActivityScoped가 이를 해결하고, Swinject에서는 .container를 주의해서 사용하세요.

자주 묻는 질문

new를 사용하면 되는데 왜 DI가 필요한가요?

new는 클래스 간에 강한 결합을 만듭니다 — 코드를 변경하지 않고 구현을 교체할 수 없습니다. 테스트가 어려워집니다: 실제 서비스 대신 모크를 주입할 수 없습니다. SRP가 위반됩니다: 클래스가 비즈니스 로직과 의존성 생성 모두에 책임을 갖습니다. DI는 외부에서 의존성을 주입하고 추상화에 대해 작업함으로써 이러한 문제를 해결합니다.

Dagger Hilt와 Koin — Android에서 무엇을 선택해야 하나요?

Dagger Hilt는 Google의 표준으로, 코드 생성이 있는 컴파일 타임 DI, 더 나은 성능 및 Jetpack 통합을 제공합니다. Koin은 런타임 DI로 설정이 쉽지만 느리고 런타임 오류가 발생합니다. 프로덕션 프로젝트에는 Hilt를 선택하세요. Koin은 프로토타입과 소규모 애플리케이션에 적합합니다.

Swinject가 iOS의 유일한 DI 옵션인가요?

아니요. iOS에서는 Swinject(런타임, 인기), Needle(Uber의 컴파일 타임), Dip(경량), Weaver(Sourcery 기반)를 사용할 수 있습니다. Apple은 내장 DI 컨테이너를 제공하지 않지만 init을 통한 수동 주입이 표준 관행입니다. SwiftUI의 경우 외부 라이브러리 없이 Environment 또는 @StateObject를 통한 수동 DI로 충분한 경우가 많습니다.

프레임워크 없이 DI를 사용할 수 있나요?

네. 생성자를 통한 수동 주입은 프레임워크 없는 DI입니다. Service Locator는 프레임워크 없는 대안입니다. 팩토리와 Factory Method도 DI의 형태입니다. 프레임워크(Dagger, Swinject)는 일상적인 등록과 의존성 해결을 자동화하지만, 10-20개 클래스의 경우 수동 DI로 충분합니다.

DI는 패턴인가요, 원칙인가요?

DI는 제어의 역전(IoC) 원칙을 구현하는 기술(템플릿)입니다. GoF 패턴과 달리 DI는 엄격한 3-4 클래스 구조가 없습니다. DI는 의존성을 구성하는 방법이지 디자인 패턴이 아닙니다. DI 컨테이너(Dagger, Swinject)는 이 기술을 자동화하는 프레임워크입니다.

요약

  • DI — 생성자, 세터 또는 메서드를 통해 외부에서 의존성을 주입하는 기술
  • Dagger Hilt — 컴파일 타임 코드 생성 및 @HiltViewModel을 갖춘 Android용 DI 표준
  • Swinject — Container, Assembly 및 범위를 갖춘 iOS용 런타임 DI
  • 생성자 주입 — 필수 의존성에 선호되는 방식
  • 범위 — 상태 없는 서비스에는 싱글톤, 화면 수준 의존성에는 기능 범위
  • 테스트 — DI는 코드 변경 없이 의존성을 모크로 대체하는 것을 단순화

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기