의존성 주입이란?
iOS 테스트 코드를 공부하며 상당히 많이 접했던 단어입니다. "의존성 주입" 은 무엇일까요?
이론적인 개념은 다음과 같습니다.
```
소프트웨어 엔지니어링에서 의존성 주입(dependency injection)은 하나의 객체가 다른 객체의 의존성을 제공하는 테크닉이다. 클라이언트가 어떤 서비스를 사용할 것인지 지정하는 대신, 클라이언트에게 무슨 서비스를 사용할 것인지를 말해주는 것이다. "주입"은 의존성(서비스)을 사용하려는 객체(클라이언트)로 전달하는 것을 의미한다.
```
출처: https://ko.wikipedia.org/wiki/%EC%9D%98%EC%A1%B4%EC%84%B1_%EC%A3%BC%EC%9E%85
대부분의 프로그램은 객체들의 조합인 Composition 으로 이루어집니다. 의존성 주입은 객체가 서로 의존 관계에 있을 때 "직접 생성하는" 것이 아니라 "제공 받는" 형태를 말합니다. 이를 예시 코드로 살펴보겠습니다.
MVC 패턴에서 주로 ViewController에서 Network 관련 객체를 생성하고, 이를 토대로 로직을 구성하게 됩니다. 이를 Dependency Injection ( 의존성 주입 ) 으로 바꾸면 다음과 같습니다.
NetworkProtocol 를 구현하고 NetworkProvider 에서는 해당 프로토콜을 채택합니다.
ViewController 에서는 이니셜라이저를 통해 필요한 의존성을 주입 받게 됩니다.
ViewController 에서는 더 이상 NetworkProvider 가 아닌 NetworkProtocol 를 가르키게 됩니다.
따라서 NetworkProtocol 를 채택한 어떤 객체도 주입 가능하게 됩니다.
이러한 프로토콜을 통한 간접화는 의존성을 쉽게 치환할 수 있습니다.
또한 해당 프로토콜을 준수한 Mock 객체를 만들어서 주입하면 쉽게 테스트 환경을 만들 수 있습니다.
의존성 주입은 좋은 방식인가? 장점은 무엇일까
Uber 에서 만든 Needle 프레임워크는 예시와 함께 의존성 주입의 장점에 대해서 설명합니다.
사진을 검색하는 앱을 만든다고 가정해보겠습니다. PhotosViewController는 PhotosService를 통해 서버에서 사진을 가져와서 보여주게 됩니다.
이때 PhotosService는 내부에서 생성하도록 구현되어 있습니다.
위와 같은 강한 커플링 구조는 다음과 같은 이슈를 만들 수 있습니다.
1. PhotosService의 코드를 수정하려면 PhotosViewController 또한 코드를 수정해야 합니다.
( 두개의 클래스에서는 괜찮아 보이지만 실제 수백개의 클래스가 있는 현업 앱에서는 앱 반복을 늦추게 됩니다. )
* 필자는 개발 주기로 이해했습니다.
2. PhotosService를 교체하려면 PhotosViewController 또한 코드를 수정해야 합니다.
( 성능을 개선한 PhotosServiceV2 를 사용하려 할 때 PhotosViewController 코드를 일일히 파악하고 수정해야 합니다. )
3. PhotosService의 호출 없이 PhotosViewController 유닛 테스트가 불가합니다.
( 내부에서 의존성을 생성하므로 테스트시에 주입이 불가합니다. )
4. PhotosViewController 와 PhotosService 를 동시에 개발할 수 없습니다.
( 작은 규모의 앱에서는 큰 문제가 아닌 것 처럼 보일 수 있지만 실제 팀에서는 엔지니어가 끊임없이 막힐 수 있습니다. )
* 의역이 있을 수 있습니다. 자세한 내용은 링크를 참고해주시면 감사하겠습니다.
https://github.com/uber/needle/blob/master/WHY_DI.md
의존성 주입은 testable 한 코드와도 무척 연관이 깊습니다. 어떤 객체가 다른 객체와 연관이 있을 때
이를 여러가지 방법(이니셜라이저, 프로퍼티, 메서드)를 통해 외부에서 주입할 수 있는 방식을 말합니다.
의존성 주입은 위와 같이 협업에서 발생할 수 있는 이슈나 유닛 테스트시에 발생하는 문제를 해결합니다.
의존성 주입의 단점
의존성 주입을 했을 때에는 결합도(coupling) 가 적다고 표현할 수 있습니다.
이때 낮은 결합도를 가진 구조는 구조화가 잘 되어있다고 표현할 수 있으며 좋은 설계로 표현될 수 있습니다. 이와 반대로 높은 결합도를 가진 구조는 높은 가독성과 유지보수성을 가진다고 할 수 있습니다.
따라서 의존성 주입 패턴은 생성자가 다른 코드에 위치하기 때문에 상대적으로 낮은 가독성을 가질 수 있다는 것이 단점입니다.
* 유지보수성의 경우에는 팀원이 많을 수록 늘어난다고 생각합니다.
의존성은 어디에서 주입되는가?
의존성 주입은 의존 관계에 있는 객체를 직접 생성하는 것이 아닌 제공받는것이라고 개념을 정리했습니다.
그러면 iOS 앱에서는 의존성을 어디에서 주입해야할까요? 크게 2가지 방식이 있습니다.
1. DI Container
의존성을 관리해주는 별도의 컨테이너를 사용하는 방식을 말합니다. 대표적으로 Swinject 라는 프레임워크가 있습니다.
DI Container는 등록하는 register 와 의존성을 풀어내는 resolve 를 지원합니다. iOS 에서는 앱 시작점 (AppDelegate)에서 필요한 의존성들을 register 하고 필요한 부분에서 resolve 하여 사용합니다.
주로 DI Container 는 싱글톤으로 구현되는 경우가 많기 때문에 어떤 지점에서도 접근이 가능합니다. 따라서 필요한 의존성을 DI Container 를 통해 직접 생성하거나 이전 뷰 컨트롤러에서 resolve 하여 주입하는 방식을 사용합니다.
2. Pure DI
DI Container 를 사용하지 않고 모든 의존성을 Compostion Root 에서 관리하는 방식을 말합니다. iOS 에서 Compostion Root 는 AppDelegate 에 해당됩니다.
Compostion Root 에서는 의존성 트리를 구성하고 Factory 형식으로 하위 객체에 필요한 의존성을 모두 제공하게 됩니다. 클로저로 이어지는 이니셜라이저 Depth 를 줄이기 위하여 Pure 라는 프레임워크를 사용할 수 있습니다.
정리
포스팅을 작성하며 의존성 주입은 iOS 뿐만 아니라 소프트웨어 공학에서 널리 사용되는 개념임을 알게 되었습니다. 따라서 개발자에 따라서 이해도나 관점이 다르기 때문에 더 많은 레퍼런스를 찾아보는 것이 좋겠다는 생각을 했습니다.
더 학습하며 잘못된 내용은 수정하고 추가할 내용은 덧붙여서 게시글을 계속 업데이트할 예정입니다.
참고 자료
'iOS Dev > iOS' 카테고리의 다른 글
정적 분석으로 iOS 앱 구조 파악하기 (0) | 2022.12.03 |
---|---|
8개월간 같은 iOS 앱을 리팩토링하며 배운점 (6) | 2021.11.20 |