Dependency Injection, 즉 의존성 주입은 객체 지향 프로그래밍(OOP)에서 결합도를 낮추고 유연성을 높이기 위한 설계 패턴이다.
간단히 표현하자면, 어떤 객체 A가 다른 객체 B(의존성)를 필요로 할 때, A가 B를 직접 만들거나 찾아오는 대신, 외부(제 3자)에서 A에게 B를 공급(주입)해 주는 방식이다.
DI는 의존성 역전 원칙을 구현하는 가장 일반적인 방법이다.
let b = B())하거나, 팩토리/싱글톤을 통해 직접 찾아감.DI는 주로 다음과 같은 OOP 설계의 문제점들을 해결하기 위해 필수적으로 사용된다.
Concrete Class)에 묶이게 된다. (= 강한 결합)BImpl)를 주입해 주므로, A는 B가 어떻게 구현되었는지 알 필요가 없다. (= 결합도 낮음)NetworkManager를 직접 생성하면, A를 테스트할 때마다 실제 네트워크 요청이 실행된다. (= 느리고 불안정)NetworkManager 대신, NetworkManager의 프로토콜을 채택한 Mock 객체나 Dummy 객체를 주입할 수 있다. 이를 통해 A의 순수한 로직만 격리하여 빠르고 안정적으로 테스트가 가능하다.DatabaseProtocol을 사용해 DI를 하면, 데이터베이스를 변경하더라도 데이터를 사용하는 객체(예: UserRepository)는 수정할 필요가 없다. 오직 주입을 담당하는 부분만 새로운 RealmRepositoryImpl로 교체하면 된다.DI를 구현하는 방법은 여러가지가 있는데, 그 중 3가지 방법이 주로 사용된다.
각 방법의 구현방법을 익히고, 프로젝트의 성격에 따라 혼합하여 사용하면 된다.
객체를 생성하는 시점에 필요한 의존성을 생성장(Initializer)의 매개변수로 전달하는 가장 보편적이고 권장되는 방법이다.
// 예시
protocol UserRepository {
func fetchUser()
}
class DefaultUserRepository: UserRepository { ... }
class UserViewModel {
private let repository: UserRepository // 의존성 선언
// 생성자를 통해 의존성 주입
init(repository: UserRepository) {
self.repository = repository
}
}
객체를 먼저 생성한 후, 나중에 객체의 속성(프로퍼티)을 통해 의존성을 주입하는 방법이다.
nil 체크가 필요하며 안전성이 낮음// 예시
class ViewController: UIViewController {
var viewModel: UserViewModel? // 프로퍼티를 옵셔널로 선언
override func viewDidLoad() {
super.viewDidLoad()
// 옵셔널 체이닝으로 viewModel의 유무 확인 및 메서드 실행
viewModel?.fetchData()
}
}
// 외부에서 의존성 주입
let vc = ViewController()
vc.viewModel = UserViewModel(repository: DefaultUserRepository())
객체의 생명주기 전체가 아닌 특정 메서드가 실행될 때만 일시적으로 의존성이 필요한 경우, 해당 메서드의 매개변수로 주입한다.
// 예시
protocol Logger {
func log(_ message: String)
}
class ReportGenerator {
// 필요한 시점에만 주입받음
func generate(with logger: Logger) { // 메서드 매개변수로 주입
// ... 리포트 생성 로직
logger.log("Report generation finished.")
}
}
오늘은 DI 패턴이 무엇인지, 왜 사용하는지에 대해 알아보았다.
최근 클린아키텍처 패턴에서 DI 패턴을 사용하는 프로젝트를 진행 중이어서, 대략적으로만 이해하고 있던 DI 패턴에 대해 보다 명확히 알 필요성을 느껴 공부를 해보았는데, 모호했던 개념들이 정리되며 구조를 더 잘 이해하게 된 것 같다.