[디자인 패턴] DI(Dependency Injection) Pattern (2) - DI Pattern 사용법

전성훈·2023년 11월 17일
0

DesignPattern

목록 보기
18/18
post-thumbnail

주제: 객체가 자신이 의존하는 다른 객체들을 받아 사용하는 방법


"의존성 주입은 25달러짜리 용어로 5센터짜리 개념이다." - 제임스 쇼어

DI(Dependency Injection)이란

  • 객체가 실행 시간에 자신의 의존성을 외부로부터 받아들이도록 설계하는 방법입니다.
  • 이 패턴은 코드의 결합도를 낮추고, 테스트 용이성을 향상시키며, 코드의 재사용성을 증가시키는 데 도움을 줍니다.
  • 여러 방법으로 Dependency Injection을 구현할 수 있는데, 대표적으로 Initializer Injection, Setter Injection(Property Injection), Interface Injection 그리고 DI Container 이 있습니다.
  • 의존성 주입 컨테이너(DI Container)는 모든 의존성을 기록, 결정, 설정하는 책임을 가진 객체이며, 결정과 해결의 책임이란 DI 컨테이너가 생성자 인자와 객체 간의 관계를 알아야 한다는 의미입니다.

패턴의 구조

DI패턴

  • 의존성(Components)
    • 어떤 클래스 또는 모듈이 다른 클래스 또는 모듈에 의존할 때, 이를 의존성이라고 합니다.
  • 주입자(Injector)
    • 의존성을 주입하는 역할을 하는 클래스 또는 모듈입니다. 이는 종종 DI 컨테이너라고 불리며, 의존성을 생성하고, 이를 필요로 하는 객체에 연결하는 역할을 합니다.
  • 클라이언트(Client)
    • 의존성이 주입되는 클래스 또는 모듈입니다. 클라이언트는 자신의 의존성을 직접 생성하지 않고, 주입자를 통해 받습니다.

패턴의 장단점

장점

  • 결합도 감소
    • 객체가 자신의 의존성을 직접 생성하지 않고 외부에서 받기 때문에, 결합도가 낮아지고, 코드의 유연성이 향상됩니다.
  • 테스트 용이성
    • 의존성을 외부에서 주입받기 때문에, 테스트 시에 mock 객체와 같은 대체 의존성을 쉽게 주입할 수 있어 테스트가 용이해집니다.
  • 재사용성 및 확장성 향상
    • 의존성을 주입할 수 있기 때문에, 동일한 객체를 다양한 컨텍스트에서 재사용할 수 있고, 새로운 기능을 추가하거나 변경하기도 쉽습니다.

단점

  • 복잡성 증가
    • 처음에 DI 패턴을 도입하면, 설정 및 관리할 부분이 많아져서 시스템의 복잡성이 증가할 수 있습니다.
  • 러닝 커브
    • DI와 관련된 개념과 도구들을 익히는데 시간이 필요하며, 이해하지 못하고 사용하면 오히려 문제를 일으킬 수 있습니다.

예시코드

Initializer Injection

  • Initializer Injection은 의존성을 주입하는 가장 선호되는 방법 중 하나입니다. 이 방법에서는 모든 의존성을 초기화 매개변수로 전달합니다.
  • 처음 코드를 보았을 때 클래스가 무엇을 필요로 하는지 쉽게 이해할 수 있으며, 모든 의존성을 볼 수 있습니다.
  • 아래 예제에서는 ViewModel이라는 클래스를 생성했습니다. ViewModel의 초기화 메소드는 두 개의 인자를 받아들이며, 이 인자들은 두 개의 객체를 변경합니다.
  • UserServiceProtocol과 RewardServiceProtocol은 서비스이며, ViewModel은 위에서 언급한 설명대로 클라이언트 입니다.
protocol UserServiceProtocol {
    func fetchUsers()
}

protocol RewardServiceProtocol {
    func fetchRewards()
}

final class ViewModel {

    private let userService: UserServiceProtocol
    private let rewardService: RewardServiceProtocol

    init(userService: UserServiceProtocol, rewardService: RewardServiceProtocol) {
        self.userService = userService
        self.rewardService = rewardService
    }

}

Setter Injection

  • Setter Injection은 의존성 주입을 위해 setter method를 사용합니다.
  • Setter Injection은 몇몇의 의존성을 optional처리를 필요로할때 유용합니다.
  • 하지만 의존성에대해 잊어버리기 쉬우니 주의가 필요합니다.
