DI 컨테이너 알아보기

권승용(Eric)·2025년 1월 23일

TIL

목록 보기
37/38

배경

  • Inversion of Control Container, 또는 DI Container의 모던한 개념은 마틴 파울러가 시작.
  • 그러나 그 이전에도 Inversion of Control Container라는 용어는 쓰이고 있었다.
  • 마틴 파울러 블로그를 참고해 어떤 개념인지 살펴보고, 어떻게 구현하면 좋을지 생각해보자.

왜 의존성 주입이 필요할까? 간단한 예제

func moviesDirectedBy(_ arg: String) -> [Movie] {
    var allMovies = finder.findAll()
    var iterator = allMovies.makeIterator()

    while let movie = iterator.next() {
        if movie.director != arg {
            if let index = allMovies.firstIndex(where: { $0 === movie }) {
                allMovies.remove(at: index)
            }
        }
    }
    
    return allMovies
}
  • 위 MovieLister 함수는 finder 객체에게 그것이 알고 있는 모든 영화들을 가져오라고 시킨다.
  • 우리가 원하는 것은 moviesDirectedBy 메소드는 영화들이 저장되어 있는 방식과 독립적인 것.
  • 따라서 moviesDirectedBy 메소드는 단지 finder만을 참조하고, 그 finder는 단지 findAll 메서드에 어떻게 응답할지 알고 있기만 하면 됨.
  • 이를 명확히 하기 위해 finder를 위한 인터페이스 정의 가능
protocol MovieFinder {
	func findAll() 
}
  • 이제 분리는 완료되었지만, 결국 어떤 시점에서는 실제로 영화를 가져오는 물리적인 구현체를 만들어야 함.
  • 이 작업을 MovieLister 클래스의 생성자에 구현해보자.
class MovieLister {
	private let finder: MovieFinder

	init() {
		// 실제 finder 클래스의 구현체
		self.finder = ColonDelimitedMovieFinder("movies1.txt")
	}

	//...
}
  • 클래스를 혼자 사용하면 문제없음.
  • 그러나 다른 개발자가 MovieLister 클래스를 재사용하고 싶다면?
  • 그리고 동시에 영화 목록을 완전히 다른 방식으로 저장하고 있다면?
    * SQL, XML 파일, 웹 서비스, 다른 형식의 텍스트 파일 등...
  • 이 경우엔 데이터를 가져오기 위한 다른 클래스가 필요함.
  • MovieFinder이라는 인터페이스가 있기 때문에, 해당 인터페이스만 준수하면 moviesDirectedBy 메서드에 영향을 미치지는 않을 것.
  • 그러나 적절한 finder 구현체의 인스턴스를 적재하는 방법이 필요함.

  • 위 이미지는 이 상황에서의 의존성을 보여준다.
  • MovieLister 클래스는 MovieFinder 인터페이스와 구현체 모두를 의존한다.
  • 인터페이스만 의존하면 좋겠지만, 그러면 인스턴스는 어떻게 생성해야 하는가? 결국 어디선가 생성해 주어야 한다.
  • 마틴 파울러의 책에서는 이러한 상황을 Plugin 이라고 설명한다.
    MovieFinder의 구현 클래스는 컴파일 시점에 프로그램에 연결되지 않는다. 어떤 구현을 사용할 지 모르기 때문.
    대신, MovieLister가 어떤 구현과도 동작할 수 있기를 원하며 그 구현이 나중에 어떤 시점에서든 연결될 수 있기를 원한다.
  • 핵심은 MovieLister가 구현체를 모르는데 어떻게 그 인스턴스와 통신하여 작업을 수행할 수 있도록 하는지이다.
  • 그리고 어떻게 이러한 Plugin 상황을 앱을 조립할 것인지이다.
  • 보통 Inversion of Control, 제어의 역전으로 해결한다.

제어의 역전 - IoC

  • 서로 다른 계층의 컴포넌트를 조립할 수 있는 일반적인 기능을 제공하는 프레임워크들을 lightweight containers라고 부르고, PicoContainer / Sprint 등의 프레임워크들이 있다.
  • 요런 컨테이너들도 Inversion of Control을 구현한다.
  • 컨테이너들이 사용하는 접근 방식은 플러그인의 사용자가 특정 규칙을 따르도록 하여, 별도의 조립 모듈이 MovieLister에 구현을 주입할 수 있도록 하는 것.
  • 마틴 파울러는 이러한 접근 방식에 구체적인 이름이 필요하다고 생각, Dependency Injection 이라는 용어를 결정함.
    제어의 역전은 프레임워크의 일반적인 특징을 의미할 수도 있음.
    ex) 전통적인 CLI 프로그램은 개발자가 입력 프롬프트를 제어했지만, GUI 프레임워크는 이벤트 루프를 프레임워크가 관리하고, 개발자는 이벤트 핸들러를 작성함.
    라이브러리는 사용자 코드가 라이브러리를 호출하지만, Control이 반전되면 프레임워크가 사용자 코드를 호출함.
    따라서 "프레임워크에 IoC를 적용했어요" 는 "내 차에 바퀴를 적용했어요" 같은 의미가 될 수 있음
    * 따라서 구체 인스턴스 주입을 의미하기 위한 구체적인 용어가 필요했고, 그것이 Dependency Injection
  • DI가 플러그인 구현으로의 의존성을 제거하는 유일한 방법은 아님 - 다른 패턴으로 서비스 로케이터가 있음

