이번 글에서는 DI와 DIP에 대해서 정리해보고자 합니다.
Dependency Injection & Dependency Inversion Principal
- 결합도를 느슨하게 되도록하고 의존관계 역전 원칙과 단일 책임 원칙을 따르도록 클라이언트 생성에 대한 의존성을 클라이언트의 행위로 부터 분리하는 것
- 기존의 의존성을 개선해 의존성을 외부에서 주입하려고 함
- DI: collection of Patterns, Techniques
- DIP: Guidline on how to create loosly coupled and easy to maintain software
여기까지 키워드들에 대해서 간단히 적어봤구요, 좀 더 들어가보겠습니다.
우선..
의존한다?
라는 의미부터 풀어보죠.
A가 B에 의존한다는 것은? B의 상태에 따라 A도 영향을 받는다. 라는 것입니다.
class User {
var feature = UserInfo()
func 아이템사용() -> String {
feature.아이템장착()
}
}
class UserInfo {
private let 아이템 = 아이템더미()
func 아이템선택() -> String {
아이템.물풍선() + 아이템.천사() + 아이템.부스터()
}
}
class 아이템더미 {
func 물풍선() -> String {
"물풍선 사용"
}
func 천사() -> String {
"천사 사용"
}
func 부스터() -> String {
"부스터 사용"
}
}
어렸을 때 했던 게임을 떠올리며 작성해본 코드입니다. . ㅎ 추억이네요 암튼!!
User, UserInfo, 아이템더미 까지 세 개의 모듈을 구현했는데요, 왼쪽으로 갈수록 상위모듈입니다.
그렇다면 아이템더미의 항목이 수정된다면 어떻게 될까요?
예를 들어 물풍선 메서드가 바나나 메서드로 말이죠
그럼 UserInfo에 컴파일 에러가 뜨겠죠?
위와 같이 하위 모듈이 변할 때, 상위 모듈까지 바뀌어야하는 경우, 상위 모듈이 하위모듈에 의존하고 있는 것입니다.
이러한 상황은 서로 강한 결합을 가진상태인데, 이는 테스트를 복잡하게 하거나 불가능하게 하기도 합니다.
이를 예방하기 위해 구현체에 의존하지 않고 추상화에 의존해서 결합을 느슨하게 만들어주려고 합니다.
1. 프로토콜 정의하기
protocol 아이템프로토콜 {
func 물풍선() -> String
func 천사() -> String
func 부스터() -> String
}
- 아이템더미의 메서드 기능들을 프로토콜로 추상화합니다.
2. 아이템더미가 프로토콜을 따르도록 변경
class 아이템더미: 아이템프로토콜 {
func 물풍선() -> String {
"물풍선 사용"
}
func 천사() -> String {
"천사 사용"
}
func 부스터() -> String {
"부스터 사용"
}
}
3. UserInfo 클래스에 DI 적용
class UserInfo {
private let 아이템: 아이템프로토콜
init(아이템: 아이템프로토콜) {
self.아이템 = 아이템
}
func 아이템선택() -> String {
return 아이템.물풍선() + 아이템.천사() + 아이템.부스터()
}
}
4. User 클래스에서 DI 적용
class User {
var feature: UserInfo
init(feature: UserInfo) {
self.feature = feature
}
func 아이템사용() -> String {
return feature.아이템선택()
}
}
5. 사용 예시
let 아이템 = 아이템더미()
let userInfo = UserInfo(아이템: 아이템)
let user = User(feature: userInfo)
print(user.아이템사용())
여기가지는 코드로 before & after로 비교해보았구요
다음은 실제 제가 구성한 프로젝트를 도식화한 것을 보여드리려고합니다.
최초로 제가 구현한 코드의 도식을 보면
ViewModel내에 RealmRepository 인스턴스를 생성해놓은 상태에서 그 안에 있는 메서드를 사용했습니다.
램을 핸들링하는 메서드가 다 레포지토리 안에 들어있었기 때문에 코드를 수정을 하게되는 경우엔 두 개의 모듈을 다 돌면서 코드를 수정해야 할 수밖에 없던 것이죠.
이러한 불편함을 해소하고자 상위모듈인 ViewModel과 하위모델인 RealmRepository가 모두 의존할 수 있는 추상화(프로토콜)을 구현해 코드를 구성하고자 했습니다.
위와 같은 그림으로 코드를 구현하게 되면 RealmRepository가 아무리 바뀌어도 컴파일 시점에서 ViewModel에서 에러날 일은 없습니다.
덕분에 좀 더 testable한 코드를 구현할 수 있고, 결합상태를 loose하게 만들 수 있는 것이죠!
제 코드를 통해 풀어내는 작업은 후에 프로젝트 회고를 통해 딥하게 풀어보겠습니다.
여기까지 내용을 풀어봤을때 DI, DIP의 장점은?
- 같은 추상화에 의존하고 있다면 모듈을 쉽게 교체 가능
- 단위 테스팅, 마이그레이션(DB교체.이동)
- 코드 추론이 더 쉬워짐
그렇다고 무조건 좋기만 한 것은 아니겠죠?
단점을 확인해보면
- 모듈이 하나 더 생기는 것이니 Cost.Complexity 증가
- 오류가 런타임 때 일어나기에, 컴파일 시점에서 종속성 주입에 관한 에러를 찾아내기 힘듦
정도가 있기에 무조건 좋다? 라고 하기엔 무리가 있으니 이 아키텍쳐가 과연 코드를 구성하는데 도움이 될지 고민해 볼 부분이 있는 것 같습니다.
예시와 함께 글을 적다보니 좀 길어졌네요.
이번글은 여기까지~!!
.
.
.
.
'Swift' 카테고리의 다른 글
[iOS] About Optional(옵셔널) (0) | 2024.10.01 |
---|---|
[iOS] About Copy-On-Write(COW) (0) | 2024.09.28 |
[iOS] About Realm (1) | 2024.09.25 |
[iOS] About 메모리 구조 (1) | 2024.09.21 |
[iOS] About StateObject, ObservedObject (0) | 2024.09.18 |