본문 바로가기

Dependency Injection

Dependency Injection - Chapter1 마지막(DIScope, Summary)

728x90


앞서 설명했듯이 DI의 중요한 요소는 다양한 책임 관계를 별도의 클래스로 나누는 것입니다. 클래스에서 분리하는 책임 중 하나는 의존성의 인스턴스를 생성하는 작업입니다.

Hello DI! 예제에서 Salutation 클래스에서 종속성 생성 책임이 해제된 예제에서 이에 대해 설명했습니다. 대신 이 책임이 애플리케이션의 Main 메서드로 옮겨졌습니다.
 
클래스가 종속성에 대한 제어권을 포기하면 특정 구현을 선택하는 것 이상의 것을 포기하는 것입니다. 이느 개발자로서 몇 가지 이점을 얻을 수 있습니다. 처음에는 클래스가 어떤 객체가 생성되는지에 대한 제어권을 포기하는 것이 불리한 것처럼 보일 수 있지만, 제어권을 잃는 것이 아니라 다른 곳으로 옮기는 것일 뿐입니다.

개발자는 종속성에 대한 클래스의 제어권을 제거함으로써 제어권을 확보할 수 있습니다. 이것은 단일 책임 원칙의 적용입니다. 클래스는 종속성 생성을 처리할 필요가 없어야 합니다.
 

위 설명은 제어의 역전에 관한 내용입니다.

 

제어의 역전?(Inversion of Control, IoC)

💡 클래스가 자신이 사용할 다른 클래스나 구성 요소(종속성)를 스스로 만들지 않고, 이를 외부에서 받아들이도록 하는 것을 '제어의 역전(Inversion of Control, IoC)'이라고 합니다. 이것이 어려워 보일 수 있지만, 사실 이 방식은 여러분이 코드를 더 잘 관리할 수 있도록 도와줍니다.

예를 들어, 자동차를 생각해보세요. 만약 자동차가 스스로 연료를 만들어야 한다면, 이는 자동차에게 매우 부담이 될 것입니다. 하지만, 실제로는 주유소에서 연료를 넣어주죠. 이처럼 클래스도 스스로 필요한 부품을 만들기보다는, 그 부품들을 제공받아 사용합니다.

이렇게 함으로써 개발자는 몇 가지 이점을 얻을 수 있습니다.

→ 단일 책임 원칙(Single Responsibility Principle):
    - 클래스는 한 가지 일만 해야 합니다. 연료를 만드는 것이 아니라, 자동차가 주행하는 것에만 집중하게 하는 것처럼, 클래스도 하나의 기능에만 집중하게 됩니다. 이렇게 되면 클래스가 더 단순해지고, 이해하기 쉬워지며, 유지보수도 쉬워집니다.

→ 재사용성:    - 자동차가 어떤 주유소든지 연료를 넣을 수 있듯이, 클래스도 다양한 상황에서 필요한 부품을 받아 사용할 수 있습니다. 이는 코드를 다른 곳에서도 쉽게 재사용할 수 있게 만듭니다.

→ 테스트 용이성:    - 자동차의 엔진을 테스트할 때 연료의 종류를 바꿔가며 테스트할 수 있어야 합니다. 클래스도 마찬가지로, 필요한 부품을 바꿔가며 쉽게 테스트할 수 있습니다.

제어의 역전을 사용하면 처음에는 클래스가 어떤 객체를 사용할지 결정하지 않고, 이 결정을 프로그램의 다른 부분에 맡기기 때문에 제어를 잃는 것처럼 보일 수 있습니다. 하지만 실제로는, 제어를 더 큰 틀에서 관리하게 되어, 전체적으로 더 많은 제어를 얻게 되는 것입니다.

 

객체 구성만이 제어의 유일한 차원이 아닙니다. 클래스는 객체의 수명을 제어할 수 있는 기능도 잃게 됩니다. 의존성 인스턴스가 클래스에 주입되면 소비자는 언제 생성되었는지, 언제 범위를 벗어날지 알 수 없습니다. 이는 소비자에게는 문제가 되지 않습니다. 소비자가 종속성의 수명을 알지 못하도록 하면 소비자를 단순화할 수 있습니다.

DI는 마치 레고 블록을 조립하는 것과 같다고 생각할 수 있습니다. 개발자가 직접 각 부품을 만들어 조립하는 대신, 미리 만들어진 부품들을 가져와 필요한 곳에 끼워 맞추는 것입니다.

일반적으로 클래스가 필요한 다른 클래스의 객체를 직접 만들면, 각 클래스마다 다른 방식으로 객체를 생성하게 됩니다. 이렇게 되면 일관성이 없어지고 나중에 변경사항이 생겼을 때 한 곳만 고치면 다른 곳에서 문제가 생길 수 있습니다.

