Swift

[Swift] DI: 의존성 주입 (Dependency Injection) in iOS/Swift

나무는tree 2021. 11. 9. 13:48
DI, 의존성 주입
주니어 개발자가 정말 많이 접하는 단어이며, 개념이 추상적이어서 머리속에 개념이 확립되지 않는 것. - 나무

실제로 개발자로 취업하기 전부터 DI에 대해 많이 찾아보았고, 이해하려 노력하였다.

검색을 하다보면 DI의 장점들이 적혀있다.

  1. 모듈간의 결합도를 낮춘다
  2. 유지보수가 용이하다
  3. 테스트에 용이하다

등등 많은 장점들이 나오고 "이런 장점이 있기 때문에 사용하는구나" 정도로 이해하였다.

 

그러다가 한 영상을 찾게 되었고, 영상을 통해 느낀점을 적어보려 한다.

 

의존성 주입 (Dependency Injection)

객체에게 필요한 의존성을 주입하는 행위

-> 어떤 객체가 필요한 것들을 전달하는 행위

(화살표는 내가 쉽게 이해하기 위해 바꾼 문장이다.)

 

전달한다 = 누군가에게 받는다 = 나에게는 없는 것을 받는다

결국, 필요한 것들을 내가 직접 생성하는 것이 아닌, 다른 누군가에게 전달받는다

 

Example

class Car {
	var handle: Handle?
}
// 1. Car Class 내부에서 직접 생성하는 경우

class Car {
	var handle: Handle? = Handle()
}
// or
class Car {
	var handle: Handle?
    
    init(){
    	self.handle = Handle()
    }
}

// 2. Car Class 내부에서 직접 생성하지 않고 전달받는 경우

class CarFactory {
	var car: Car = Car()
    
	func setHandle() {
		self.car.handle = Handle()
    }
}

이런 식으로 의존성을 주입한다. (끝)

그래서 의존성 주입에 대한 정의는 알겠는데, 그래서 이걸 써서 얻는 장점인

  1. 모듈간의 결합도를 낮춘다
  2. 유지보수가 용이하다
  3. 테스트에 용이하다

이것들은 어떻게 느낄 수 있는가?

 

 

의존성 낮추기

protocol HandleType {
	func honk() // 경적을 울리다
}

final class AHandle: HandleType {
	func honk() {
    	print("뿌뿌")
    }
}

final class CHandle: HandleType {
	func honk() {
    	print("뛰뛰")
    }
}

final class BHandle: HandleType {
	func honk() {
    	print("빵빵")
    }
}

이처럼 핸들의 종류는 수도 없이 많을 것이며, 서로 다른 기능/책임을 갖고 있을 수 있다.

 

하나의 Car 객체는 Handle이 필요하다.

class Car {
	var handle: HandleType?
}

이때, HandleType Protocol 타입을 프로퍼티로 선언하여 내부 handle 프로퍼티를 선언하였다면

handle은 A, B, C Handle 모두 사용이 가능하다.

 

이때, CarFactory에서 car.handle = AHandle() 처럼 프로퍼티 주입을 사용할 수 있는데

이렇게 됐을 때, Car 클래스는 AHandle에 대한 의존성을 갖지 않게 된다.

-> 다시 말해, Car 클래스는 Handle 이라는 protocol에 대한 소스코드 의존성이 생겼을 뿐

-> AHandle Class 에 대한 소스코드 의존성이 생기진 않는다.

-> 또한, 소스코드 의존성은 빌드시간에 영향을 미치기 때문에 

 

-> 사실 이런 protocol을 사용하는 건 DI에 항상 연관되어서 나오는데

-> DI만을 따지자면 위에서 말한 "어떤 객체가 필요한 것들을 전달하는 행위" 뿐이다
-> DI와 protocol을 합쳐서 의존성(필요한 것)을 주입하고 소스 코드 의존성을 줄이는 것을 DI라고 하는 것으로 이해했다.

 

DI = 필요한 것을 주입하는 행위 + Procotol을 이용한 소스코드 의존성 감소

 

CarFactory, AHandle, BHandle, CHandle이 각각 다른 모듈에 있다고 가정해보자.

각각의 핸들을 교체할 때마다 해당 Handle 파일이 있는 모듈을 import해야 할 것이며

이는 빌드시간에 그대로 영향을 미친다.

 

이를 Protocol을 사용하여 사용하는 모듈에서 의존성 주입을 통해 주입하게 되면

이때 발생하는 개념이 "IoC (Inversion of Control)" 즉, 제어의 역전이다.

 

// 1. DI X

// 비즈니스 로직 수행 모듈 A

import B
...
...
let service = ServiceImpl()
let info = service.getInfo()


// 구현체 모듈 B
class ServiceImpl {
	func getInfo() -> Info {
    	...
    }
}


// 2. DI

// 비즈니스 로직 수행 모듈 A
// import B -> 사라짐
....
.....
init(service: Service) {
	self.service = service
}

....

let info = service.getInfo()


protocol Service {
	func getInfo() -> Info
}



// 구현체 모듈 B

import A // -> 생김

class ServiceImpl: Service {
	func getInfo() -> Info {
    	return info
    }
}

예제를 통해서 보면 1번 예제에서는 모듈 A에서 모듈 B를 import하여서 사용하였고

2번 예제에서는 DI를 통해 전달받기 때문에, 모듈 A에서 모듈 B를 import하지 않고

모듈 B에서 모듈 A를 import하였다.

 

즉, 상위 모듈이 하위 모듈(구현체)을 import 해야 하는 의존방향에서 역전이 된 것이다.

그렇다고 코드 호출 방향이 달라지는 것은 아니다.

여전히 상위 모듈(비즈니스 로직 수행)에서 하위 모듈(구현체)의 기능을 호출한다.

 

 

IoC = 방향이 같던 "코드 호출방향"과 "소스코드 의존성" 에서 "소스코드 의존성"의 방향을 반대로 뒤집어
하위모듈에 대한 상위모듈의 의존성을 역전시킨 것

 

장점

  • 확장과 재사용성
    • 새 기능 개발 용이
    • 기존 기능 수정 수월
    • 모듈별 독립적인 재사용 가능
  • 유지보수성
    • 영향 범위 파악이 쉬움
    • 빌드 시간 단축
  • 테스트 용이성
    • 테스트 대역으로 치환 용이

단점

  • 코드가 복잡해져 이해하기 어려워짐
    • 호출들이 protocol로 연결되어 있어서 호출을 따라가기가 힘들어짐
    • 디버깅이 어려워짐