티스토리 뷰

728x90

 

 

2025.03.24-[DI] DI가 어떻게 동작하는지 소스코드 해부하기(feat. typeDI)

typeDI의 컨테이너 관련 글에 이어 singleton 서비스가 전역으로 적용되는 메커니즘에 대해 알아봅니다.

 

 

🌠 목차
✅ 내부 코드에서의 의존성 주입 흐름
✅ Singleton 서비스가 전역이 되는 메커니즘
✅ 실제 작동 예시

 

의존성 주입 흐름


@Service()데코레이터가 호출될 때 어떤 방식으로 의존성 주입이 되는지 알아봅니다.

내부 흐름과 구현되어 있는 코드를 간략히 요약했습니다.

 

1. @Service() 호출

2. ContainerInstance().set() 호출하여 서비스 등록

3. 서비스 메타데이터가 컨테이너의 metadataMap에 저장

4. Container.get()이 호출되면 저장된 메타데이터 기반으로 인스턴스 생성/반환

 

 

아래는 구현 코드입니다.

정확히 다 이해하기 위해선 인터페이스를 구성하는 모든 필드값들과 모든 소스코드를 보여줘야겠지만,

그렇게되면 코드가 너무 길어져서 핵심 코드만 공유드립니다. 

* 코드 보기가 어렵다면 옵션은 생략하고 보시면 되겠습니다.