DI를 사용하면 이런 문제를 해결할 수 있습니다. DI는 마치 레고 블록을 조립할 때 누군가가 정확한 부품을 정확한 시점에 건네주는 것과 같습니다. 이렇게 함으로써 모든 부품이 동일한 방법으로 만들어지고 조립됩니다. 이것이 바로 '일관된 방식으로 관리'하는 것이죠.

이제, 레고 블록을 조립할 때 각 블록이 어떻게 작동하는지, 언제 교체해야 하는지 알려주는 것을 상상해보세요. DI는 이런 역할을 합니다. 

DI는 각 부품(클래스의 객체)을 '인터셉트'(가로채서 조작)하여 필요한 설정을 하고, 그 부품이 언제 만들어지고, 얼마나 오래 살아있어야 할지를 관리합니다. 이 모든 것을 애플리케이션의 다른 부분이 신경 쓸 필요 없이 처리해주는 것이죠.
 
DI에 대해 요약하자면

  • 객체 구성: 필요한 객체를 적절한 상태로 만들어 제공
  • 인터셉션: 객체가 사용되기 전에 필요한 설정이나 변경을 가할 수 있음
  • 수명관리: 객체가 언제 생성되고 언제 제거될지 관리할 수 있음

 

Object Composition

extensibility, late binding 및 parallel development의 이점을 활용하려면 클래스를 애플리케이션으로 구성할 수 있어야 합니다. 즉, 전기 제품을 연결하듯이 개별 클래스를 조합하여 애플리케이션을 만들 수 있어야 한다는 뜻입니다.

그리고 가전제품과 마찬가지로 새로운 요구 사항이 도입될 때 기존 클래스를 변경하지 않고도 이러한 클래스를 쉽게 재배치할 수 있어야 합니다.

객체 구성은 애플리케이션에 DI를 도입하는 주된 동기인 경우가 많습니다. 사실 처음에 DI는 객체 컴포지션과 동의어였으며, 이 주제에 대한 Martin Fowler의 원본 기사에서 유일하게 논의된 측면입니다.

많은 사람들이 DI를 제어의 역전(IoC)이라고 부릅니다. 이 두 용어는 때때로 같은 의미로 사용되기도 하지만 DI는 IoC의 하위 집합입니다. 이 책 전체에서 가장 구체적인 용어인 DI를 일관되게 사용합니다. IoC는 구체적으로 다음과 같은 것을 의미합니다.
 

Dependency Injection or Inversion of Control?

IoC는 원래 프로그램 흐름을 프레임워크가 제어하는 프로그래밍 스타일을 의미했습니다.

iOS 개발에 비유하자면, IoC는 UIKit이나 SwiftUI 같은 애플의 프레임워크가 앱의 흐름을 제어하는 것을 말합니다. 예를 들어, UIKit에서는 뷰 컨트롤러가 있고, 사용자의 인터랙션에 따라 프레임워크가 적절한 메서드를 호출합니다. 따라서, 개발자는 메서드를 언제 호출할지 직접 제어하지 않고 프레임워크에 이를 맡깁니다.

DI는 이런 IoC의 한 예로, 클래스가 필요로 하는 종속성(다른 클래스의 객체 등)을 외부에서 전달받는 것을 의미합니다. iOS 개발에서는 이를 프로토콜과 구조체, 클래스를 사용해서 구현할 수 있습니다. 예를 들어, 데이터를 가져오는 클래스가 있고 이를 여러 뷰 컨트롤러에서 사용해야 할 때, 각 뷰 컨트롤러가 데이터 클래스의 인스턴스를 직접 만들지 않고 이를 생성하여 전달하는 책임을 다른 구성요소에 맡기게 됩니다. 이렇게 함으로써 더 유연하고 테스트하기 쉬운 코드를 만들 수 있습니다.

정리하자면, IoC는 개발자가 아닌 프레임워크가 애플리케이션의 흐름을 제어하는 개념이고, DI는 그런 프레임워크 내에서 필요한 객체를 생성하고 관리하는 방식 중 하나입니다. DI는 프로토콜과 같은 추상화를 통해 구현되며, 이를 통해 클래스의 재사용성과 테스트 용이성이 향상됩니다.

 

Object LifeTime

클래스가 자신의 의존성을 직접 제어하지 않고 외부에서 주입받도록 하면, 구체적인 구현물을 선택하는 권한뿐만 아니라 인스턴스가 생성되는 시점이나 범위를 벗어나는 시점을 제어하는 권한도 포기하게 됩니다. .NET 환경에서는 가비지 컬렉터가 이러한 역할을 대신 수행해 주는데, iOS 개발에서는 ARC(Automatic Reference Counting)가 이 역할을 합니다. 클래스는 필요한 의존성을 주입받아 사용하고, 더 이상 참조되지 않을 때 ARC에 의해 메모리에서 해제됩니다.

