목차
6.
의존성 주입이란?
클래스나 구조체가 필요로 하는 외부 객체(의존성)를 직접 생성하지 않고 외부에서 전달받는 방식을 말합니다.
쉽게 말해서, 내가 필요한 것을 직접 만들지 않고 누군가에게 받아서 사용한다는 개념입니다.
간단한 예시로 커피 주문 시스템을 통해 알아보겠습니다.
의존성 주입이 없다면 다음과 같이 코드를 작성하게 됩니다.
class CafeCounter {
func serveDrink() {
let americano = Americano() // 직접 생성 - 강한 결합
print(americano.prepare())
}
}
Swift
복사
이렇게 작성하면 CafeCounter와 Americano가 강하게 결합되어 다른 음료로 변경하기 어렵고 테스트하기도 어려워집니다.
Property Injection
class CafeCounter {
var beverage: Americano?
func serveDrink() {
guard let beverage = beverage else {
print("음료가 선택되지 않았습니다!")
return
}
print(beverage.prepare())
}
}
let counter = CafeCounter()
counter.beverage = Americano() // 속성으로 주입
counter.serveDrink()
Swift
복사
가장 기본적인 형태의 주입 방식이지만 몇 가지 단점이 있습니다.
•
옵셔널 사용 필수 - 객체 생성 시점에 의존성이 주입되지 않으므로 옵셔널로 선언해야 합니다.
•
불안정성 - 런타임에 값이 nil이 될 수 있어 크래시 위험이 있습니다.
•
변경 가능성 - var로 선언되어 있어 외부에서 언제든 값을 변경할 수 있습니다.
Initializer Injection
class BetterCafeCounter {
private let beverage: Latte
init(beverage: Latte) {
self.beverage = beverage
}
func serveDrink() {
print(beverage.prepare())
}
}
let betterCounter = BetterCafeCounter(beverage: Latte())
betterCounter.serveDrink()
Swift
복사
생성자를 통해 의존성을 주입하면 위의 문제점들을 해결할 수 있습니다. 장점으로는 다음과 같습니다.
•
안전성 - 옵셔널을 사용하지 않아 런타임 크래시 위험이 없습니다.
•
불변성 - let으로 선언하여 한번 설정된 의존성이 변경되지 않습니다.
•
명확성 - 객체 생성 시점에 필요한 의존성이 명확합니다.
•
캡슐화 - private 키워드로 외부 접근을 제한할 수 있습니다.
Interface Injection
인터페이스 주입은 프로토콜을 활용하여 더욱 유연한 구조를 만듭니다.
protocol Beverage {
func prepare() -> String
}
class Americano: Beverage {
func prepare() -> String {
return "아메리카노 준비 완료"
}
}
class Latte: Beverage {
func prepare() -> String {
return "라떼 준비 완료"
}
}
class GreenTea: Beverage {
func prepare() -> String {
return "녹차 준비 완료"
}
}
Swift
복사
class FlexibleCafeCounter: BeverageProvider {
var beverage: Beverage
init(beverage: Beverage) {
self.beverage = beverage
}
func serveDrink() {
print(beverage.prepare())
}
}
let flexibleCounter = FlexibleCafeCounter(beverage: Americano())
flexibleCounter.beverage = GreenTea() // 음료 변경 가능
flexibleCounter.serveDrink()
Swift
복사
•
유연성 - 프로토콜을 통해 다양한 구현체를 쉽게 교체할 수 있습니다.
•
확장성 - 새로운 음료 타입을 추가하기 쉽습니다.
•
다형성 - 런타임에 다른 타입으로 교체 가능합니다.
•
테스트 용이성 - Mock 객체를 쉽게 만들 수 있습니다.
의존성 주입의 장점
•
테스트 용이성
// 테스트를 위한 Mock 객체 생성이 쉬워집니다
class MockBeverage: Beverage {
func prepare() -> String {
return "테스트용 음료"
}
}
let testCounter = BetterCafeCounter(beverage: MockBeverage())
Swift
복사
•
코드 재사용성 - 다른 프로젝트에서도 쉽게 재사용할 수 있습니다.
•
유지보수 용이성 - 의존성이 명확하여 코드 수정이 쉬워집니다.
•
결합도 감소 - 각 컴포넌트간의 의존성이 줄어듭니다.
실제 활용 예시
예를 들어 네트워킹 계층을 구현할 때
protocol NetworkService {
func fetch(url: URL, completion: @escaping (Result<Data, Error>) -> Void)
}
class APIManager {
private let networkService: NetworkService
init(networkService: NetworkService) {
self.networkService = networkService
}
func fetchUserData(userId: String) {
guard let url = URL(string: "https://api.example.com/users/\(userId)") else { return }
networkService.fetch(url: url) { result in
// 결과 처리
}
}
}
Swift
복사
이렇게 구현하면 실제 네트워크 호출, 테스트용 Mock 데이터 등 다양한 구현체를 쉽게 교체할 수 있습니다.