[TIL] Dependency Injection(의존성 주입)

Valse·2022년 8월 23일
0

iOSInterViewController

목록 보기
3/8
post-thumbnail

의존성 주입

의존성이란 특정 객체 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는 요구사항인 weaponarmor를 구체화하고 있다.
그후 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())

정리

  1. 싱글톤 패턴의 과도한 의존성 커플링을 해제할 수 있는 방안으로 의존성 주입 패턴을 활용할 수 있다.
  2. 의존성 주입 패턴은 특정 객체 내부에서 타 객체의 인스턴스를 생성하지 않고, 외부에서 인스턴스를 생성하여 주입하는 방식으로 의존성을 주입하는 패턴을 의미한다.
  3. 의존성 주입 패턴에는 대략 3가지 방법이 있는데 각각 속성 주입, 생성자 주입, 메소드 주입 방식이다. 생성자 주입 방식은 스토리보드로 UI를 구성할 때는 활용할 수 없다.
  4. 의존성 주입 패턴을 활용하여 코드의 재사용성과 유연성, 테스트 용이성을 향상할 수 있다.
  5. 의존성을 주입하는 컨테이너는 앱 내의 모든 객체 간 의존성을 담당하게 된다.

참고자료

Effective Swift <- 일독 강력 추천
Dependency Injection in Swift
Dependency Injection Tutorial for iOS: Getting Started

220823

profile
🦶🏻🦉(발새 아님)

0개의 댓글