* ex: 옵션off @Service(), 옵션on @Service({scope: 'transient})

 

Service 데코레이터 구현 코드 

export function Service<T = unknown>(): Function;
export function Service<T = unknown>(options: ServiceOptions<T>): Function;
export function Service<T>(options: ServiceOptions<T> = {}): ClassDecorator {
  return targetConstructor => {
    const serviceMetadata: ServiceMetadata<T> = {
      id: options.id || targetConstructor,
      type: targetConstructor as unknown as Constructable<T>,
      factory: (options as any).factory || undefined,
      multiple: options.multiple || false,
      eager: options.eager || false,
      scope: options.scope || 'container',
      referencedBy: new Map().set(ContainerRegistry.defaultContainer.id, ContainerRegistry.defaultContainer),
      value: EMPTY_VALUE,
    };

    ContainerRegistry.defaultContainer.set(serviceMetadata);
  };
}

1. 데코레이터 함수 선언

2. 데코레이터 구현

2-1. 클래스 데코레이터 반환

2-2. 기본값으로 빈 옵션 객체 사용

3. 서비스 메타데이터 생성

     id: 서비스 식별자 

     type: 실제 클래스의 생성자

     factory: 사비스 생성을 위한 팩토리 함수

     multiple: 다중 인스턴스화 여부

     eager: 즉시 인스턴스화 여부

     scope: 서비스 범위

     referencedBy: 서비스를 참조하는 컨테이너 정보

     value: 초기값

4. 컨테이너 등록

 

ContainerInstance의 set 함수

public set<T = unknown>(serviceOptions: ServiceOptions<T>): this {
    this.throwIfDisposed();

    /**
     * If the service is marked as singleton, we set it in the default container.
     * (And avoid an infinite loop via checking if we are in the default container or not.)
     */
    if (serviceOptions.scope === 'singleton' && ContainerRegistry.defaultContainer !== this) {
      ContainerRegistry.defaultContainer.set(serviceOptions);

      return this;
    }

    const newMetadata: ServiceMetadata<T> = {
      /**
       * Typescript cannot understand that if ID doesn't exists then type must exists based on the
       * typing so we need to explicitly cast this to a `ServiceIdentifier`
       */
      id: ((serviceOptions as any).id || (serviceOptions as any).type) as ServiceIdentifier,
      type: (serviceOptions as ServiceMetadata<T>).type || null,
      factory: (serviceOptions as ServiceMetadata<T>).factory,
      value: (serviceOptions as ServiceMetadata<T>).value || EMPTY_VALUE,
      multiple: serviceOptions.multiple || false,
      eager: serviceOptions.eager || false,
      scope: serviceOptions.scope || 'container',
      /** We allow overriding the above options via the received config object. */
      ...serviceOptions,
      referencedBy: new Map().set(this.id, this),
    };

    /** If the incoming metadata is marked as multiple we mask the ID and continue saving as single value. */
    if (serviceOptions.multiple) {
      const maskedToken = new Token(`MultiMaskToken-${newMetadata.id.toString()}`);
      const existingMultiGroup = this.multiServiceIds.get(newMetadata.id);

      if (existingMultiGroup) {
        existingMultiGroup.tokens.push(maskedToken);
      } else {
        this.multiServiceIds.set(newMetadata.id, { scope: newMetadata.scope, tokens: [maskedToken] });
      }

      /**
       * We mask the original metadata with this generated ID, mark the service
       * as  and continue multiple: false and continue. Marking it as
       * non-multiple is important otherwise Container.get would refuse to
       * resolve the value.
       */
      newMetadata.id = maskedToken;
      newMetadata.multiple = false;
    }

    const existingMetadata = this.metadataMap.get(newMetadata.id);

    if (existingMetadata) {
      /** Service already exists, we overwrite it. (This is legacy behavior.) */
      // TODO: Here we should differentiate based on the received set option.
      Object.assign(existingMetadata, newMetadata);
    } else {
      /** This service hasn't been registered yet, so we register it. */
      this.metadataMap.set(newMetadata.id, newMetadata);
    }

    /**
     * If the service is eager, we need to create an instance immediately except
     * when the service is also marked as transient. In that case we ignore
     * the eager flag to prevent creating a service what cannot be disposed later.
     */
    if (newMetadata.eager && newMetadata.scope !== 'transient') {
      this.get(newMetadata.id);
    }

    return this;
  }

set 함수의 흐름

1. 싱글톤 처리

2. 새로운 메타데이터 생성(기본값들을 설정하고, 사용자 정의 옵션으로 덮어씀)

3. 다중 서비스 처리(고유 토큰 발급)

4. 기존서비스 업데이트 또는 새 서비스 등록

5. eager 즉시 생성 처리 

 

MetadataMap 필드 값과 구현 내부 

private metadataMap: Map<ServiceIdentifier, ServiceMetadata<unknown>> = new Map();

------------------------------------------------------------------------------------
export type ServiceIdentifier<T = unknown> =
  | Constructable<T>
  | AbstractConstructable<T>
  | CallableFunction
  | Token<T>
  | string;
------------------------------------------------------------------------------------
export interface ServiceMetadata<Type = unknown> {
  /** Unique identifier of the referenced service. */
  id: ServiceIdentifier;

  /**
   * The injection scope for the service.
   *   - a `singleton` service always will be created in the default container regardless of who registering it
   *   - a `container` scoped service will be created once when requested from the given container
   *   - a `transient` service will be created each time it is requested
   */
  scope: ContainerScope;

  /**
   * Class definition of the service what is used to initialize given service.
   * This property maybe null if the value of the service is set manually.
   * If id is not set then it serves as service id.
   */
  type: Constructable<Type> | null;

  /**
   * Factory function used to initialize this service.
   * Can be regular function ("createCar" for example),
   * or other service which produces this instance ([CarFactory, "createCar"] for example).
   */
  factory: [Constructable<unknown>, string] | CallableFunction | undefined;

  /**
   * Instance of the target class.
   */
  value: unknown | Symbol;

  /**
   * Allows to setup multiple instances the different classes under a single service id string or token.
   */
  multiple: boolean;

  /**
   * Indicates whether a new instance should be created as soon as the class is registered.
   * By default the registered classes are only instantiated when they are requested from the container.
   *
   * _Note: This option is ignored for transient services._
   */
  eager: boolean;

  /**
   * Map of containers referencing this metadata. This is used when a container
   * is inheriting it's parents definitions and values to track the lifecycle of
   * the metadata. Namely, a service can be disposed only if it's only referenced
   * by the container being disposed.
   */
  referencedBy: Map<ContainerIdentifier, ContainerInstance>;
}

 

 

Singleton 서비스가 전역이 되는 메커니즘


어떤 컨테이너에서 싱글톤 서비스를 등록하려 해도, 항상 기본 컨테이너에 저장됩니다.

 

등록 시  항상 기본 컨테이너에 등록

container-instance.class.ts의 set 함수 일부 

 

public set<T = unknown>(serviceOptions: ServiceOpions<T>): this {
	this.throwIfDisposed();
    if(serviceOptions.scope === 'singleton' && COntainereRegistry.defaultContainer !== this) {
    	ContainerRegistry.defaultContainer.set(serviceOptions);
        return this;
    }

이 코드의 의미

  • 어떤 컨테이너에서 singleton 서비스를 등록하려고 해도
  • 해당 서비스는 항상 기본 컨테이너(ContainerRegistry.defaultContainer)에 등록됨
  • 이렇게 하면 서비스 인스턴스가 애플리케이션 전체에서 단 하나만 존재하게 됨 

 

 

조회 시 항상 기본 컨테이너에서 먼저 확인

container-instance.class.ts의 get 함수 일부

public get<T = unknown>(identifier: ServiceIdentifier<T>): T {
	this.thorwIfDisposed();
    
    const global = ContainerRegistry.defaultContainer.metadataMap.get(identifier)
    const local = this.metadataMap.get(identifer);
    
    const metadata = global?.scope === 'singleton' ? global : local;
}

이 코드의 의미

  • 서비스를 조회할 때 항상 기본 컨테이너를 먼저 확인
  • 기본 컨테이너에 singleton으로 등록된 서비스가 있으면 그것을 반환
  • 로컬 컨테이너에 있는 버전보다 항상 기본 컨테이너의 singleton이 우선됨

 

실제 작동 예시


 

// 데이터베이스 연결은 싱글톤으로 설정
@Service({ scope: 'singleton' })
class Database {
  id = Math.random();  // 인스턴스마다 고유한 ID
  
  connect() {
    console.log(`데이터베이스 ${this.id} 연결`);
  }
}

// 여러 컨테이너에서 사용해도 같은 인스턴스
const db1 = Container.get(Database);
db1.connect();  // "데이터베이스 0.123456 연결"

const userContainer = new ContainerInstance('user');
const db2 = userContainer.get(Database);
db2.connect();  // "데이터베이스 0.123456 연결" (같은 ID)

// 두 인스턴스는 동일함
console.log(db1 === db2);  // true

 

 

 

글 잼나게 보셨으면 좋아요 눌러주고 가세요

관심받는 거 좋아합니다

 

728x90
댓글
250x250
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday