본문 바로가기

Dependency Injection

DependencyInjection - What, Why, and How (DI에 대한 오해)

728x90

재작년 부터였던가. 매번 읽자 읽자 하다가 내용도 어렵고 너무 방대한 분량 때문에 미뤘던 책 Dependency Injection Principles Practices and Patterns (종속성 주입 원칙 사례 및 패턴) 을 드디어 읽기 시작했다.

사실 영문으로 되어 있기 때문에 번역기로 돌려서 읽는 중이지만… 한글로 번역된 문장들도 쉽게 읽히지 않는것은 내 머리의 한계를 탓할수 밖에 없다.

어쨌거나. 이 ‘의존성 주입’ 카테고리에 포스팅할 내용들은 Dependency Injection Principles, Practies, and Patterns 을 번역, 요약한 글들과 마치 책에 실린 내용인것 마냥 위장하고 있는 내 의견이 있을 예정이다. 
시작은 조금 쉬운 내용으로 시작한다. 

The Dependency Injection: What, Why, and How

DI를 둘러싼 두려움, 불확실성, 의심에도 불구하고 DI는 쉽게 배울 수 있는 기술이다.
배우는 동안 실수를 할 수도 있지만, 일단 익히고 나면 다시는 성공적으로 적용하지 못할 일은 없을 것이다.

스택오버플로에는 “5살짜리 아이에게 의존성 주입을 어떻게 설명할 수 있을까?” 라는 질문에 대한 답변이 있다. 
답변중 가장 높은 평가를 받은 John Munsch의 답변은 놀랍도록 정확한 비유를 제공한다. [링크]

 

내용을 그대로 설명해보자면 다음과 같다.

냉장고에서 직접 물건을 꺼내면 문제를 일으킬 수 있다. 문을 열어두면 엄마나 아빠가 원하지 않는 음식이 나올수도 있다. 심지어 가지고 있지도 않거나 유통기한이 지난것을 찾고 있을 수도 있다.

여러분이 해야할 일은 “점심을 먹으면서 마실 것이 필요해” 라고 필요를 말하면, 식사를 하기 위해 자리에 앉았을 때 무언가가 있는지 확인하는 것 뿐이다.

OOP측면에서 이것이 의미하는바는 협업하는 클래스(5세 어린이)는 필요한 서비스를 제공받기 위해 인프라(부모)에 의존해야 한다는 것이다.

DI용어에서는 서비스와 컴포넌트에 대해 자주 언급한다. 서비스는 일반적으로 서비스를 제공하는 것에 대한 정의인 추상화 이다. 
추상화의 구현은 종종 동작을 포함하는 클래스인 컴포넌트 라고 불린다.  이 책에서는 추상화와 클래스라는 용어로 이를 대신한다고 한다.

 

유지 관리 가능한 코드 작성(DI의 목적)

DI는 어떤 용도로 사용될까? DI의 본질은 목표가 아니라 목적을 위한 수단이다.
대부분의 프로그래밍 기술의 목적은 효율적으로 작동하는 소프트웨어를 제공하는 것이다. 그 목적의 한 측면은 유지관리 가능한 코드를 작성하는 것이다.

우린 대부분의 경우 기존코드베이스를 유지하고 관리, 확장하게 된다. 유지 관리가 용이할수록 이러한 작업들을 더 수월하게 해낼 수 있다.

코드를 유지 관리하기 쉽게 만드는 가장 좋은 방법은 무엇이 있을까? 바로 느슨한 결합을 사용하는 것이다. GOF의 디자인 패턴을 작성할 당시에 이미 이러한 사실은 널리 알려져 잇었다.

“구현이 아닌 인터페이스로 프로그래밍 하세요.”

디자인 패턴의 전제이다. 느슨한 결합은 몇번 강조해도 지나치지 않다. 느슨한 결합은 곧 코드를 확장 가능하게 만들고, 확장성은 유지보수를 용이하게 만든다.

DI는 느슨한 결합을 가능하게 하는 기술에 지나지 않는다. 그러나 DI에 대한 오해가 많아서 제대로 이해하는데 방해가 되기도 한다. 
DI에 대한 오해에 어떤 것들이 있는지 한 번 알아보도록 하자.


DI에 대한 일반적인 오해

  • DI is only relevant for late binding.
  • DI is only relevant for unit testing.
  • DI is a sort of Abstract Factory on steroids. 
  • DI requires a DI Container.

수년에 걸쳐 나타난 DI에 대한 가장 일반적인 오해 4가지를 간략히 살펴보고자 한다.

 

LateBinding

LateBinding이란 코드를 다시 컴파일 하지 않고 애플리케이션 일부를 교체할 수 있는 기능이다. 

늦은 바인딩(Late Binding)을 Swift 코드를 통해 이해해보자. Swift에서는 프로토콜과 의존성 주입을 사용하여 LateBinding 구현할 수 있다. 예를 들어, 다양한 데이터베이스 엔진에 접근하는 경우를 생각해 볼 수 있다.

먼저, 모든 데이터베이스 엔진이 준수해야 하는 프로토콜을 정의한다.

