일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 자료구조
- java
- 파이썬 알고리즘 인터뷰
- Operating System
- codestates
- 자바
- typeScript
- OS
- react 기초
- 프로그래머스
- python algorithm
- 자바스크립트
- 파이썬
- 알고리즘
- JavaScript
- 비동기
- context switching
- node.js
- Zerobase
- 컴퓨터공학
- Python
- useState
- Computer Science
- 개발공부
- 글또
- algorithm
- REACT
- 코드스테이츠
- 운영체제
- execution context
- Today
- Total
Back to the Basics
[Design Pattern] IoC & DI Pattern을 구현해보자 본문
[Design Pattern] IoC & DI Pattern을 구현해보자
9Jaeng 2024. 7. 18. 01:51개요
Spring이나 NestJS 같은 프레임워크에서는 클래스들 간의 종속성을 IoC 컨테이너에 의해 주입받는다. (IoC 컨테이너와 종속성 주입에 대한 개념은 아래에서 간단히 설명) 이를 통해 클래스들이 필요한 기능을 직접 생성하지 않고 외부에서 주입받음으로써 객체 간 결합도를 낮출 수 있다.
그럼 이런 기능은 어떻게 구현이 될까? Java Spring이나 NestJs모두 런타임에 메타데이터를 활용하여 객체의 생성과 종속성 관리를 수행한다. Nestjs는 reflect-metadata 라는 라이브러리를 사용하고 Spring은 Java의 리플랙션 API를 사용한다. (이것이 무엇인지는 아래 간략하게 정리하였다)
메타데이터를 어떻게 활용하는지 확인하고 IoC DI에 대해 조금 더 깊이 있는 이해를 위해 직접 구현해보았다.
목차는 다음과 같다
1. IoC & DI의 개념 및 알아야 할 개념
2. IoC & DI 구현 방법
3. 구현 코드
4. 마무리
1. IoC & DI의 개념 및 알아야 할 개념
IoC와 DI
제어의 역전(Inversion of Control, IoC)과 의존성 주입(Dependency Injection, DI)은 소프트웨어 디자인 패턴으로, 클래스의 의존성을 프레임워크가 관리하고 주입해주는 것을 의미한다.
- 제어의 역전(IoC): 클래스의 의존성을 프로그래머가 아닌 프레임워크에서 관리하는 것을 의미한다.
- 의존성 주입(DI): 클래스 내부에서 의존성을 생성하는 대신, 외부에서 주입하는 것을 의미한다. 그리고 이 외부를 IoC 컨테이너라고 한다. IoC 컨테이너는 주입되는 클래스의 인스턴스를 싱글톤으로 관리하고, 클래스에 필요한 의존성을 주입해주는 기능을 한다.
위의 글을 요약하자면, 제어의 역전과 의존성 주입은 IoC 컨테이너를 통해 클래스의 의존성을 관리하고 주입해주는 것을 의미한다.
자세한 내용은 이 포스팅에 정리 해놓았다.
reflect-metadata 패키지
개요 부분에서 언급을 했듯이 IoC DI는 런타임에 메타데이터를 이용하여 객체의 생성과 의존성 관리를 한다고 했다. Nestjs는 이를 통해 필요한 객체의 데이터를 메타데이터로 등록하고 이를 활용하는 방법으로 구현한다.
reflect-metadata 패키지는 본래 NodeJs에 내장된 Reflect Api를 확장한다. 참고로 Reflect Api는 ES6 표준으로 객체의 프로퍼티를 다루는 메서드를 제공한다. 그리고 reflect-metadata는 이 Reflect 네임스페이스를 확장하여 메타데이터 관리 기능을 추가하는 패키지이다.
Reflect는 이미 정의된 속성을 다루기 위한 API를 제공해주지만 reflect-metadata는 이미 정의된 프로퍼티 뿐 아니라 추가적인 메타데이터를 추가하기 위해 제안된 개념이라고 한다 ECMAScript 제안서 를 통해 관련 내용을 확인해볼 수 있다.
그럼 reflect란 무엇일까? java도 Reflection 이라는 API를 통해 메타데이터를 사용하는데 이 Reflection이라는 것은 간단하게 말하면 "프로그램이 실행 중에 자신을 검사하고 수정할 수 있는 능력"을 의미한다. 이를 통해 객체의 메타데이터(클래스, 메서드, 속성 등)를 런타임에 동적으로 분석하고 조작할 수 있다고 한다. 지금은 간단하게만 알아보고 TODO로 정리를 해보자..
Reflection 관련 내용은 여기를 참고하자
참고로 reflect-metadata 을 사용하려면 아래와 같이 해주어야한다
- tsconfig.json 에서 아래의 옵션을 활성화해야 한다.
"emitDecoratorMetadata": true, // TypeScript 컴파일러는 emitDecoratorMetadata 옵션을 사용하여 생성자 매개변수 타입 정보를 자동으로 메타데이터로 생성한다. "experimentalDecorators": true
- index.ts 최상위에 reflect-metadata를 import해주어야 한다
import "reflect-metadata";
2. IoC & DI 구현 방법
- 개발 환경 : IoC, DI는 Nodejs의 ExpressJS framework를 기반으로 하였고 typescript 환경에서 구현하였다.
- IoC 와 DI를 구현하기 위해 필요한 기능은 아래와 같다.
- IoC Container class
- 핵심적인 기능인 IoC Container의 역할을 하는 클래스이다. 한 개만 존재하면 되기 때문에 싱글톤으로 구성하였다.
- IoC의 기능은 간단하게 말해 1.instance를 생성하고 2.요청이 있을 때 instance를 반환하는 역할을 한다.
- IoC Controller는 의존성 instance들을 관리해야하므로 이를 담을 배열 변수가 필요하다
- 인자로 받은 class 생성자의 instance를 생성하는 함수가 필요하다 (IoC container에 등록하는 메서드)
- 생생자를 인자로 받아서 그 생성자에 해당하는 instance를 반환 하는 함수가 필요하다
- Decorator
- Decorator를 사용하여 서버 실행시 의존성주입에 필요한 메타데이터를 등록할 수 있다.
- IoC, DI에 필요한 최소한의 데코레이터는 아래와 같다 (Module, Controller 등의 데코레이터는 제외하였다.)
- Injectable decorator : 클래스를 IoC Container에 등록하는 역할을 한다.
- Inject decorator : 클래스의 생성자 매개변수에 종속성을 주입하는 역할을 한다.
constructor(@Inject(LoggerService) private loggerService: LoggerService) {}
- Get decorator : HTTP 메서드의 정보를 메타데이터로 저장하는 데코레이터
- 초기화 메서드
- setupController 메서드 :
- 라우터를 등록해주는 메서드가 필요하다
- 서버 초기화시 Controller들을 app에 등록하고 컨트롤러의 메서드들을 라우트 핸들러로 등록하는 초기화 메서드이다.
- setupController 메서드 :
3. 구현 코드
위의 구현 방법에 따라 작성한 코드는 아래와 같다
IoC Container
export class IoCContainer {
// 인스턴스를 저장하기 위한 Map 객체
private static instances = new Map<string, any>(); // string: key, any: value (instance)
// class 생성자를 받아서 그 매개변수들을 사용하여 인스턴스를 생성하고 instances에 저장
static register<T>(constructor: ClassType<T>): void {
const paramTypes =
Reflect.getMetadata("design:paramtypes", constructor) || [];
const dependencies = paramTypes.map((paramType: any) =>
IoCContainer.resolve(paramType)
);
IoCContainer.instances.set(
constructor.name,
new constructor(...dependencies)
);
}
//생성자를 인자로 받아서 이를 이용하여 생성자에 해당하는 인스턴스를 반환하는 메서드
//없다면 register를 하고 instance를 반환한다.
static resolve<T>(constructor: ClassType<T>): T {
const instance = IoCContainer.instances.get(constructor.name);
if (!instance) {
IoCContainer.register(constructor);
return IoCContainer.instances.get(constructor.name);
}
return instance;
}
}
코드를 설명하자면
- static instances
- Container class는 의존성 주입에 사용될 instance들을 관리하는 변수이다. 이 변수는 key를 받아서 key에 해당하는 instance를 저장한다.
- static으로 선언한 이유는 싱글톤으로 구현하기 위해서이다. static을 사용하면 인스턴스 없이도 사용할 수 있다. 또한 애플리케이션 전역에서 상태를 공유하고 관리할 수 있다.
- register
- 주석에 써있는 대로 인자로 생성를 받아서 그 생성자를 사용해서 instance를 생성하고 이를 instances 변수에 넣는 함수이다.
Reflect.getMetadata("design:paramtypes", constructor)
는 constructor의 parameter types를 가져온다. 그리고 이 매개별수 타입의 instance를 IoC Container에서 가져온다. (의존성을 가져오는 것)- 가져온 의존성을 가져왔다면 이것을 생성자의 인자로 넣어 instance를 만든다.
- 참고로 design:paramtypes 는 Reflect metadata에 등록되어있는 표준 메타데이터 키이며 메서드 또는 생성자의 매개변수 타입 정보를 나타낸다. typescript의 컴파일러는 이 키를 이용하여 매개변수 타입 배열을 저장한다. 이 배열은 생성자의 각 매개변수에 대한 타입 정보를 포함한다.
- resolve
- 이것도 주석에 써있는 대로 생성자를 인자로 받아서 이를 이용하여 생성자에 해당하는 인스턴스를 반환하는 메서드
- 만약 해당하는 instance가 없다면 IoCContainer의 register를 호출하여 생성하도록 한다. 이렇게 재귀적으로 호출하게 함으로써 연관된 Controller, Service, Repository 들의 의존성들을 모두 가져올 수 있게 된다.
- 예를 들면 userController에는 userService가 필요하고 Service는 userepository가 필요하다. 재귀적으로 돌면서 없다면 생성하고 가져오도록 함으로써 의존성읠 주입받도록 한다.
Decorator
- Injectable Decorator
// Class를 IoC Conrainer에 등록하기 위한 Decorator
export function Injectable(): ClassDecorator {
return (target: any) => {
IoCContainer.register(target);
};
}
- Injectable 데코레이터는 클래스가 IoC 컨테이너에 의해 관리될 수 있도록 한다.
- 위의 코드는 IoC Container의 register 메서드를 이용하여 클래스를 등록한다.
- ClassDecorator : 클래스에 적용되는 데코레이터의 타입이다.
- target은 데코레이터가 적용된 클래스의 생성자 함수이다.
- ClassDecorator 를 반환하는 함수이며 class에 적용될 떄 호출된다.
- Inject Decorator
// 클래스의 생성자 매개변수에 종속성을 주입하는 데코레이터
export function Inject(type: any): ParameterDecorator {
return (
target: any,
propertyKey: string | symbol | undefined,
parameterIndex: number
) => {
const existingInjectedParams =
Reflect.getMetadata("custom:inject", target) || [];
existingInjectedParams.push({ index: parameterIndex, type });
// 데코레이터는 매개변수의 인덱스와 타입을 메타데이터로 저장
Reflect.defineMetadata("custom:inject", existingInjectedParams, target);
};
}
- Inject 데코레이터는 클래스의 생성자 매개변수에 종속성을 주입한다.
- Reflect.defineMetadata를 사용하여 생성자 매개변수의 인덱스와 타입을 메타데이터로 저장한다.
- ParameterDecorator
- 클래스 생성자 또는 메서드의 매개변수에 적용되는 데코레이터 타입이다.
- target은 데코레이터가 적용된 객체이다.
- propertyKey는 데코레이터가 적용된 메서드의 이름이다. 생성자의 경우 undefined일 수 있다.
- parameterIndex는 데코레이터가 적용된 매개변수의 인덱스이다.
- Get Decorator
// 해당 데코레이터가 적용된 HTTP 메서드의 정보를 메타데이터로 저장한다. // 이 정보는 나중에 Express 서버 설정시 사용되며 라우트를 등록하도록 사용된다. export function Get(path: string): MethodDecorator { // MethodDecorator : HTTP 요청을 처리 할 메서드를 데코레이트 하기 위해 사용 return ( target: any, // 데코레이터가 적용된 클래스의 프로토타입을 가리킨다. propertyKey: string | symbol, // 데코레이터가 적용된 메서드의 이름이다. descriptor: PropertyDescriptor ) => { // 만약 routes 라는 메타데이터 키가 없다면 빈 배열을 메타 데이터로 설정한다. // 라우트의 정보를 저장하기 위한 배열임 if (!Reflect.hasMetadata("routes", target.constructor)) { Reflect.defineMetadata("routes", [], target.constructor); } const routes = Reflect.getMetadata("routes", target.constructor); // routes 메타데이터를 가져와서 배열에 새로운 라우드 정보를 추가한다. routes.push({ method: "get", path, handleName: propertyKey, }); // 수정된 routes 배열을 다시 메타데이터로 설정한다. Reflect.defineMetadata("routes", routes, target.constructor); }; }
- HTTP 메서드의 정보를 메타데이터로 저장하는 데코레이터이다.
- "routes" 라는 키로 메타데이터를 가져온다. 없다면 생성하고 라우트 정보를 추가한다.
- MethodDecorator
- 클래스 메서드에 적용되는 데코레이터 타입이다.
- target은 데코레이터가 적용된 객체이다.
- propertyKey는 데코레이터가 적용된 메서드의 이름이다.
- descriptor는 데코레이터가 적용된 메서드의 프로퍼티 디스크립터이다.(메서드 속성 설명자)
- 속성 설명자란 메서드의 속성을 설명하는 객체이다. 이 객체는 메서드의 속성을 나타내는 다양한 속성을 가지고 있다.
- setupController 메서드
export function setupController( app: express.Express, controllers: any[] ): void { controllers.forEach((controller) => { const instance = IoCContainer.resolve(controller) as any; const routes = Reflect.getMetadata("routes", controller); // Get decorator에 의해 설정됨 routes.forEach((route: any) => { const handler = instance[route.handleName].bind(instance); switch (route.method) { case "get": app.get(route.path, handler); break; // post등 추가시 여기에 추가 default: break; } }); }); }
- 서버 초기화시 Controller들을 app에 등록하고 컨트롤러의 메서드들을 라우트 핸들러로 등록하는 초기화 메서드이다.
- 서버 시작시 클래스 정의시 데코레이터에 의해 등록된 IoC인스턴스를 가져와서 라우트를 등록한다.
- 없다면 IoC 컨테이너에 등록한다.
- 실행
서버를 실행하면 클래스들이 먼저 정의된다. 그리고 데코레이터들은 클래스가 정의될 때 실행이 된다. 동시에 IoC에 필요한 의존성 instance들을 생성한다.
그 후 setupController이 실행되면서 의존성읠 주입해줌과 동시에 path에 해당하는 라우트를 등록한다.
4. 마무리
지금까지 간단하게 DI를 구현해 보았다. 라우팅을 해주는 부분도 데코레이터를 추가하여 고도화가 필요하지만 이번 목적은 DI를 구현하는 것이었기 때문에 더 진행하지는 않았다. 직접 구현해 보면서 메타데이터를 어떻게 등록하고 사용하는지 확인할 수 있었다. 스프링에서도 메타데이터를 활용하여 의존서 주입을 구현하는 것 처럼 메타데이터를 이용한 프로그래밍은 다른 언어를 사용할 떄에도 중요한 개념이라는 생각이 든다.
전체 코드는 여기서 볼 수 있다