Swift Dependency Injection(DI, 의존성 주입) 을 Swift 프로토콜로 어떻게 설계하는지 쉽고 실전적으로 설명해드리겠습니다. DI는 유연하고 테스트 가능한 코드를 만들기 위한 핵심 설계 기법이에요. 특히 Swift에서는 프로토콜 + DI 조합이 가장 많이 사용됩니다!
[Swift Apple ] Dependency Injection(DI) |
🚀 Dependency Injection(DI) x Swift 프로토콜 설계법
– 유연성과 테스트 효율을 높이는 프로페셔널한 설계 전략
✅ Dependency Injection(DI)란?
의존성 주입(DI) 이란,
클래스나 구조체가 직접 의존 객체를 생성하지 않고,
외부에서 필요한 객체를 주입받는 설계 방식입니다.
쉽게 말해! ❌ 나 혼자 만들지 말고, 필요한 건 밖에서 받아 써라!
🔧 DI가 필요한 이유
-
유지보수성 향상: 코드 수정이 쉬워짐
-
확장성 증가: 새로운 기능 추가 시 기존 코드 변경 최소화
-
테스트 용이성: Mock 객체 주입으로 유닛 테스트 가능
-
느슨한 결합(Loose Coupling): 클래스 간 의존성 최소화
🎯 DI를 프로토콜로 설계하는 기본 구조
1️⃣ 프로토콜 정의 → 역할만 선언
2️⃣ 구현체(Concrete Class) 생성
3️⃣ 필요한 곳에 프로토콜 타입으로 주입
이렇게 하면 구체 타입에 의존하지 않고,
추상화(프로토콜) 에 의존하는 깨끗한 코드가 완성됩니다.
🏗️ 예제 1: 기본 DI 패턴 – 네트워크 서비스 설계
1. 프로토콜 정의
protocol NetworkService {
func fetchData(endpoint: String, completion: @escaping (Data?) -> Void)
}
2. 실제 구현 클래스
class APIManager: NetworkService {
func fetchData(endpoint: String, completion: @escaping (Data?) -> Void) {
// 실제 API 호출 로직
print("📡 Fetching data from \(endpoint)")
completion(nil)
}
}
3. ViewModel에 의존성 주입
class UserViewModel {
private let networkService: NetworkService
init(networkService: NetworkService) {
self.networkService = networkService
}
func loadUser() {
networkService.fetchData(endpoint: "/user") { data in
print("✅ 유저 데이터 처리 완료")
}
}
}
4. 사용하기 (의존성 주입)
let apiManager = APIManager()
let viewModel = UserViewModel(networkService: apiManager)
viewModel.loadUser()
💡 이렇게 하면 좋은 점
-
UserViewModel 은 APIManager 구체 타입을 전혀 몰라요!
-
나중에 네트워크 로직이 변경되더라도 ViewModel은 영향 ❌
-
테스트할 때는 MockNetworkService 를 주입하면 끝!
🧪 예제 2: 테스트용 Mock 객체 주입
Mock 클래스 생성
class MockNetworkService: NetworkService {
func fetchData(endpoint: String, completion: @escaping (Data?) -> Void) {
print("🧪 [Mock] 가짜 데이터 반환")
completion(Data())
}
}
테스트 코드
let mockService = MockNetworkService()
let testViewModel = UserViewModel(networkService: mockService)
testViewModel.loadUser()
✅ 덕분에 네트워크 없이도 테스트 환경 구성 가능!
🎯 DI 방식 3가지
방식 |
설명 |
---|---|
생성자 주입 |
init()을 통해 주입 (가장 많이 사용) |
프로퍼티 주입 |
외부에서 변수로 주입 |
메서드 주입 |
함수 호출 시 의존성 전달 |
예시 - 프로퍼티 주입
class ProfileViewModel {
var networkService: NetworkService?
}
하지만 생성자 주입이 가장 안전하고 권장되는 방식입니다.
🚨 주의사항
-
프로토콜로 추상화 하지 않으면 DI의 효과가 줄어듭니다.
-
구체 타입을 직접 참조하면 다시 강한 결합이 발생!
-
의존성이 많아지면, DI 컨테이너 사용 고려 (Swift에서는 보통 간단하게 직접 관리)
✏️ 실습 아이디어
-
프로토콜 기반 데이터베이스 서비스 설계 (DI 적용)
-
이미지 다운로드 서비스를 DI + Mock으로 테스트
-
Strategy 패턴과 결합하여 동적 기능 변경 + DI 설계
-
SPM 패키지와 함께 DI 구조로 프로젝트 분리
각 실습 아이디어에 대한 Swift 코드 예시입니다. 프로토콜 기반 설계 + DI(의존성 주입) + 테스트 가능성 + 확장성을 고려하여 구성, 실제 프로젝트 구조로 발전시키기 쉬운 설계 방식입니다.
✅ 1. 프로토콜 기반 데이터베이스 서비스 설계 (DI 적용)
// 1️⃣ 데이터베이스 서비스 프로토콜
protocol DatabaseService {
func save(data: String)
func load() -> String
}
// 2️⃣ 실제 DB 구현체
class LocalDatabase: DatabaseService {
private var storage: String = ""
func save(data: String) {
storage = data
print("💾 데이터 저장됨: \(data)")
}
func load() -> String {
return storage
}
}
// 3️⃣ 데이터 매니저 (의존성 주입 사용)
class DataManager {
let database: DatabaseService
init(database: DatabaseService) {
self.database = database
}
func processData() {
database.save(data: "사용자 정보")
print("📦 불러온 데이터: \(database.load())")
}
}
// 사용 예시
let manager = DataManager(database: LocalDatabase())
manager.processData()
✅ 2. 이미지 다운로드 서비스를 DI + Mock으로 테스트
// 1️⃣ 이미지 서비스 프로토콜
protocol ImageDownloader {
func download(url: String, completion: @escaping (String) -> Void)
}
// 2️⃣ 실제 구현
class RealImageDownloader: ImageDownloader {
func download(url: String, completion: @escaping (String) -> Void) {
completion("📸 실제 이미지 다운로드 완료: \(url)")
}
}
// 3️⃣ Mock 객체
class MockImageDownloader: ImageDownloader {
func download(url: String, completion: @escaping (String) -> Void) {
completion("🧪 Mock 이미지 사용: \(url)")
}
}
// 4️⃣ 사용 클래스
class ImageViewer {
let downloader: ImageDownloader
init(downloader: ImageDownloader) {
self.downloader = downloader
}
func showImage() {
downloader.download(url: "http://example.com/image.jpg") { result in
print(result)
}
}
}
// 테스트 실행
let viewer = ImageViewer(downloader: MockImageDownloader())
viewer.showImage()
✅ 3. Strategy 패턴 + DI 설계: 메시지 발송 시스템
// 1️⃣ 메시지 발송 전략 프로토콜
protocol MessageStrategy {
func send(message: String)
}
// 2️⃣ 전략 구현
class EmailStrategy: MessageStrategy {
func send(message: String) {
print("📧 이메일 전송: \(message)")
}
}
class SMSStrategy: MessageStrategy {
func send(message: String) {
print("📩 문자메시지 전송: \(message)")
}
}
// 3️⃣ 컨텍스트 클래스 (DI로 전략 주입)
class MessageManager {
var strategy: MessageStrategy
init(strategy: MessageStrategy) {
self.strategy = strategy
}
func deliver(message: String) {
strategy.send(message: message)
}
}
// 사용 예시
let manager = MessageManager(strategy: SMSStrategy())
manager.deliver(message: "쿠폰이 도착했습니다!")
// 전략 교체 (동적 기능 변경)
manager.strategy = EmailStrategy()
manager.deliver(message: "이메일 확인해 주세요.")
✅ 4. SPM 패키지와 함께 DI 구조로 프로젝트 분리 (예시 구조 설명 포함)
🧱 구조 개요
MyProject/
├── App (실행부)
│ └── ContentView.swift
├── CorePackage (SPM 패키지)
│ ├── DataService.swift
│ ├── Protocols.swift
│ └── MockService.swift
📦
CorePackage
내부 예시 코드
Protocols.swift
public protocol DataService {
func fetchData() -> String
}
DataService.swift
public class RealDataService: DataService {
public init() {}
public func fetchData() -> String {
return "📡 서버에서 데이터 가져옴"
}
}
MockService.swift
public class MockDataService: DataService {
public init() {}
public func fetchData() -> String {
return "🧪 테스트 데이터 사용"
}
}
🧩 App Target 내 코드
import CorePackage
class ViewModel {
let service: DataService
init(service: DataService) {
self.service = service
}
func load() {
print("📦 결과: \(service.fetchData())")
}
}
// 실사용 또는 테스트 용도 선택 가능
let vm = ViewModel(service: RealDataService()) // 또는 MockDataService()
vm.load()
✅ SPM으로 분리하면 유지보수성과 테스트, 협업 효율이 비약적으로 증가합니다.
✅ 요약
실습 주제 |
주요 기술 요소 |
---|---|
프로토콜 기반 DB 서비스 + DI |
책임 분리, 확장 가능 |
이미지 다운로드 + Mock 테스트 |
테스트 자동화에 유용 |
Strategy + DI |
동적 교체 + 유연한 설계 |
SPM + DI 분리 프로젝트 |
모듈화, 테스트/실서비스 분리 |
🎯 마무리 정리
핵심 포인트 |
설명 |
---|---|
프로토콜 정의 |
역할만 선언 (추상화) |
구현체와 분리 |
구체 클래스는 외부에서 주입 |
테스트 유연성 |
Mock 객체 활용 가능 |
확장성 & 유지보수성 |
기능 변경 시 최소 수정 |
Swift에서 프로토콜 + DI 를 잘 활용하면
더 이상 강하게 얽힌 코드가 아닌,
유연하고 테스트 가능한 구조를 갖출 수 있습니다!
댓글 쓰기