[Swift Apple ] Dependency Injection(DI) x Swift 프로토콜 설계법

Swift Dependency Injection(DI, 의존성 주입)Swift 프로토콜로 어떻게 설계하는지 쉽고 실전적으로 설명해드리겠습니다. DI는 유연하고 테스트 가능한 코드를 만들기 위한 핵심 설계 기법이에요. 특히 Swift에서는 프로토콜 + DI 조합이 가장 많이 사용됩니다!


[Swift Apple ] Dependency Injection(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()


💡 이렇게 하면 좋은 점

  • UserViewModelAPIManager 구체 타입을 전혀 몰라요!

  • 나중에 네트워크 로직이 변경되더라도 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에서는 보통 간단하게 직접 관리)




✏️ 실습 아이디어

  1. 프로토콜 기반 데이터베이스 서비스 설계 (DI 적용)

  2. 이미지 다운로드 서비스를 DI + Mock으로 테스트

  3. Strategy 패턴과 결합하여 동적 기능 변경 + DI 설계

  4. 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 를 잘 활용하면

더 이상 강하게 얽힌 코드가 아닌,

유연하고 테스트 가능한 구조를 갖출 수 있습니다!

댓글 쓰기