티스토리 뷰
iOS 개발을 하다 보면 ViewController에서 네트워크 요청을 직접 호출하거나, 데이터베이스를 직접 접근하는 코드를 본 적이 있을 것이다.
이렇게 하면 간단한 기능은 빠르게 구현할 수 있지만, 코드가 복잡해질수록 유지보수가 어렵고 테스트하기도 어려워진다.
이 문제를 해결하는 방법이 바로 의존성 주입(Dependency Injection, DI) 이다!
DI를 활용하면 유지보수가 쉬워지고, 유닛 테스트가 편리해지며, 코드의 결합도가 낮아져 더 유연한 아키텍처를 만들 수 있다.
이번 포스팅에서는 DI의 개념과 활용하는 방법을 정리해보겠다.
1️⃣ 의존성이란?
✅ "의존성"이 뭘까?
객체가 다른 객체를 사용할 때, 이를 "의존성(Dependency)" 이라고 한다.
예를 들어, ViewController가 NetworkService를 직접 생성해서 사용한다면, ViewController는 NetworkService에 의존한다고 볼 수 있다.
class ViewController: UIViewController {
let networkService = NetworkService() // 직접 객체 생성
override func viewDidLoad() {
super.viewDidLoad()
networkService.fetchData()
}
}
이 코드의 문제점은?
- ViewController가 NetworkService에 강하게 결합되어 있어서 다른 네트워크 서비스로 교체하기 어려움
- 유닛 테스트가 어렵다 (NetworkService를 실제로 호출하기 때문에 Mocking이 어려움)
이런 문제를 해결하려면 객체를 직접 생성하는 것이 아니라, 외부에서 주입받아야 한다.
이 개념이 바로 의존성 주입(Dependency Injection, DI) 이다.
2️⃣ 의존성 주입(Dependency Injection)이란?
✅ DI의 개념
의존성 주입(Dependency Injection) 은 객체가 직접 의존성을 생성하는 것이 아니라, 외부에서 주입받도록 하는 설계 패턴이다.
이렇게 하면
- 코드의 결합도가 낮아지고
- 다른 구현체로 쉽게 변경할 수 있으며
- 유닛 테스트도 쉽게 작성할 수 있다
3️⃣ 의존성 주입의 3가지 방법
DI를 적용하는 방법에는 3가지 방법이 있다.
| 방법 | 설명 코드 | 코드 예제 |
| 1. 생성자 주입 (Constructor Injection) | 객체를 생성할 때, 의존성을 주입 | init(service: NetworkService) |
| 2. 프로퍼티 주입 (Property Injection) | 객체 생성 후, 외부에서 의존성을 설정 | vc.service = NetworkService() |
| 3. 메서드 주입 (Method Injection) | 특정 메서드를 호출할 때, 의존성을 주입 | vc.setup(service: NetworkService()) |
이제 각각의 방법을 코드로 살펴보자.
✅ 1. 생성자 주입 (Constructor Injection)
가장 선호되는 방법으로, 객체가 생성될 때 의존성을 주입하는 방식이다.
protocol NetworkService {
func fetchData()
}
class APIService: NetworkService {
func fetchData() {
print("네트워크에서 데이터 가져오기")
}
}
// ✅ 생성자 주입을 사용하여 의존성 주입
class ViewController {
let service: NetworkService
init(service: NetworkService) {
self.service = service
}
func loadData() {
service.fetchData()
}
}
// 사용 예제
let apiService = APIService()
let vc = ViewController(service: apiService) // 의존성을 주입
vc.loadData()
📌 이 방법이 좋은 이유
- ViewController가 NetworkService 프로토콜에만 의존 → 의존성 역전 원칙(DIP)를 준수
- APIService 대신 MockService를 주입하면 유닛 테스트가 가능
class MockService: NetworkService {
func fetchData() {
print("Mock 데이터 가져오기")
}
}
let mockService = MockService()
let testVC = ViewController(service: mockService) // 테스트에서 Mock 객체 주입
testVC.loadData()
📌 언제 사용하면 좋을까?
- 불변(immutable) 의존성을 다룰 때 (한 번 주입되면 변경되지 않는 경우)
- 객체의 생명주기가 길 때 (서비스 레이어, 유틸리티 클래스 등)
✅ 2. 프로퍼티 주입 (Property Injection)
객체를 생성한 후, 외부에서 의존성을 설정하는 방식이다.
class ViewController {
var service: NetworkService? // 처음에는 nil
func loadData() {
service?.fetchData()
}
}
// 사용 예제
let vc = ViewController()
vc.service = APIService() // 외부에서 주입
vc.loadData()
📌 언제 사용하면 좋을까?
- 의존성이 선택적(optional)일 때
- 뷰 컨트롤러와 같은 UI 관련 클래스에서 사용 (예: Storyboard 사용 시)
✅ 3. 메서드 주입 (Method Injection)
특정 메서드를 호출할 때 의존성을 주입하는 방식이다.
class ViewController {
func loadData(service: NetworkService) {
service.fetchData()
}
}
// 사용 예제
let vc = ViewController()
vc.loadData(service: APIService()) // 실행할 때마다 다른 서비스 주입 가능
📌 언제 사용하면 좋을까?
- 매번 다른 의존성을 주입해야 하는 경우 (예: 전략 패턴)
- 유닛 테스트에서 특정 시점에 의존성을 변경할 필요가 있을 때
4️⃣ 의존성 주입이 실무에서 중요한 이유
- 테스트가 쉬워진다!
- 실제 NetworkService 대신 MockService를 주입하면, 네트워크 요청 없이 테스트 가능
- 코드의 유연성이 증가한다!
- 네트워크 서비스 변경 시 APIService 구현체만 교체하면 됨
- DI가 없으면 ViewController를 수정해야 하는데, DI를 적용하면 기존 코드 수정 없이 가능
- 아키텍처 패턴과의 궁합이 좋다!
- MVVM, Clean Architecture, TCA 등의 패턴에서는 DI가 필수적
- 특히 Dependency Injection Container(예: Swinject)를 활용하면 객체 생성을 더 간편하게 관리 가능
정리
✅ 의존성(Dependency)이란? 한 객체가 다른 객체를 사용할 때의 관계
✅ 의존성 주입(DI)이란? 객체가 직접 의존성을 생성하는 것이 아니라, 외부에서 주입하는 설계 패턴
✅ DI를 적용하면 코드의 유지보수가 쉬워지고, 테스트가 간편해지며, 결합도가 낮아진다
✅ DI의 3가지 방법
- 생성자 주입: 객체 생성 시 의존성 주입 (가장 선호되는 방식)
- 프로퍼티 주입: 객체 생성 후 의존성 설정 (UI 관련 클래스에 적합)
- 메서드 주입: 특정 메서드 실행 시 의존성 전달 (전략 패턴에 유용)
DI를 잘 활용하면 더 유지보수하기 쉬운 iOS 앱을 만들 수 있다.
이제 직접 프로젝트에서 적용해보자!
'프로그래밍 공부' 카테고리의 다른 글
| Clean Architecture - Layer Separation & Dependency Rule (0) | 2025.03.10 |
|---|---|
| MVVM(Model-View-ViewModel) 패턴 제대로 이해하기 (0) | 2025.03.07 |
| ARC(Automatic Reference Counting) – Swift는 어떻게 메모리를 관리할까? (1) | 2025.02.20 |
| Swift – iOS 개발자가 알아야 할 모든 것 (2) | 2025.02.19 |
| 객체 지향 설계의 SOLID 원칙 – 제대로 알고 써보자! (0) | 2025.02.18 |