[iOS/Swift] DI와 DIP는 다르다! - 의존성 주입 알아보기

Swift의 의존성 주입과 관련해서 DI와 DIP라는 용어를 듣게 될 것이다.

처음에는 DI나 DIP나 그냥 의존성 주입을 하는 애구나.. 정도로만 알고있었다.

그런데, 좀 더 자세히 알아보니 이 둘은 (당연히) 서로 다른 개념이며, 의존성 주입을 해야하는 이유에 대해서도 생각해보게 되었다.

 

이번 글에서는 DI와 DIP에 대해 알아보도록 하겠다.

 


DI (Dependency Injection)

DI는 Dependency Injection의 약어로, 의존성 주입을 뜻한다.

먼저 DI가 필요한 상황과, 그 방법에 대해 알아보자.

 

  • A클래스가 B클래스에 의존하는 상황이라면, B클래스에서 변화가 일어날 때 A클래스에까지 영향을 미치는 상황이 있다.
/*
Guest 클래스는 A_Restaurant 에, A_Restaurant는 MrChef 클래스에 의존하고 있는 코드

Guest 클래스가 A_Restaurant 에 의존한다.
A_Restaurant 뭔가 변화가 일어나면, Guest 클래스에 영향을 미친다
 */

// 손님
class Guest {
    var restaurant = A_Restaurant()
    
    func lunch() -> String {
        restaurant.lunchMenu()
    }
}

// 음식점
class A_Restaurant {
    private let chef = MrChef()
    
    func lunchMenu() -> String {
        return chef.국밥() + chef.평양냉면() + chef.꿔바로우()
    }
}

// 주방장
class MrChef {
    func 평양냉면() -> String {
        return "맛있는 냉면"
    }
    
    func 국밥() -> String {
        return "맛있는 국밥"
    }
    
    func 꿔바로우() -> String {
        return "맛있는 꿔바로우"
    }
}

여기서 Guest클래스의 restaurant을 A_Restaurant의 인스턴스가 아닌 A_Restaurant 타입으로 만들고, 초기화 시점에 값을 넣어보자!

 

// 손님
class Guest {
    var restaurant: A_Restaurant
    
    init(restaurant: A_Restaurant) {
        self.restaurant = restaurant
    }
    
    func lunch() -> String {
        restaurant.lunchMenu()
    }
}

// 실제 사용
let restaurant = A_Restaurant()
let person = Guest(restaurant: restaurant)

person.lunch()

- Guest클래스는 A_Restaurant식당을 초기화 시점에 주입받고있다.

- Guest클래스는 A_Restaurant을 상속받는 어떤 클래스의 인스턴스로도 생성이 가능하다

- A_Restaurant을 init타입으로 지정하는 것은 Guest클래스에 어떤 클래스가 들어오는지 모르지만, 동작은 시킬 수 있다.

 

즉, 이 상태는 DI를 통해 객체의 생성과 사용을 분리하고 있다.

인스턴스 생성 시점에 별도의 클래스를 주입해주고 있으며, 이를 의존성 주입(DI)라고 한다.

 

 

그러나... 사실 우리가 원하는 건 이런게 아니다!!

왜냐면, 의존성 주입을 한다고 하더라도, 여전히 하위 클래스의 변화가 상위 클래스에 영향을 끼치고 있다!!

즉, A_Restaurant의 lunchMenu()라는 메서드 이름이 lunchMenu123() 으로 바뀌면, 여전히 Guest 클래스의 lunch 메서드 내부 코드를 수정해야 한다는 것이다.

 

즉, 실제 프로젝트에서 이와같이 의존관계가 얽혀있을 경우, 수정해야될 코드가 많아진다는 것이다.

 


추상화에 의존

이러한 불편함을 해결하기 위해, 실제 구현체가 아닌 추상화(interface, swift에서는 protocol)에 의존하는 방식으로 해결해볼 수 있다.

 

  • Restaurant이라는 protocol을 만들고,restaurant를 A_Restaurant의 인스턴스가 아닌, Restaurant을 준수하는 형태로 만들어주자
  • 이렇게되면 A_Restaurant뿐 아니라 B_Restaurant가 새로 생기더라도 Restaurant프로토콜만 준수한다면, 어떤 식당에도 대응이 가능하다