만약 두 객체가 동일한 의존성의 인스턴스를 공유한다면 어떻게 될까요? 같은 의존성 유형을 각각의 객체에 따로 주입할 수도 있고, 하나의 인스턴스를 여러 객체가 공유하도록 선택할 수도 있습니다. 하지만 객체가 사용하는 관점에서는 이 사이에 차이가 없습니다. 리스코프 치환 원칙(Liskov Substitution Principle)에 따르면, 객체는 주어진 인터페이스의 모든 인스턴스를 동등하게 취급해야 합니다.

이를 iOS 개발 상황으로 바꾸어 설명하자면, 예를 들어 데이터 소스를 관리하는 `DataSource` 프로토콜이 있고, 이를 구현한 `CoreDataDataSource`와 `NetworkDataSource` 두 가지 클래스가 있다고 가정해 봅시다. 어떤 뷰 컨트롤러는 `CoreDataDataSource` 인스턴스를 사용할 수도 있고, 다른 뷰 컨트롤러는 `NetworkDataSource` 인스턴스를 사용할 수도 있습니다. 또한, 하나의 `DataSource` 인스턴스를 여러 뷰 컨트롤러가 공유해서 사용할 수도 있습니다. 그러나 뷰 컨트롤러는 자신이 사용하는 데이터 소스가 무엇인지, 어떻게 생성되었는지 알 필요가 없으며, 단지 `DataSource` 프로토콜을 통해 데이터에 접근하게 됩니다.
 
관련하여 아래 예시를 보겠습니다.
첫 번째 상황: 각 사용자가 같은 유형의 의존성에 대해 자신만의 인스턴스를 가지는 경우

protocol MessageWriter {
    func writeMessage()
}

class ConsoleMessageWriter: MessageWriter {
    func writeMessage() {
        // 메시지를 콘솔에 출력하는 로직
        print("Hello, World! - from Console")
    }
}

class Salutation {
    let writer: MessageWriter
    
    init(writer: MessageWriter) {
        self.writer = writer
    }
    
    func greet() {
        // 인사말을 출력하는 로직
        writer.writeMessage()
    }
}

class Valediction {
    let writer: MessageWriter
    
    init(writer: MessageWriter) {
        self.writer = writer
    }
    
    func farewell() {
        // 작별 인사를 출력하는 로직
        writer.writeMessage()
    }
}

// 각 사용자에게 자신만의 ConsoleMessageWriter 인스턴스를 생성하여 주입
let writer1 = ConsoleMessageWriter()
let writer2 = ConsoleMessageWriter()

let salutation = Salutation(writer: writer1)
let valediction = Valediction(writer: writer2)

 
두 번째 상황: 두 사용자가 동일한 의존성 인스턴스를 공유하는 경우

// 하나의 ConsoleMessageWriter 인스턴스를 생성
let sharedWriter = ConsoleMessageWriter()

// 동일한 인스턴스를 두 소비자에게 주입
let sharedSalutation = Salutation(writer: sharedWriter)
let sharedValediction = Valediction(writer: sharedWriter)

종속성을 공유할 수 있기 때문에 단일 사용자가 수명을 제어할 수 없습니다. 그러나 ARC가 있으므로 이는 큰 문제가 되지 않습니다. 
 

Conclusion


Dependency Injection  은 그 자체가 목표가 아니라 목적을 위한 수단입니다.

유지 관리 가능한 코드의 중요한 부분인 느슨한 결합을 활성화하는 가장 좋은 방법입니다. 

느슨한 결합을 통해 얻을 수 있는 이점이 항상 즉각적으로 드러나는 것은 아니지만 코드 베이스의 복잡성이 증가함에 따라 시간이 지남에 따라 가시화될 것입니다. DI와 관련하여 느슨한 결합에 대한 중요한 점은 효과를 발휘하려면 코드 베이스의 모든 곳에 있어야 한다는 것입니다.

긴밀하게 결합된 코드 베이스는 결국 스파게티 코드로 변질되지만,잘 설계되고 느슨하게 결합된 코드 베이스는 유지 관리가 가능합니다. 진정으로 유연한 설계에 도달하려면 느슨하게 결합하는 것 이상의 것이 필요하지만,인터페이스에 대한 프로그래밍은 전제 조건입니다.

DI는 디자인 원칙과 패턴의 모음에 불과합니다. 도구와 기술이라기보다는 코드를 사고하고 설계하는 방식에 관한 것입니다. DI의 목적은 코드를 유지 관리하기 쉽게 만드는 것입니다.  고전적인 Hello World의 예처럼 작은 코드 베이스는 그 크기 때문에 본질적으로 유지 관리가 어렵습니다

그렇기 때문에 DI는 간단한 예제에서 오버엔지니어링처럼 보이는 경향이 있습니다. 코드 베이스가 커질수록 이점이 더 눈에 띄게 나타납니다. 이 내용은 다음에 더 자세히 다뤄볼 예정입니다.
 
Summary

  • 종속성 주입은 느슨하게 결합된 코드를 개발할 수 있게 해주는 일련의 소프트웨어 설계 원칙과 패턴입니다. 느슨하게 결합하면 코드의 유지 관리가 더 쉬워집니다.
  • 느슨하게 결합된 인프라를 구축하면 애플리케이션의 코드 베이스와 인프라를 크게 변경하지 않고도 누구나 사용할 수 있고 변화하는 요구 사항과 예상치 못한 요구 사항에 맞게 조정할 수 있습니다.
  • 원인 제공자의 범위가 좁아지므로 문제 해결의 부담이 줄어듭니다.
  • DI를 사용하면 원본 코드를 다시 컴파일할 필요 없이 클래스나 모듈을 다른 것으로 대체하는 기능인 Late Binding을 사용할 수 있습니다.
  • DI를 사용하면 전기 플러그와 소켓으로 작업할 때 유연성을 확보하는 것과 마찬가지로 명시적으로 계획하지 않은 방식으로 코드를 쉽게 확장하고 재사용할 수 있습니다.
  • DI는 각 팀원 또는 팀 전체가 고립된 부분을 더 쉽게 작업할 수 있는 관심사 세퍼레이션을 통해 동일한 코드 기반에서 병렬 개발을 간소화합니다.
  • DI는 단위 테스트를 작성할 때 종속성을 테스트 구현으로 대체할 수 있으므로 소프트웨어를 더 쉽게 테스트할 수 있습니다.
  • DI를 실행할 때 협업하는 클래스는 필요한 서비스를 제공하기 위해 인프라에 의존해야 합니다. 이를 위해 클래스가 구체적인 구현 대신 상호 작용에 의존하도록 하면 됩니다.
  • 클래스는 제3자에게 종속성을 요청해서는 안 됩니다. 이것은 서비스 로케이터라는 안티 패턴입니다. 대신 클래스는 생성자 매개변수를 사용하여 필요한 의존성을 정적으로 지정해야 하는데, 이를 생성자 주입이라고 합니다.
  • 많은 개발자가 DI를 사용하려면 DI 컨테이너라고 불리는 특수한 도구가 필요하다고 생각합니다. 이는 잘못된 생각입니다. DI 컨테이너는 유용하지만 선택 사항인 도구입니다.
  • DI를 가능하게 하는 가장 중요한 소프트웨어 설계 원칙 중 하나는목록 리스코프(Lis-kov) 대체 원칙입니다. 이 원칙은 클라이언트나 구현을 손상시키지 않고 인터페이스의 한 구현을 다른 구현으로 대체할 수 있게 해줍니다.
  • 종속성이 이미 사용 가능하고, 결정론적 동작을 하며, 관계형 데이터베이스와 같은 설정 런타임 환경이 필요하지 않고, 대체, 래핑 또는 인터셉트할 필요가 없는 경우 종속성은 안정성으로 간주됩니다.
  • 종속성이 개발 중이거나, 모든 개발 머신에서 항상 사용할 수 있는 것은 아니거나, 비결정적 동작을 포함하거나, 대체, 래핑 또는 인터셉트해야 하는 경우 휘발성으로 간주됩니다.
  • 휘발성 종속성은 DI의 핵심입니다. 클래스의 생성자에 휘발성 종속성을 주입합니다.
  • 종속성에 대한 제어권을 소비자로부터 빼앗아 애플리케이션 진입점으로 옮기면 교차 절단 우려를 더 쉽게 적용할 수 있고 종속성의 수명을 더 효과적으로 관리할 수 있습니다.
  • 성공하려면 DI를 광범위하게 적용해야 합니다. 모든 클래스는 생성자 주입을 사용하여 필요한 휘발성 종속성을 가져와야 합니다. 느슨한 커플링과 DI를 기존 코드 베이스에 다시 적용하기는 어렵습니다.

 
지금까지 `Depend Inejction Principles, Practices, and Patterns'의 Par1 Putting Dependency Injection on the map의 chapter1을 살펴봤다.
읽기 시작한지 한달이 넘은것 같은데 아직도 par1의 1/3밖에 못읽었다니...

갈 길이 멀다

번역을 하면서 iOS개발자가 이해하기 어려운 내용들은 얼추 비슷한 내용으로 갈아끼워서 정리했다. 
.NET이니 BCL이니. 나는 그런거 잘 모르기 때문이다.
코드도 Swift코드로 대체했으니, 이 글을 보는 iOS개발자들에겐 도움이 되길 바란다. 
 
다음에 알아볼 내용은 Chapter2. Writing tightly coupled code 이다. 한동안 너무 열심히 한거같은데 
좀 쉬고 시작해야겠다.
 
 

728x90