Dependency) 의존성 관리란?

Havi·2021년 5월 21일
3

Dependency

목록 보기
1/1

Swinject 적용을 검토하게된 이유

회사에서 코드를 작성하다보면 다음의 코드가 반복적으로 들어가게 된다.

// SomeViewModel.swift
private let provider: ServiceProviderType
    
init(provider: ServiceProviderType) {
    self.provider = provider
}

이 코드가 필요한 이유에 대해 생각해보았다.

// SomeViewModel.swift
private let provider: ServiceProvider = ServiceProvider()

// provider.doSomething()

원래는 SomeViewModelServiceProvider에 의존성(Dependency)을 가지고 있었다.

하지만 ServiceProviderType이라는 프로토콜로 의존성을 역전시켰고(ioc),
ServiceProvider를 외부에서 주입받아(Dependency Injection)
결합도가 낮게함으로써 Testable하고 Refactorable한 코드를 작성할 수 있다.

하지만 위 코드처럼 매번 viewmodel을 생성할 때마다 provider를 주입시켜주는 것은 여간 귀찮은 일이 아닐 수 없었다.
개발자는 역시 귀찮은 것을 싫어한다.

따라서 더 쉽고 간편하게 의존성을 관리 할 수 있는 방법이 있을까?

를 찾아보던 중 Swinject라는 것을 알게되었고, 회사 프로젝트에 적용해보고자 본 포스팅을 작성하게 되었다.

그리하여

  1. Dependency란 무엇이고
  2. ioc란 무엇이며
  3. 왜 Dependency Injection을 해야하고
  4. 어떻게 해야 의존성을 잘 관리할 수 있는가?
  5. Swinject는 어떻게 동작하고,
  6. Swinject를 어떻게 잘 활용할 수 있을까?

순으로 의존성 관리에 대해 다시 한번 복습한 뒤에, Swinject에 대해 알아보고자 한다.

의존성이란 ?

class SomeNetworkService {
    func fetchData() {
        print(#function, "이 먼저 실행되어야함")
    }
}

class SomeViewModel {
    let dependency = SomeNetworkService()
    
    // dependency.fetchData함수를 실행해야만 하는 함수
    func viewModelFunction() {
        dependency.fetchData()
        print("dependency에서 가져온 data를 business logic으로 처리")
    }
}

let viewModel = SomeViewModel()
viewModel.viewModelFunction()

위 예제를 보면 SomeViewModel class는 SomeNetworkService class를 인스턴스로 가지고 있다. 그 이유는 viewModelFunction에서 함수를 실행할 때 dependency.fetchData를 먼저 실행해서 네트워크 통신을 하여 데이터를 가져온 뒤에 비지니스 로직에 따라 데이터를 처리할 수 있기 때문이다.

따라서 SomeViewModel은 SomeNetworkService 없이는 함수를 실행할 수 없으므로 SomeNetworkService에 의존성을 가지고 있다.

이렇게 의존성을 가지게 될 경우, 테스트가 불가능하고, 강한 결합을 가지게 된다.
모듈화를 할 때 이러한 강한 결합때문에 에로사항이 생기게된다.

따라서 우리는 SOLID(객체 지향 설계) 에 D에 해당하는 Dependency Inversion Principle(의존관계 역전 원칙) 에 따라 의존관계를 역전시켜보겠다.

Dependency Inversion Principle(의존관계 역전 원칙)

객체 지향 프로그래밍에서 의존관계 역전 원칙은 소프트웨어 모듈들을 분리하는 특정 형식을 지칭한다. 이 원칙을 따르면, 상위 계층(정책 결정)이 하위 계층(세부 사항)에 의존하는 전통적인 의존관계를 반전(역전)시킴으로써 상위 계층이 하위 계층의 구현으로부터 독립되게 할 수 있다. 이 원칙은 다음과 같은 내용을 담고 있다.

  1. 상위 모듈은 하위 모듈에 의존해서는 안된다. 상위 모듈과 하위 모듈 모두 추상화에 의존해야 한다.
  2. 추상화는 세부 사항에 의존해서는 안된다. 세부사항이 추상화에 의존해야 한다.

이 원칙은 '상위와 하위 객체 모두가 동일한 추상화에 의존해야 한다'는 객체 지향적 설계의 대원칙을 제공한다

출처 : 위키백과

Inversion of Control(제어의 역전)

지금 SomeNetworkService는 추상화 되어있지 않다. 따라서 세부사항은 추상화에 의존해야하는 데 이 원칙을 지키고 있지않다. 다음과 같이 SomeNetworkService를 추상화 해보겠다.

protocol SomeNetworkServiceType {
    func fetchData()
}

class SomeNetworkService: SomeNetworkServiceType {
    func fetchData() {
        print(#function, "이 먼저 실행되어야함")
    }
    
    func configureSomeNetworkDetails() {
        // do something
    }
}

엥? 뭐가 달라진거임? 할 수도 있을거 같다.
필자는 SomeNetworkServiceType 프로토콜을 정의해줌으로써 SomeNetworkService가 맡아야할 책임을 정의해주고 있다.

이제 viewmodel 부분의 코드를 바꿔보겠다.

class SomeViewModel {
    let dependency: SomeNetworkServiceType = SomeNetworkService()
    