// 추상화
protocol Restaurant {
    func lunchMenu() -> String
}

// 손님
class Guest {
    var restaurant: Restaurant
    
    init(restaurant: Restaurant) {
    	self.restaurant = restaurant
    }
    
    func lunch() -> String {
        restaurant.lunchMenu()
    }
}

// 식당 1
class A_Restaurant: Restaurant {
    private let chef = MrChef()

    func lunchMenu() -> String {
        return chef.국밥() + chef.평양냉면() + chef.꿔바로우()
    }
}

// 식당 2
class B_Restaurant: Restaurant {
    private let chef = MrChef()
    
    func lunchMenu() -> String {
        "샌드위치"
    }
}

// 주방장
class MrChef {
    func 평양냉면() -> String {
        return "맛있는 냉면"
    }
    
    func 국밥() -> String {
        return "맛있는 국밥"
    }
    
    func 꿔바로우() -> String {
        return "맛있는 꿔바로우"
    }
}

// 실제 사용
let restaurant = A_Restaurant()
let person = Guest(restaurant: restaurant)
person.restaurant = A_Restaurant()

person.lunch()

- 이제 Guest클래스는 A_Restaurant이나 B_Restaurant 등 구현체에 의존하고 있지 않다.

- 대신 Restaurant이라는 프로토콜에 의존하고 있다.

- 이제 Restaurant 프로토콜을 채택하고 있는 어떤 구체타입이든 다 적용할 수 있다.

- 즉, 실제 사용 부분에서 restaurant 에 B_Restaurant을 넣어줘도 추가로 코드를 수정할 부분이 없다 !!!

 

이제 Guest는 Restaurant이라는 프로토콜, 즉 구현체가 아닌 추상화에 의존함으로써

DIP를 준수하게 되었다!


DIP (Dependency Inversion Principle)

DIP는 Dependency Inversion Principe의 약어로, 의존성 역전 원칙이라는 뜻이다.

사실상 DI와는 다른 개념이며, 

DIP를 준수하기 위한 방법중 하나가 DI라고 보는것이 더 맞다.

 

그렇다면 왜 의존성 역전이라는건지 그림으로 알아보자.

위의 그림이 DIP를 적용하기 전의 의존관계이고,

아래 그림이 DIP를 적용한 이후의 의존관계이다.

 

화살표 방향을 보면 DIP 적용 후에 역전된것을 볼 수 있다.

 

그렇다면, 이렇게 DIP를 통해 추상화에 의존해야 하는 이유가 뭘까?

내가 직접 코드를 짜면서 와닿은 이유는 다음과 같다.

 

 

1. 실제 데이터가 아닌 Mock 데이터를 사용하기가 쉬워진다.

 네트워크 통신을 해야 하는데, 아직 서버가 구축되기 전이거나 서버에 문제가 생겨서 통신 응답값을 받아보기 어려운 상황이 있을 수 있다.

실제로 서버와 함께 프로젝트를 할 때, 클라이언트단에서 뷰 작업이 모두 끝났는데 서버가 완성이 안된 경우가 있었다.

그 때 DIP를 적용해 실제 데이터가 아닌 MockData를 통해 테스트를 했었는데, 실제 데이터와 MockData 둘 다 같은 프로토콜을 채택하도록 만들어 두었기 때문에, 서버가 완성됐을때 그 부분만 갈아끼워주면 됐었다.

즉, Mock데이터에서 실제 데이터로의 전환을 DIP 덕분에 불필요한 코드 수정 없이 간편하게 적용할 수 있었다.

 

 

2. 테스트하기 좋은 형태가 된다.

실제 데이터가 아닌 Mock데이터를 사용하기 쉬워진다는 말은 UnitTest시 테스트 코드 작성도 수월해진다는 뜻이다.

테스트코드에서 실제로 네트워크 통신을 하기 위해선 (특히 XCTest에선) 비동기 처리도 신경써주어야 하고, 10,000번 중 한번이라도 네트워크에 문제가 생기면 테스트의 신뢰도가 떨어질 수 있다.

그래서 UnitTest를 할때는 실제 네트워크 통신을 하는게 아닌, 동일한 프로토콜을 채택하고 있는 Mock데이터를 대신 사용할 수 있다.