protocol DatabaseEngine {
    func query(sql: String) -> String
}


그 다음, Oracle과 SQL Server에 대한 구체적인 구현을 제공한다.

class OracleDatabaseEngine: DatabaseEngine {
    func query(sql: String) -> String {
        // Oracle 데이터베이스에 대한 쿼리 로직
        return "Oracle 결과"
    }
}

class SQLServerDatabaseEngine: DatabaseEngine {
    func query(sql: String) -> String {
        // SQL Server 데이터베이스에 대한 쿼리 로직
        return "SQL Server 결과"
    }
}



마지막으로 데이터베이스 엔진을 사용하는 클라이언트 클래스를 작성한다. 이 클래스는 구체적인 데이터베이스 엔진에 의존하지 않고, 대신 프로토콜을 통해 의존성을 주입받는다.

class DatabaseClient {
    let databaseEngine: DatabaseEngine

    init(databaseEngine: DatabaseEngine) {
        self.databaseEngine = databaseEngine
    }

    func performQuery(sql: String) -> String {
        return databaseEngine.query(sql: sql)
    }
}


이제 응용 프로그램의 다른 부분에서 DatabaseClient 인스턴스를 생성할 때 구체적인 데이터베이스 엔진을 주입할 수 있다. 이는 런타임에 엔진을 교체할 수 있게 해주고 늦은 바인딩의 이점을 제공한다.

결과적으로 아래와 같은 코드를 작성할 수 있게 된다.

let oracleEngine = OracleDatabaseEngine()
let sqlServerEngine = SQLServerDatabaseEngine()

let client1 = DatabaseClient(databaseEngine: oracleEngine)
let client2 = DatabaseClient(databaseEngine: sqlServerEngine)

print(client1.performQuery(sql: "SELECT * FROM users")) // Oracle 결과
print(client2.performQuery(sql: "SELECT * FROM users")) // SQL Server 결과


이러한 접근 방식을 통해 코드의 유연성과 테스트 용이성을 높일 수 있다.
DI가 LateBinding을 지원한다고 하지만 이는 DI의 여러 측면중 하나에 불과하다. 
LateBinding에만 관련이 있다고 생각했다면 이 점을 바로 잡아야한다. DI는 이 이상의 기능을 수행할 수 있기 때문이다.

Unit Testing

누군가는 DI가 단위 테스트 지원에만 관련이 있다고 생각한다.
DI가 단위테스트 지원의 중요한 부분인 것은 분명하지만 이 역시 LateBinding에만 관련이 있다는 말과 마찬가지로 편협한 관점이다. 

Abstract Factory 

“DI is a sort of Abstract Factory on steroids” 문장은 의존성 주입이 추상팩토리 패턴의 확장된 형태라는 오해를 말한다. 더 풀어 말하자면 DI가 애플리케이션에서 필요한 의존성들의 인스턴스를 생성하기 위한 추상 팩토리로 간주된다는 것을 의미한다.

추상 팩토리 패턴은 일반적으로 여러 메소드를 포함하는 추상화로, 각 메소드는 특정 유형의 객체를 생성한다. 이 패턴은 일반적으로 여러 플랫폼에서 실행되어야 하는 클라이언트 애플리케이션에 사용된다.

예를 들면 아래와 같은 코드가 그 예시다.

protocol UIControlFactory {
    func createButton()
    func createTextBox()
}


각 운영체제 따라 이 UIControlFactory의 구현이 다를 수 있다. 

DI를 단순히 추상 팩토리의 확장된 형태로 오해하는 것은 DI의 진정한 목적과 범위를 제한하는 것이다. DI의 주요 목적은 객체 간의 느슨한 결합을 달성하여 코드의 재사용성과 유지 보수성을 향상시키는 것. DI는 단순히 객체 생성에 국한되지 않고, 객체 간의 의존성을 관리하며, 다양한 디자인 패턴과 원칙을 지원한다. DI를 과소평가 하지말자.

DIContainer

마지막으로 DI를 사용하려면 DIContainer가 꼭 필요하다는 오해이다.
DIContainer는 애플리케이션을 구성할 때 클래스를더 쉽게 작성할 수 있도록 도와주는 선택적인 라이브러리일 뿐, 반드시 필요한 것은 아니다. 

DIContainer 없이 애플리케이션을 작성하는 경우 이를 PureDI라고 한다. 
코드량이 늘어나는 등 더 많은 작업이 필요하지만 말이다.

다시 한번 숙지하자. DI는 원칙과 패턴의 집합이며, DIContainer는 유용하지만 선택적인 도구이다.

이번 포스팅은 DI에 대한 아주 간단명료한 설명과 목적, DI에 대한 오해들에 대해서 살펴봤다. 아직 DIContainer가 무엇인지 정확히 설명하지도 않았고, 모든 오해들에 아주 설득력 있는 근거를 제시하진 않았다. 사실이다.

이 책은 이러한 일반적인 오해에 대한 하나의 큰 논증이기 때문에 다시 이를 다룰 것이다. 다음에 이어서 내용을 정리하겠다. 

 

다음글 GO

728x90