"일꾼이 일을 잘하려면 먼저 도구를 갈고 닦아야 한다." - 공자, 『논어』.
첫 장 > 프로그램 작성 > TypeScript의 TSyringe 및 종속성 주입

TypeScript의 TSyringe 및 종속성 주입

2024년 10월 31일에 게시됨
검색:480

TSyringe and Dependency Injection in TypeScript

저는 NestJS와 같은 대형 프레임워크를 별로 좋아하지 않습니다. 나는 가벼운 방식으로 내가 결정한 구조로 원하는 방식으로 소프트웨어를 구축할 수 있는 자유를 항상 좋아했습니다. 하지만 NestJS를 테스트할 때 마음에 들었던 점은 종속성 주입이었습니다.

종속성 주입(DI)은 클래스에서 종속성을 생성하고 관리하는 책임을 제거하여 느슨하게 결합된 코드를 개발할 수 있는 디자인 패턴입니다. 이 패턴은 유지 관리, 테스트 및 확장 가능한 애플리케이션을 작성하는 데 중요합니다. TypeScript 생태계에서 TSyringe는 이 프로세스를 단순화하는 강력하고 가벼운 종속성 주입 컨테이너로 돋보입니다.

TSyringe는 TypeScript/JavaScript 애플리케이션을 위한 경량 종속성 주입 컨테이너입니다. Microsoft가 GitHub(https://github.com/microsoft/tsyringe)에서 유지관리하며 데코레이터를 사용하여 생성자 주입을 수행합니다. 그런 다음 Inversion of Control 컨테이너를 사용하여 인스턴스 또는 값으로 교환할 수 있는 토큰을 기반으로 종속성을 저장합니다.

종속성 주입 이해

TSyringe에 대해 자세히 알아보기 전에 종속성 주입이 무엇인지, 왜 중요한지 간략하게 살펴보겠습니다.

종속성 주입은 객체가 자체적으로 생성하는 대신 외부 소스로부터 종속성을 받는 기술입니다. 이 접근 방식은 다음과 같은 여러 가지 이점을 제공합니다.

  1. 향상된 테스트 가능성: 단위 테스트에서 종속성을 쉽게 조롱하거나 스텁할 수 있습니다.
  2. 모듈성 향상: 구성 요소가 더욱 독립적이며 쉽게 교체하거나 업데이트할 수 있습니다.
  3. 더 나은 코드 재사용성: 애플리케이션의 여러 부분에서 종속성을 공유할 수 있습니다.
  4. 유지관리성 향상: 종속성에 대한 변경 사항은 종속 코드에 최소한의 영향을 미칩니다.

T주사기 설정

먼저 TypeScript 프로젝트에서 TSyringe를 설정해 보겠습니다.

npm install tsyringe reflect-metadata

tsconfig.json에 다음 옵션이 있는지 확인하세요.

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

애플리케이션 진입점에서 반영 메타데이터 가져오기:

import "reflect-metadata";

애플리케이션의 진입점은 예를 들어 Next.js 13의 루트 레이아웃이거나 작은 Express 애플리케이션의 기본 파일일 수 있습니다.

TSyringe를 사용하여 종속성 주입 구현

소개의 예를 들어 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)
...

이 예는 각 클래스가 다른 클래스에만 의존하기 때문에 매우 간단합니다. 그러나 우리 서비스는 많은 클래스에 의존할 수 있으며 종속성 주입은 모든 것을 깔끔하게 유지하는 데 정말 도움이 됩니다.

하지만 잠깐만요! 나를 그렇게 두지 마세요! 테스트는 어때요?

TSyringe를 사용한 테스트

우리의 주입은 모의 개체를 종속성으로 직접 보내 코드를 테스트하는 데도 도움이 될 수 있습니다. 코드 예제를 살펴보겠습니다:

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 토큰에는 종속 클래스에 주입될 모의 객체가 포함되어 있습니다.

모범 사례 및 팁

  1. 인터페이스 사용: 종속성을 쉽게 교체하고 테스트할 수 있도록 인터페이스를 정의합니다. 이 글에서는 단순화를 위해 인터페이스를 사용하지 않았지만 인터페이스는 생명입니다.
  2. 순환 종속성 방지: TSyringe에 문제를 일으킬 수 있는 순환 종속성을 방지하도록 코드를 구성하세요.
  3. 이름 지정에 토큰 사용: 주입 토큰에 문자열 리터럴을 사용하는 대신 상수 토큰을 만듭니다.

    export const USER_REPOSITORY_TOKEN = Symbol("UserRepository");
    
    
  4. 범위 컨테이너: 웹 애플리케이션의 요청 범위 종속성에 대해 범위 컨테이너를 사용합니다.

  5. DI를 과도하게 사용하지 마세요: 모든 것을 주입할 필요는 없습니다. 교차 문제 및 구성 가능한 종속성을 위해 DI를 사용합니다.

여기까지 오셨다면 읽어주셔서 감사하다는 말씀드리고 싶습니다. 이 기사가 도움이 되었기를 바랍니다. 종속성 주입 및 아키텍처 패턴을 구현할 때는 항상 프로젝트의 특정 요구 사항을 고려하는 것을 잊지 마세요.

좋아요와 댓글 피드백은 개선을 위한 가장 좋은 방법입니다.

즐거운 코딩하세요!

릴리스 선언문 이 글은 https://dev.to/gdsources/tsyringe-and-dependent-injection-in-typescript-3i67?1에서 복제됩니다.1 침해 내용이 있는 경우, [email protected]으로 연락하여 삭제하시기 바랍니다.
최신 튜토리얼 더>

부인 성명: 제공된 모든 리소스는 부분적으로 인터넷에서 가져온 것입니다. 귀하의 저작권이나 기타 권리 및 이익이 침해된 경우 자세한 이유를 설명하고 저작권 또는 권리 및 이익에 대한 증거를 제공한 후 이메일([email protected])로 보내주십시오. 최대한 빨리 처리해 드리겠습니다.

Copyright© 2022 湘ICP备2022001581号-3