의존성 주입의 형태들

  • DI의 기본 개념은 어셈블러라는 분리된 객체가 MovieFinder 인터페이스에 대한 적절한 구현을 MovieLister 클래스를 위해 채워주는 것.

  • 의존성 주입에는 세 가지 주요 스타일이 있다.
  • Constructor Injection, Setter Injection, Interface Injection
    • IoC에서는 type 1 IoC, type 2 IoC ... 등등으로 불렸지만 마틴 파울러는 외우기 힘들다고 새로 이름 붙였다.

PicoContainer를 사용하는 Constructor Injection

  • 마틴 파울러 동료들이 개발에 참여해서 PicoContainer 소개한 의존성 주입 소개
  • 생성자를 활용해 MovieFinder 클래스를 MovieLister 클래스에 주입함. 이를 위해 MovieLister 클래스는 필요한 의존성을 포함하는 생성자를 선언해야 함
class MovieLister {
    private let finder: MovieFinder

    init(finder: MovieFinder) {
        self.finder = finder
    }
}
  • MovieFinder 구현체인 ColonMovieFinder도 피코컨테이너에 의해 관리되며 파일 이름을 주입받음
class ColonMovieFinder: MovieFinder {
    private let filename: String

    init(filename: String) {
        self.filename = filename
    }
}
  • 아래와 같은 코드로 피코컨테이너를 설정함. 여기서 인터페이스와 구현체를 연결하고 파라미터를 주입함
func configureContainer() -> MutablePicoContainer {
    let pico = DefaultPicoContainer()
    let finderParams: [Parameter] = [ConstantParameter("movies1.txt")]
    pico.registerComponentImplementation(MovieFinder.self, ColonMovieFinder.self, parameters: finderParams)
    pico.registerComponentImplementation(MovieLister.self)
    return pico
}
  • 이 설정 코드는 일반적으로 별도의 클래스에 작성.
  • 아래 코드로 컨테이너를 사용한다.
func testWithPico() {
    let pico = configureContainer()
    let lister = pico.getComponentInstance(MovieLister.self) as! MovieLister
    let movies = lister.moviesDirectedBy("Sergio Leone")
    XCTAssertEqual("Once Upon a Time in the West", movies[0].title)
}
  • 피코컨테이너는 setter, constructor 주입 둘 다 가능하지만 생성자 주입을 선호함

Spring을 사용한 Setter Injection

  • spring도 생성자, 설정자 주입 둘 다 가능하지만 보통 설정자 주입 많이 사용
class MovieLister {
    private let finder: MovieFinder

    // 세터 메서드를 통해 의존성 주입
    func setFinder(finder: MovieFinder) {
        self.finder = finder
    }
}
  • 이렇게 세터 메서드로 의존성 주입하는 방법
  • 이 방법은 객체 생성 이후에도 의존성을 변경할 수 있는 유연성을 제공함

Interface Injection

  • 의존성 주입 요구사항을 가진 프로토콜(인터페이스)를 통해 주입하는 방식
  • 일반적으로 생성자, 세터 방식을 많이 사용하기 때문에 굳이 다루지는 않을 예정이다.

그래서 DI Container가 뭔데?

  • 링크한 마틴 파울러의 블로그 글에서, 의존성 분리를 위해 의존성을 주입할 수 있는 경량 컨테이너들이 여럿 있음을 확인할 수 있다.
  • 상위 레벨에서 의존성을 관리하고 구현체 생성 + 주입을 담당하는 객체가 바로 DI Container

왜 필요한건데?

  • DIP -> 의존성 역전을 활용해 구체 인스턴스가 아닌 인터페이스에만 의존하도록 해도, 문제점은 발생한다.
  • 어딘가에서 구현체를 생성해 주입해준다는 것.
  • 이 작업을 생성자를 호출하는 상위 객체에서 수행한다면, 주입할 구체 타입의 변경에 상위 객체도 함께 영향받는다.
  • 그 이유는 구현체를 결정해 주입해준다는 책임이 숨어있기 때문.
  • 따라서 그 책임을 분리해 DI Container, 의존성 주입 컨테이너로 이름짓고 관리하면 인터페이스에 대해 어떤 구현체를 주입할 것인지 결정하는 로직과 실제로 구현체를 생성하고 주입하며 객체들을 생성하는 로직을 담당하도록 하는 것.
  • 그러면 DI Container에서 사용되는 객체들은 자신이 의존하는 인터페이스에만 신경쓰면 되고, 무엇이 어디서 생성되어 주입되는지는 신경쓰지 않아도 된다.

Swift에서 DI Container를 구현하는 방법

final class DependencyContainer {
    private static var shared = DependencyContainer()
    
    private init() { }
    
    private var dependencies: [String: Any] = [:]

    static func register<T>(_ dependency: T) {
        shared.register(dependency)
    }

    static func resolve<T>() -> T {
        shared.resolve()
    }

    private func register<T>(_ dependency: T) {
        let key = String(describing: T.self)
        dependencies[key] = dependency as Any
    }

    private func resolve<T>() -> T {
        let key = String(describing: T.self)
        let dependency = dependencies[key] as? T

        precondition(dependency != nil, "\(key) Dependency가 없음")

        return dependency!
    }
}
  • 기본적인 DI 컨테이너는 위와 같이 구현 가능
    * 출처 - 정주는 개발 중
  • SwInject 등 외부 라이브러리 사용도 가능
  • 직접 구현 vs 외부 라이브러리 사용?
    직접 구현 -> 커스텀 가능성 높음, 가벼움, 포폴에 쓸 거 생김, 작업에 시간 들어감 + 러닝커브
    외부 라이브러리 사용 -> 커스텀 가능성 낮음, 무거움, 포폴에 쓸 거 적어짐, 러닝커브만 있음
profile
ios 개발자에용

0개의 댓글