protocol UserServiceProtocol {
    func fetchUsers()
}

protocol RewardServiceProtocol {
    func fetchRewards()
}

final class ViewModel {

    private var userService: UserServiceProtocol?
    private var rewardService: RewardServiceProtocol?

    func setUserService(userService: UserServiceProtocol) {
        self.userService = userService
    }

    func setRewardService(rewardService: RewardServiceProtocol) {
        self.rewardService = rewardService
    }
}

Interface Injection

  • Interface Injection은 클라이언트가 의존성 프로토콜을 채택함으로 사용합니다.
protocol UserServiceProtocol {
    func fetchUsers()
}

protocol RewardServiceProtocol {
    func fetchRewards()
}

protocol ServiceProtocol {
    func users(service: UserServiceProtocol)
    func rewards(service: RewardServiceProtocol)
}

final class ViewModel: ServiceProtocol {
    
    private var userService: UserServiceProtocol?
    private var rewardService: RewardServiceProtocol?
    
    func users(service: UserServiceProtocol) {
        self.userService = service
    }
    
    func rewards(service: RewardServiceProtocol) {
        self.rewardService = service
    }   
}

DI Container 사용

  • 원칙적으로 많은 다른 객체와 많은 의존성을 가지고 있는 경우 의존성 주입 컨테이너를 사용하는 것이 올바른 방법이 될 수 있습니다.
protocol DIProtocol {
    func register<Service>(type: Service.Type, service: Any)
    func resolve<Service>(type: Service.Type) -> Service?
}
  • 우선, 두 가지 주요 함수가 있고, 이 두 함수는 이름과 같은 역할을 가지고 있습니다.
final class AppDIContainer: DIProtocol {
    static let shared = AppDIContainer()
    
    private init() { }
    
    var services: [String: Any] = [:]
    
    func register<Service>(type: Service.Type, service: Any) {
        services["\(type)"] = service
    }
    
    func resolve<Service>(type: Service.Type) -> Service? {
        return services["\(type)"] as? Service
    }
}
  • 의존성 주입 컨테이너를 싱글톤으로 만들면 의존성 주입 컨테이너 클래스의 여러 인스턴스를 강제로 사용하는 것을 방지하고, 의존성 일부가 누락되는 등의 예측할 수 없는 동작을 방지할 수 있습니다.
  • services는 모든 서비스를 보유하고 있는 String-Any 타입의 딕셔너리입니다. 딕셔너리의 String 키는 서비스의 이름을 나타내고, Any 값은 서비스 인스턴스를 참조합니다.
  • Service는 제네릭 타입입니다. 밀접하게 결합된 코드를 방지하기 위해, 프로토콜만 사용할 예정입니다.
protocol UserServiceProtocol {
    func fetchUsers()
}

protocol RewardServiceProtocol {
    func fetchRewards()
}

final class UserService: UserServiceProtocol {
    func fetchUsers() {
        print("User fetching")
    }
}

final class RewardService: RewardServiceProtocol {
    func fetchRewards() {
        print("Reward fetching")
    }
}

final class ViewModel {
    private let userService: UserServiceProtocol
    private let rewardService: RewardServiceProtocol
    
    init(
        userService: UserServiceProtocol = AppDIContainer.shared.resolve(type: UserServiceProtocol.self)!,
        rewardService: RewardServiceProtocol = AppDIContainer.shared.resolve(type: RewardServiceProtocol.self)!
    ) {
        self.userService = userService
        self.rewardService = rewardService
    }
    
    func fetchUsers() {
        userService.fetchUsers()
    }
    
    func fetchRewards() {
        rewardService.fetchRewards()
    }
}
  • 마지막으로, 의존성 주입 컨테이너 클래스를 사용할 준비가 되었습니다.
  • ViewModel 클래스를 호출하기 전에, 정의한 서비스를 등록해야합니다.
let container = AppDIContainer.shared

container.register(type: UserServiceProtocol.self, service: UserService())
container.register(type: RewardServiceProtocol.self, service: RewardService())

let viewModel = ViewModel()

viewModel.fetchUsers()
viewModel.fetchRewards()


// User fetching
// Reward fetching

출처(참고문헌)

제가 학습한 내용을 요약하여 정리한 것입니다. 내용에 오류가 있을 수 있으며, 어떠한 피드백도 감사히 받겠습니다.

감사합니다.

0개의 댓글