    // dependency.fetchData함수를 실행해야만 하는 함수
    func viewModelFunction() {
        dependency.fetchData()
        print("dependency에서 가져온 data를 business logic으로 처리")
    }
}

바뀐 부분은 dependency가 SomeNetworkServiceType이 되었다는 사실 뿐이다.
하지만 그로 인해 우리는 dependency.configureSomeNetworkDetails에 접근하지 못하고, dependency.fetchData에만 접근할 수 있게 되었다.

이 말은 즉슨 제어의 주체가 SomeNetworkService(class - 세부사항)에서 SomeNetworkServiceType(protocol - 추상화)로 바뀌었다.

이를 그림으로 그려보면 다음과 같이 의존관계의 방향이 달라지며 제어가 반전된 것을 확인할 수 있다.

하지만 제어의 주체가 바뀌었다고 해서 테스트가 가능해지거나 강한 결합이 느슨해지지는 않는다. 아직도 의존성은 SomeViewModel내에 존재한다. 따라서 의존성을 외부에서 주입시켜 주는 방법을 알아보도록 하자.

Dependency Injection

장점

Clint Jang님의 블로그를 참고했다.

확실하지는 않지만 여태까지 필자가 사용했을 때 여기서 핵심 장점은 테스트에 용이함재사용성을 높여줌, 결합도를 낮춰 유연성과 확장성을 향상시킴 인거 같다.

의존성을 주입하는 방법

DI를 하는 방법에는 3가지 방법이 있다.

Constructor Injection(생성자 주입)

class SomeViewModel {
    let dependency: SomeNetworkServiceType
    
    init(dependency: SomeNetworkServiceType) {
        self.dependency = dependency
    }
    
    // dependency.fetchData함수를 실행해야만 하는 함수
    func viewModelFunction() {
        dependency.fetchData()
        print("dependency에서 가져온 data를 business logic으로 처리")
    }
}

let dependency = SomeNetworkService()
let viewModel = SomeViewModel(dependency: dependency)

필자는 맨 위에 코드와 같이 SomeViewModel 객체를 생성할 때 dependency를 initializer로 주입해주는 방법을 주로 사용한다.

Property Injection(프로퍼티 주입)

class SomeViewModel {
    var dependency: SomeNetworkServiceType!
    
    // dependency.fetchData함수를 실행해야만 하는 함수
    func viewModelFunction() {
        dependency.fetchData()
        print("dependency에서 가져온 data를 business logic으로 처리")
    }
}

let dependency = SomeNetworkService()
let viewModel = SomeViewModel()
viewModel.dependency = dependency

스토리보드를 사용하는 경우 ViewController를 intantiate할 때 initializer를 통해 viewmodel을 주입할 수 없으므로 이런식으로 필드 인젝션을 한다고 알고있다.
하지만 이 방법은 개발자가 실수로 뷰모델을 주입하지 않으면 앱이 죽는다.
따라서 휴먼에러가 발생할 수 있는 위험한 방법이다.

수정: iOS13부터 사용할 수 있는 instantiateViewController(identifier:creator:)함수를 사용해 failable initializer를 생성해서 생성자 주입을 할 수 있다.

https://www.hackingwithswift.com/example-code/uikit/how-to-use-dependency-injection-with-storyboards

Method Injection(함수 주입)

class SomeViewModel {
    var dependency: SomeNetworkServiceType!
    
    // dependency.fetchData함수를 실행해야만 하는 함수
    func viewModelFunction() {
        dependency.fetchData()
        print("dependency에서 가져온 data를 business logic으로 처리")
    }
    
    func setUpDependency(_ dependency: SomeNetworkServiceType) {
        self.dependency = dependency
    }
}

let dependency = SomeNetworkService()
let viewModel = SomeViewModel()
viewModel.setUpDependency(dependency)

프로퍼티 주입과 비슷하게 메소드로도 주입할 수 있지만 실제로 사용해본 적은 없다.

결론

이렇게 의존성을 주입하게 되면 마침내 맨 처음에 본 코드와 같게 된다.

따라서 네트워크 테스트를 할 때 mockNetworkService를 생성하여 실제로 네트워크에 의존하지 않고 테스트를 진행해 볼 수 있다.

해당 내용과 관련된 내용으로 우아한 형제들 네트워크 테스팅 블로그에 자세하게 나와있으니 참고하기 바란다.

이제 의존성 주입까지 알아보았으니, 의존성을 잘 관리하기 위해 DIContainer 개념에 대해 알아보도록 하겠다. (다음 글에서)

레퍼런스
공식 깃헙
싱글톤 문제점
DI using Factories
네트워크 테스트 + swinject 블로그
Gaki님 블로그
Clint Jang님 블로그
은진님 블로그
민소네님 블로그
what a nice developer님 블로그
해외 유튭
레이웬더리치
전수열님 swinject, reactorkit 예제

profile
iOS Developer

1개의 댓글

comment-user-thumbnail
2022년 4월 4일

도움이 되었습니다. 잘봤어요~

답글 달기