의존성이란 특정 객체 A가 변할 때, 객체 B도 함께 변화함을 의미한다.
따라서 객체 서로 간 영향력을 행사하는 정도가 클수록 의존성이 크다.
의존성이 커질수록 코드 작성 중 발생하는 애로사항이 많아지는데, 이를 극복하기 위해 의존성 주입 패턴이 등장한다.
iOS에서 대표적으로 의존성과 밀접하게 등장하는 개념은 싱글톤 패턴 이라 할 수 있겠다.
싱글톤 패턴은 단 하나의 객체 인스턴스에 여러 객체가 동시에 접근하는 상황을 야기한다.
그래서 과도한 싱글톤 패턴의 남발은 테스트 주도 개발의 장애물이 될 수 있고, 객체 간 의존성을 복잡하게 증가시킬 수 있다.
아래 예시는 간략한 싱글톤 객체의 사용 형태이다.
뷰컨트롤러 내부에서 직접 Service
의 전역 인스턴스 shared
에 접근하고 있다.
이러한 뷰컨트롤러가 10개, 20개가 된다고 생각해보면 단 하나의 전역 인스턴스와 관계를 맺고 있는 객체가 얼마나 많아지고 복잡해질지 감이 온다.
핵심은 대체 왜 뷰컨트롤러가 Service
라는 객체 인스턴스를 생성하고, 그 책임을 갖고 있느냐에 있다.
// MARK: Singleton Object
struct Service {
static let shared = Service()
func curlData() {
// code
}
private init() {
// code
}
}
// MARK: Random Object
final class RandomViewController: UIViewController {
private let serviceManager = Service.shared
// code
override func viewDidLoad() {
super.viewDidLoad()
getData()
}
func getData() {
Service.shared.curlData() ...
}
}
의존성 주입이라는 표현이 되게 복잡한 표현이지만, 생각해보면 그 표현에 답이 있다(이렇게 당연한 말을?).
위 싱글톤 패턴 예시에서는 뷰컨트롤러 자신이 외부 객체 인스턴스에 직접 접근해서 그것을 소유하게 된다.
의존성 주입은 뷰컨트롤러가 외부 객체 인스턴스를 직접 소유하도록 하는 것을 방지하고 다른 방법으로 뷰컨트롤러에게 외부 객체 인스턴스를 제공한다.
이를 코드로 구현하는 일반적인 방법에는 3가지 정도가 있으며, 쓰기 편한 순서대로 예시 코드를 설명하고자 한다(주관적).
차례대로 속성 주입, 생성자 주입, 메소드 주입이다.
속성 주입 방식은 꽤 편리하지만 의존성의 변형이 일어날 수 있다.
var
로 선언되는 serviceManager
는 변수인 동시에 public
하기도 하기 때문.
다만 스토리보드를 쓴다면 아래에 소개할 생성자 의존성 주입 패턴 은 사용할 수 없다.
스토리보드로는 생성자를 내 마음대로 커스텀하여 구현할 수 없기 때문이다.
// MARK: 속성 주입방법
struct Service {
// code
init() { ... }
}
// MARK: UIViewController
final class RandomViewController: UIViewController {
var serviceManager: Service?
}
// MARK: 인스턴스 생성을 담당하는 외부 객체
let randomViewController = RandomViewController()
randomViewController.serviceManager = Service()
생성자 의존성 주입 패턴은 굉장히 쓰임이 많고 유연하다.
생성되는 과정 중에는 내가 원하는 조건이나 사전 설정이 가능하며, 이렇게 정의되는 속성과 메소드는 상수로 선언할 수 있다.
여기서부터 프로토콜이 가져다주는 이점이 동시에 활용될 수 있는데, 특정 프로토콜을 채택한 객체에 대해서만 의존성을 주입하는 방식의 응용이 가능하다.
예시에서는 Items
라는 프로토콜이 기준이 된다.
이 프로토콜을 채택하는 Beginners
는 요구사항인 weapon
과 armor
를 구체화하고 있다.
그후 Warrior
클래스는 Items
프로토콜을 채택한 equipment
라는 속성을 갖는데, 이 속성은 생성자를 통해 구체화 된다.
또한 이 클래스는 Items
프로토콜을 채택하는 인스턴스 생성 책임 객체가 된다.
이 equipment
가 의존 속성이 되며, 이것은 private let
하기 때문에 이 클래스가 한 번 생성된 후에는 변하지 않는다.
protocol Items {
var weapon: (String, Int) { get }
var armor: (String, Int) { get }
}
struct Beginners: Items {
var weapon: (String, Int) {
return ("Beginners Sword", 10)
}
var armor: (String, Int) {
return ("Beginners Armor", 15)
}
}
class Warrior {
private let equipment: Items
init(_ equipment: Items) {
self.equipment = equipment
print(self.equipment.weapon, self.equipment.armor)
}
}
let warrior = Warrior(Beginners())
마지막은 메소드 의존성 주입 방식이다.
위에서 언급한 생성자 의존성 주입 방식에서 생성자가 메소드로 바뀐 것이 전부다.
인스턴스 초기화 시점이 아닌 내가 원하는 시점에 의존성을 주입할 수 있다.
또한 어떤 아규먼트를 전달할 것인지도 조금 더 유연하게 결정할 수 있다는 장점도 있다.
class Warrior {
private var equipment: Items?
func setWarrior(_ equipment: Items) {
self.equipment = equipment
}
}
let warrior = Warrior()
warrior.setWarrior(Beginners())
Effective Swift <- 일독 강력 추천
Dependency Injection in Swift
Dependency Injection Tutorial for iOS: Getting Started
220823