저는 NestJS와 같은 대형 프레임워크를 별로 좋아하지 않습니다. 나는 가벼운 방식으로 내가 결정한 구조로 원하는 방식으로 소프트웨어를 구축할 수 있는 자유를 항상 좋아했습니다. 하지만 NestJS를 테스트할 때 마음에 들었던 점은 종속성 주입이었습니다.
종속성 주입(DI)은 클래스에서 종속성을 생성하고 관리하는 책임을 제거하여 느슨하게 결합된 코드를 개발할 수 있는 디자인 패턴입니다. 이 패턴은 유지 관리, 테스트 및 확장 가능한 애플리케이션을 작성하는 데 중요합니다. TypeScript 생태계에서 TSyringe는 이 프로세스를 단순화하는 강력하고 가벼운 종속성 주입 컨테이너로 돋보입니다.
TSyringe는 TypeScript/JavaScript 애플리케이션을 위한 경량 종속성 주입 컨테이너입니다. Microsoft가 GitHub(https://github.com/microsoft/tsyringe)에서 유지관리하며 데코레이터를 사용하여 생성자 주입을 수행합니다. 그런 다음 Inversion of Control 컨테이너를 사용하여 인스턴스 또는 값으로 교환할 수 있는 토큰을 기반으로 종속성을 저장합니다.
TSyringe에 대해 자세히 알아보기 전에 종속성 주입이 무엇인지, 왜 중요한지 간략하게 살펴보겠습니다.
종속성 주입은 객체가 자체적으로 생성하는 대신 외부 소스로부터 종속성을 받는 기술입니다. 이 접근 방식은 다음과 같은 여러 가지 이점을 제공합니다.
먼저 TypeScript 프로젝트에서 TSyringe를 설정해 보겠습니다.
npm install tsyringe reflect-metadata
tsconfig.json에 다음 옵션이 있는지 확인하세요.
{ "compilerOptions": { "experimentalDecorators": true, "emitDecoratorMetadata": true } }
애플리케이션 진입점에서 반영 메타데이터 가져오기:
import "reflect-metadata";
애플리케이션의 진입점은 예를 들어 Next.js 13의 루트 레이아웃이거나 작은 Express 애플리케이션의 기본 파일일 수 있습니다.
소개의 예를 들어 T주사기 설탕을 추가해 보겠습니다.
어댑터부터 시작해 보겠습니다.
// @/adapters/userAdapter.ts import { injectable } from "tsyringe" @injectable() class UserAdapter { constructor(...) {...} async fetchByUUID(uuid) {...} }
@injectable() 데코레이터가 보이시나요? 이 클래스가 런타임에 주입될 수 있음을 TSyringe에 알리는 것입니다.
그래서 내 서비스는 방금 만든 어댑터를 사용하고 있습니다. 해당 어댑터를 내 서비스에 주입해 보겠습니다.
// @/core/user/user.service.ts import { injectable, inject } from "tsyringe" ... @injectable() class UserService { constructor(@inject('UserAdapter') private readonly userAdapter: UserAdapter) {} async fetchByUUID(uuid: string) { ... const { data, error } = await this.userAdapter.fetchByUUID(uuid); ... } }
여기에서는 서비스가 내 명령 클래스에 주입될 것이기 때문에 @injectable 데코레이터도 사용했지만 생성자 매개변수에도 @inject 데코레이터를 추가했습니다. 이 데코레이터는 TSyringe에게 런타임에 userAdapter 속성에 대한 UserAdapter 토큰에 대한 인스턴스 또는 값을 제공하도록 지시합니다.
그리고 마지막으로 내 핵심의 루트인 명령 클래스(종종 유스케이스라고 잘못 불림)입니다.
// @/core/user/user.commands.ts import { inject } from "tsyringe" ... @injectable() class UserCommands { constructor(@inject('UserService') private readonly userService: UserService) {} async fetchByUUID(uuid) { ... const { data, error } = this.userService.fetchByUUID(uuid); ... } }
이 시점에서 우리는 TSyringe에게 무엇을 주입할 것인지, 무엇을 생성자에 주입할 것인지 알려 주었습니다. 하지만 우리는 아직 종속성을 저장할 컨테이너를 만들지 않았습니다. 두 가지 방법으로 이를 수행할 수 있습니다:
종속성 주입 레지스트리를 사용하여 파일을 만들 수 있습니다.
// @/core/user/user.dependencies.ts import { container } from "tsyringe" ... container.register("UserService", {useClass: UserService}) // associate the UserService with the token "UserService" container.register("UserAdapter", {useClass: UserAdapter}) // associate the UserAdapter with the token "UserAdapter" export { container }
하지만 @registry 데코레이터를 사용할 수도 있습니다.
// @/core/user/user.commands.ts import { inject, registry, injectable } from "tsyringe" ... @injectable() @registry([ { token: 'UserService', useClass: UserService }, { token: 'UserAdapter', useClass: UserAdapter }, ]) export class UserCommands { constructor(@inject('UserService') private readonly userService: UserService) {} async fetchByUUID(uuid) { ... const { data, error } = this.userService.fetchByUUID(uuid); ... } } container.register("UserCommands", { useClass: UserCommands}) export { container }
두 가지 방법 모두 장단점이 있지만 결국 취향의 문제입니다.
이제 컨테이너가 종속성으로 채워졌으므로 컨테이너의 해결 방법을 사용하여 필요에 따라 컨테이너에서 이를 가져올 수 있습니다.
import { container, UserCommands } from "@/core/user/user.commands" ... const userCommands = container.resolve("UserCommands") await userCommands.fetchByUUID(uuid) ...
이 예는 각 클래스가 다른 클래스에만 의존하기 때문에 매우 간단합니다. 그러나 우리 서비스는 많은 클래스에 의존할 수 있으며 종속성 주입은 모든 것을 깔끔하게 유지하는 데 정말 도움이 됩니다.
하지만 잠깐만요! 나를 그렇게 두지 마세요! 테스트는 어때요?
우리의 주입은 모의 개체를 종속성으로 직접 보내 코드를 테스트하는 데도 도움이 될 수 있습니다. 코드 예제를 살펴보겠습니다:
import { container, UserCommands } from "@/core/user/user.commands" describe("test ftw", () => { let userAdapterMock: UserAdapterMock let userCommands: UserCommands beforeEach(() => { userAdapterMock = new UserAdapter() container.registerInstance("UserAdapter", userAdapter) userCommands = container.resolve ("UserCommands") }); ... });
이제 UserAdapter 토큰에는 종속 클래스에 주입될 모의 객체가 포함되어 있습니다.
이름 지정에 토큰 사용: 주입 토큰에 문자열 리터럴을 사용하는 대신 상수 토큰을 만듭니다.
export const USER_REPOSITORY_TOKEN = Symbol("UserRepository");
범위 컨테이너: 웹 애플리케이션의 요청 범위 종속성에 대해 범위 컨테이너를 사용합니다.
DI를 과도하게 사용하지 마세요: 모든 것을 주입할 필요는 없습니다. 교차 문제 및 구성 가능한 종속성을 위해 DI를 사용합니다.
여기까지 오셨다면 읽어주셔서 감사하다는 말씀드리고 싶습니다. 이 기사가 도움이 되었기를 바랍니다. 종속성 주입 및 아키텍처 패턴을 구현할 때는 항상 프로젝트의 특정 요구 사항을 고려하는 것을 잊지 마세요.
좋아요와 댓글 피드백은 개선을 위한 가장 좋은 방법입니다.
즐거운 코딩하세요!
부인 성명: 제공된 모든 리소스는 부분적으로 인터넷에서 가져온 것입니다. 귀하의 저작권이나 기타 권리 및 이익이 침해된 경우 자세한 이유를 설명하고 저작권 또는 권리 및 이익에 대한 증거를 제공한 후 이메일([email protected])로 보내주십시오. 최대한 빨리 처리해 드리겠습니다.
Copyright© 2022 湘ICP备2022001581号-3