Swift에서의 DI와 DIP

임창묵·2022년 11월 26일
0

1. 의존성 주입(DI)이란?

위키피디아: 의존성 주입
하나의 객체가 다른 객체의 의존성을 제공하는 테크닉

의존성 주입의 의도는 객체의 생성과 사용의 관심을 분리하는 것이다. 이는 가독성과 코드 재사용을 높혀준다.
혼동하기 쉬운데, 의존성을 분리하기 위해서 의존성을 주입하는 방법을 사용하는 것이며, DI는 의존성을 분리하여 주입하는 것을 합한 테크닉을 의미한다.

의존성(Dependency)이란?

한 클래스가 변경될 때, 다른 클래스도 변경되어야 하는 관계

예를 들어, class A가 멤버 변수로 class B의 인스턴스 객체를 가지는 경우, A의 인스턴스 객체를 만들기 위해 B의 인스턴스 객체 생성이 선행되어야 하는데, 이러한 성질을 A가 B에 대해 의존성을 가진다고 표현한다.

class A {
	private let dependencyB: B
    
    init(dependencyB: B) {
    	self.dependencyB = dependencyB
    }
    
    func displayA() {
    	self.dependencyB.displayB()
    }
}

class B {
	func displayB() {
    	print("안녕")
    }
}

이 때, 의존성은 A와 B의 관계를 의미하기도 하지만 B라는 멤버 객체를 의미하기도 하며, B는 A의 의존성(Dependency)이다라고 표현하기도 한다.

따라서, A를 생성하기 위해 필요한 의존성(B)를 주입한 것이라고도 표현할 수 있고, 이 때의 의존성 주입과 DI라는 테크닉을 혼동해서는 안된다.

왜 의존성을 분리해야 하는가?

의존성이 있으면 코드를 변경할 부분이 많아진다.

예를 들어, B가 변경되는 경우 A도 변경해야 한다. 위 코드의 func displayB()가 삭제된다면 func displayA()를 사용할 수 없게 되는데, 이처럼 변경해야 하는 코드의 양이 많아진다.

이를 해결하기 위해서는 다른 곳에서 인스턴스를 생성하고 필요한 곳에 주입(Injection)해주면 된다.

Swift에서는 어떻게 의존성을 분리하는가?

멤버변수를 Protocol로 선언하고, 외부에서 그 Protocol을 준수하는 변수를 생성해서 주입해준다.

먼저 Protocol로 반드시 따라야 하는 형태를 정해주어 의존성을 분리하고, Protocol만으로 객체를 생성할 수 없으므로 따로 생성하여 의존성을 주입해주는 방식을 사용한다.

protocol CPU {
	func run()
}
class DefaultCPU: CPU {
	func run() {
    	print("DefaultCPU is running!")
    }
}

class SpecialCPU: CPU {
	func run() {
    	print("SpecialCPU is running!")
    }
}
class Computer {
	private let cpu: CPU
    
    // 1. 생성자를 이용한 주입
    init(cpu: CPU) {
    	self.cpu = cpu
    }
    
    // 2. 함수(+패러미터)를 이용한 주입
    func changeCPU(cpu: CPU) {
    	self.cpu = cpu
    }
    
    // 3. 프로퍼티를 이용한 주입
    func setDefaultCPU() {
    	let defaultCPU = DefaultCPU()
        self.cpu = defaultCPU
    }
    
    func turnOn() {
    	self.cpu.run()
    }
}

// 1. 생성자
let myCPU = DefaultCPU()
let myComputer = Computer(cpu: myCPU)

// 2. 함수(+패러미터)
let specialCPU = SpecialCPU()
myComputer.changeCPU(specialCPU)

// 3. 프로퍼티
myComputer.setDefaultCPU()

class Computer의 멤버 변수인 cpu가 protocol CPU를 준수하기만 하면되므로 원하는 대로 변수를 갈아끼워도 되며, 변수를 갈아끼워도 class Computer의 func run()은 변경할 필요가 없다.

변수를 갈아끼우는 방식은 생성자 주입, 함수 주입, 프로퍼티 주입 중 필요한 방식을 선택하면 된다.

2. DIP(의존관계 역전 원칙, Dependency Inversion Principle)란?

위키피디아: 의존관계 역전 원칙
첫째, 상위 모듈은 하위 모듈에 의존해서는 안된다. 상위 모듈과 하위 모듈 모두 추상화에 의존해야 한다.
둘째, 추상화는 세부 사항에 의존해서는 안된다. 세부사항이 추상화에 의존해야 한다.

Protocol을 class의 멤버 변수 및 메서드의 패러미터와 output에 사용하고, protocol을 채택한 객체에서 메서드의 비즈니스 로직을 구현함으로써 역전이 일어난다는 의미.

protocol CPU {
	func run()
}

class AMDCpu: CPU {
	func run() {
    	print("AMD's CPU is the best cpu.")
    }
}
protocol CPUFactory {
	func make() -> CPU
}

class AMDFactory: CPUFactory {
	func makeCPU() -> CPU {
    	return AMPCpu()
    }
}
class CPUStore {
	let factory: CPUFactory
    
    init(factory: CPUFactory) {
    	self.factory = factory
    }
    
    func getCPU() -> CPU {
    	return self.factory.makeCPU()
    }
}

let myCPUStore = CPUStore(factory: AMDFactory())
let myCPU = myCPUStore.getCPU()
myCPU.run()

상위 객체인 CPUStore의 멤버변수나 메서드 모두 protocol에 의존하는 방식으로 추상화가 이루어지고, func makeCPU()의 비즈니스 로직을 상위 객체인 CPUStore가 구현하는 것이 아니라 protocol을 채택한 하위 객체인 CPUFactory가 구현하고 있는 형식으로 역전이 일어남.

0개의 댓글