Swift / Dependency Injection

iOS 앱개발 공부

목록 보기
18/30

🧠 핵심 요약

DI (Dependency Injection)이란?

Dependency Injection, 즉 의존성 주입은 객체 지향 프로그래밍(OOP)에서 결합도를 낮추고 유연성을 높이기 위한 설계 패턴이다.

간단히 표현하자면, 어떤 객체 A가 다른 객체 B(의존성)를 필요로 할 때, A가 B를 직접 만들거나 찾아오는 대신, 외부(제 3자)에서 A에게 B를 공급(주입)해 주는 방식이다.

DI는 의존성 역전 원칙을 구현하는 가장 일반적인 방법이다.

  • Befor IoC(객체 스스로 제어): 객체 A가 필요한 의존성 B를 직접 생성(let b = B())하거나, 팩토리/싱글톤을 통해 직접 찾아감.
  • After IoC (외부에서 제어): 객체 A는 의존성 B가 필요하다고 선언만 하고, 객체 A를 사용하는 외부 시스템(주입자)이 B를 생성하여 A에게 전달.

🤔 DI는 왜 사용할까?

DI는 주로 다음과 같은 OOP 설계의 문제점들을 해결하기 위해 필수적으로 사용된다.

1) 결합도(Coupling) 감소

  • 문제: 객체 A가 의존성 B를 직접 생성하면, A는 B의 구체적인 구현 클래스(Concrete Class)에 묶이게 된다. (= 강한 결합)
  • DI 역할: DI를 사용하면 A는 B의 추상적인 규약(Protocol)에만 의존한다. 외부에서 B의 실제 구현체(BImpl)를 주입해 주므로, A는 B가 어떻게 구현되었는지 알 필요가 없다. (= 결합도 낮음)

2) 테스트 용이성(Testability) 증대

  • 문제: 객체 A가 복잡한 NetworkManager를 직접 생성하면, A를 테스트할 때마다 실제 네트워크 요청이 실행된다. (= 느리고 불안정)
  • DI 역할: DI를 통해 테스트 시에는 실제 NetworkManager 대신, NetworkManager의 프로토콜을 채택한 Mock 객체나 Dummy 객체를 주입할 수 있다. 이를 통해 A의 순수한 로직만 격리하여 빠르고 안정적으로 테스트가 가능하다.

3) 유연성 및 확장성 확보

  • 문제: 데이터베이스를 CoreData에서 Realm으로 변경하려면, CoreData를 직접 생성하던 모든 코드를 수정해야 한다.
  • DI 역할: DatabaseProtocol을 사용해 DI를 하면, 데이터베이스를 변경하더라도 데이터를 사용하는 객체(예: UserRepository)는 수정할 필요가 없다. 오직 주입을 담당하는 부분만 새로운 RealmRepositoryImpl로 교체하면 된다.

⚒️ DI를 어떻게 사용하면 될까? (구현 방법)

DI를 구현하는 방법은 여러가지가 있는데, 그 중 3가지 방법이 주로 사용된다.
각 방법의 구현방법을 익히고, 프로젝트의 성격에 따라 혼합하여 사용하면 된다.

1) Initializer Injection (생성자 주입)

객체를 생성하는 시점에 필요한 의존성을 생성장(Initializer)의 매개변수로 전달하는 가장 보편적이고 권장되는 방법이다.

  • 장점: 의존성이 객체의 생명주기 내내 존재함을 보장하므로 가장 안전하며, 강제적인 주입을 통해 누락성을 방지함.
// 예시

protocol UserRepository { 
	func fetchUser() 
}

class DefaultUserRepository: UserRepository { ... }

class UserViewModel {
	private let repository: UserRepository // 의존성 선언
    
    // 생성자를 통해 의존성 주입
    init(repository: UserRepository) {
    	self.repository = repository
    }
}

2) Property Injection (프로퍼티 주입)

객체를 먼저 생성한 후, 나중에 객체의 속성(프로퍼티)을 통해 의존성을 주입하는 방법이다.

  • 장점: 초기화 과정이 복잡한 UIKit 환경에서 유용함
  • 단점: 프로퍼티가 옵셔널일 수 있어, 사용 시점에 nil 체크가 필요하며 안전성이 낮음
// 예시

class ViewController: UIViewController {
	var viewModel: UserViewModel? // 프로퍼티를 옵셔널로 선언
    
    override func viewDidLoad() {
    	super.viewDidLoad()
        
        // 옵셔널 체이닝으로 viewModel의 유무 확인 및 메서드 실행
        viewModel?.fetchData()
    }
}

// 외부에서 의존성 주입
let vc = ViewController()
vc.viewModel = UserViewModel(repository: DefaultUserRepository())

3) Method Injection (메서드 주입)

객체의 생명주기 전체가 아닌 특정 메서드가 실행될 때만 일시적으로 의존성이 필요한 경우, 해당 메서드의 매개변수로 주입한다.

  • 장점: 의존성의 범위(Scope)가 메서드 실행 시간으로 국한되어 명확함.
// 예시

protocol Logger {
	func log(_ message: String)
}

class ReportGenerator {

	// 필요한 시점에만 주입받음
	func generate(with logger: Logger) { // 메서드 매개변수로 주입
    	// ... 리포트 생성 로직
    	logger.log("Report generation finished.")
    }
}

✒️ 결론

오늘은 DI 패턴이 무엇인지, 왜 사용하는지에 대해 알아보았다.
최근 클린아키텍처 패턴에서 DI 패턴을 사용하는 프로젝트를 진행 중이어서, 대략적으로만 이해하고 있던 DI 패턴에 대해 보다 명확히 알 필요성을 느껴 공부를 해보았는데, 모호했던 개념들이 정리되며 구조를 더 잘 이해하게 된 것 같다.

profile
이유있는 코드를 쓰자!!

0개의 댓글