Search
💉

Why Dependency Injection? | DI, DI Container, IoC, IoC Container 개념

subtitle
Dependency Injection, Dependency Injection Container, Inversion of Control
Tags
개발 일반
Created
2021/07/04
2 more properties

Dependency Injection

Dependency란 클래스에서 필요로 하는 클래스나 객체를 의미한다. Dependency Injection은 클래스가 직접 dependency를 생성하지 않고 외부에 dependency를 요청하는 것을 말한다.
dependency injection에는 3가지 방법이 있다.
1.
생성자 주입
2.
setter 주입
3.
Interface 주입
작은 인터페이스의 경우 interface에 의한 주입도 나쁘진 않으나 아래 예시처럼 dependency가 많아질 경우 component와 dependency를 연결하는 데 많은 작업을 해야하므로 생성자나 setter에 의한 주입이 선호된다.
그렇다면 생성자 주입과 setter 주입 방식 중 무엇이 좋을까?
Kent Beck의 Smalltalk Best Practice Patterns 책에 의하면 생성자 주입 방식이 좋다고 한다. 생성자 파라미터로 주입할 경우 컴포넌트가 dependency에 의존하고 있음을 명확히 설명해준다. 또한, setter를 사용할 경우 컴포넌트 외부에서 다른 dependency로 수정할 수 있다. 그러므로 setter를 사용하지 않음으로써 불변해야하는 dependency 필드를 숨길 수 있다.
// constructor 주입 class Something { constructor(private db: DB) { } } // setter 주입 class Something { set db(db: DB) {} } // interface 주입 interface InjectDB { db: DB injectDB: (db: DB) => void } class Something implements InjectDB { db: DB injectDB(db: DB) { this.db = db } }
TypeScript
복사
function에서는 아래 코드처럼 인자로 dependency를 주입하여 사용하기도 한다.
const getSomething = async (dependency: any) => { }
TypeScript
복사

Inversion of Control

IoC는 코드의 복잡성을 줄여주고 재사용 가능한 코드로 만드는 데 도움을 주는 설계 원칙이다. Dependency Injection은 IoC를 구현하는 패턴 중 하나이다. IoC와 DI에 대한 차이가 무엇이냐에 대한 논쟁이 있었는데 마틴 파울러에 의하면 IoC는 좀 더 일반적인 용어이기 때문에 사람들이 헷갈려 하는 경우가 많아 DI라고 부르기로 했다고 한다.
Inversion of Control is too generic a term, and thus people find it confusing. As a result with a lot of discussion with various IoC advocates we settled on the name Dependency Injection. - Martin Fowler
Inversion of Control은 프레임워크와 라이브러리를 가름하는 핵심이기도 하다. 기존 프로그래밍 설계에서는 사용자 코드가 라이브러리를 호출하는 형태지만, Control이 반전되면 프레임워크(또는 Container)가 사용자 코드를 호출하게 된다. 이런 현상을 Inversion of Control이라고 하며 Hollywood Principle이라고도 한다. Don't call us, we'll call you

Why do we use dependency injection?

클래스 내부에서 사용하는 의존성을 외부에서 주입하면 아래와 같은 이점을 얻을 수 있다.
Substitutability: 종속성을 대체하기 쉬워진다. 종속성을 대체하고 싶은 경우 정의된 인터페이스를 구현할 수 있는 새 종속성으로 대체하면 된다.
Flexibility: 'Open Closed Principle'에 따라 확장할 수 있는 부분은 개방되고, 수정될만한 부분은 폐쇄해야 한다.
Delegation: IoC는 다른 사람에게 기능 구현에 대해 위임하되, hook나 plugin, callback을 제공한다.
Testability: Dependency를 주입함으로써 test할 때 핵심 기능을 mock으로 대체할 수 있다.
ex. DI는 코드를 좀 더 testable하게 만들어준다.
const assert = require('assert') describe('get something test', () => { test('fetch url test', async () => { function fakeFetch(url: string) { assert(url === 'https://something.com') return Promise.resolve(() => function json() { return Promise.resolve(() => [{ ...something... }]) }) } getSomething(fakeFetch, id) }) })
TypeScript
복사
DI는 코드 간 결합도를 낮추는 디자인 패턴 중 하나이며 코드의 복잡성 또한 낮출 수 있다.
기존의 방식대로 프로그래밍하는 경우를 예로 들어보자.
클래스 내에서 다른 클래스의 인스턴스를 생성하여 사용하는 경우, 해당 클래스에 의존성을 갖게 된다.
아래처럼 DB에 접근해 user 데이터를 반환하는 서비스 클래스가 있다고 할 때, UserService 클래스는 DB에 대해 의존하고 있다. UserService 클래스 뿐만 아니라 DB에 접근하는 모든 서비스 클래스는 DB에 의존할 것이다. 이때 DB 클래스 생성자에 파라미터가 하나 추가되기라도 하면 DB 클래스를 참조하는 모든 서비스 클래스의 initialize 문에 인자를 추가해줘야한다.
class UserService { private db: DB private logger: Logger private authService: AuthService // ...생략 constructor() { this.db = new DB() this.logger = new Logger() this.authService = new AuthService() // ...생략 } }
TypeScript
복사
따라서 위와 같은 코드는 다른 클래스와의 결합도가 높고 의존하는 클래스에 변화가 생길 시에 참조하는 클래스 코드를 모두 변경해야하는 번거로움이 생긴다.
이런 문제를 해결하기 위해 클래스 내부가 아닌 외부에서 DB 인스턴스를 생성하여 주입한다. dependency 구현에 대한 종속성을 제거하여 코드 결합성은 낮추고 좀 더 깔끔한 코드를 만들 수 있다.
class UserService { constructor(private db:DB, private logger: Logger, private authService: AuthService) { } }
TypeScript
복사

Dependency Injection Container

dependency injection container(DIC)는 상위 레벨에서 dependency를 관리하고 주입하는 컨테이너를 말한다.
컴포넌트와 dependency를 연결하기 위해 DIC를 만들고 위의 UserService 클래스 예시에 적용시켜보자. dependency들을 DependencyInjectionContainer 에서 관리하고 컴포넌트에 주입한다.
class UserService { contructor(private db: DB) { } } class DependencyInjectionContainer { const dbHost = process.env.DB_HOST const dbPW = process.env.DB_PASSWORD // ...중략... const db = new DB(dbHost, dbPW, ...) const userService = new UserService(db) const postService = new PostService(db) return { userService, postService, ... } }
TypeScript
복사
IoC는 Angular DI, Nest.js(inspired by Angular), 리액트 훅 등 여러 프레임워크에서 사용되고 있는 디자인 패턴이다. 프레임워크 구현이 아니더라도 프로젝트에서 재사용 가능한 코드, 확장성을 고려한 코드를 구현할 때 DI를 활용하면 좋을 것이다.

References