Resolution Planning — 조회 캐시와 스코프 라우팅

작성일:2026.05.09|조회수:4

Resolution Planning — 조회 캐시와 스코프 라우팅

앞선 파트에서 bootstrapModule()이 모듈 그래프를 컴파일하고 provider를 Container에 등록하는 과정을 살펴봤다. 이번 파트에서는 그 다음 단계, 즉 “이 토큰을 어떻게 찾고, 어느 캐시에 저장하며, 어떤 스코프로 라우팅할 것인가”를 결정하는 Resolution Planning 레이어를 분석한다.

실제 인스턴스를 만드는 로직은 다음 파트의 주제다. 그 전에 fluo는 꽤 많은 사전 계획을 수행한다. 이 planning이 없으면 매 resolve() 호출마다 컨테이너 계층을 반복 탐색해야 하고, alias chain이나 request scope 여부도 매번 처음부터 계산해야 한다. 더 나쁘게는 provider graph가 바뀐 뒤 낡은 조회 결과를 계속 믿는 문제가 생길 수 있다.

Resolution Planning은 이 두 문제를 동시에 다룬다. 한쪽에서는 provider lookup, multi provider aggregation, request-scope verdict, effective provider resolution을 캐시한다. 다른 한쪽에서는 registration, override, dispose처럼 그래프를 바꾸는 작업이 일어날 때 캐시를 안전하게 무효화한다.

이 글에서 보는 코드는 대부분 “객체를 만들기 직전”의 준비 작업이다. 그래서 겉으로는 단순한 Map 조회처럼 보이지만, 실제로는 fluo DI의 성능과 scope 안전성을 받치는 조용한 레이어에 가깝다.

1. 캐시 무효화 기반: graphRevisionlineageRevision

fluo 컨테이너의 조회 캐시는 단순한 Map<Token, Value>가 아니다. 각 캐시 엔트리는 CachedResolutionPlan<T> 형태로 저장된다.

TS
interface CachedResolutionPlan<T> {
  readonly lineageRevision: string;
  readonly value: T;
}

여기서 핵심은 lineageRevision이다. 컨테이너는 자기 자신의 graphRevision 값을 가지고 있고, provider 등록·override·dispose처럼 provider graph를 바꿀 수 있는 일이 일어나면 이 값을 증가시킨다. 단일 컨테이너만 있다면 숫자 하나로 충분하지만, fluo의 컨테이너는 parent/child 계층을 가진다. request scope는 root container의 child로 만들어지고, child는 parent provider를 볼 수 있다.

그래서 fluo는 현재 컨테이너 하나의 revision만 보지 않고, 부모 체인의 revision을 이어 붙인 문자열을 만든다.

TS
private currentLineageRevision(): string {
  const parentRevision = this.parent?.currentLineageRevision();

  return parentRevision
    ? `${parentRevision}/${this.graphRevision}`
    : String(this.graphRevision);
}

예를 들어 root의 revision이 3이고 request child의 revision이 1이라면 child의 lineage는 3/1이 된다. root에 provider가 추가되어 root revision이 4가 되면 child lineage도 4/1로 바뀐다. child 자신이 바뀌면 3/2처럼 뒤쪽 숫자가 바뀐다.

캐시를 읽을 때는 저장된 lineage와 현재 lineage가 일치하는지 확인한다.

TS
private readCachedPlan<T>(
  cache: Map<Token, CachedResolutionPlan<T>>,
  token: Token,
): CachedResolutionPlan<T> | undefined {
  const cached = cache.get(token);

  if (!cached || cached.lineageRevision !== this.currentLineageRevision()) {
    return undefined;
  }

  return cached;
}

이 구조의 장점은 명확하다. 컨테이너 계층 어디에서든 provider graph가 바뀌면 lineage 문자열이 달라진다. 그러면 기존 캐시 엔트리는 자동으로 stale 상태가 된다. 별도의 observer나 parent-child invalidation 이벤트를 만들지 않아도, 캐시를 읽는 순간 현재 graph와 맞지 않는 결과를 걸러낼 수 있다.

쓰기 쪽도 단순하다.

TS
private writePlanCache<T>(
  cache: Map<Token, CachedResolutionPlan<T>>,
  token: Token,
  value: T,
): T {
  cache.set(token, {
    lineageRevision: this.currentLineageRevision(),
    value,
  });

  return value;
}

이 패턴은 “계산한 값”과 “그 값을 계산한 graph 버전”을 함께 저장한다. 따라서 provider lookup 결과뿐 아니라 “없다”는 결과도 안전하게 캐시할 수 있다. 어떤 token이 지금은 없다는 사실은 현재 graph에서만 참이다. 이후 register나 override가 일어나면 lineage가 바뀌고, 그 “없음” 캐시는 더 이상 사용되지 않는다.

캐시 무효화는 명시적으로도 수행된다.

TS
private advanceGraphRevision(): void {
  this.graphRevision += 1;
  this.clearResolutionPlanCaches();
}

private clearResolutionPlanCaches(): void {
  this.providerLookupPlanCache.clear();
  this.multiProviderPlanCache.clear();
  this.requestScopeVerdictPlanCache.clear();
  this.effectiveProviderPlanCache.clear();
}

advanceGraphRevision()은 register, override, dispose처럼 graph 의미가 바뀔 수 있는 지점에서 호출된다. 즉 fluo는 두 겹으로 방어한다. 현재 컨테이너의 plan cache는 즉시 비우고, parent/child lineage가 달라지는 경우에는 read 시점에 stale entry를 걸러낸다.

2. 4가지 Plan Cache

Container는 provider 조회 과정에서 반복 계산을 줄이기 위해 planning 결과를 4개의 Map에 나누어 저장한다. 모두 같은 목적의 캐시처럼 보이지만, 실제로는 서로 다른 질문에 답한다.

providerLookupPlanCache

단일 token 조회 결과를 캐시한다.

TXT
token -> NormalizedProvider | undefined

이 캐시는 “이 token에 대응되는 single provider가 보이는가?”라는 질문에 답한다. lookupProvider()는 먼저 현재 컨테이너의 registrations를 보고, 없으면 parent로 올라간다. 이 탐색 결과는 같은 token에 대해 반복될 가능성이 높으므로 캐시된다.

여기서 undefined도 중요한 값이다. provider가 없다는 사실도 계산 결과다. 없는 token을 계속 resolve하려는 상황에서 매번 parent chain을 끝까지 타지 않아도 된다. 다만 이 undefined는 현재 lineage에서만 유효하다. 이후 provider가 등록되면 graph revision이 바뀌고 캐시는 무효화된다.

multiProviderPlanCache

Multi provider 조회 결과를 캐시한다.

TXT
token -> readonly NormalizedProvider[]

Multi provider는 single provider보다 계산이 더 비싸다. 현재 컨테이너만 보면 되는 것이 아니라, parent chain의 multi providers를 모아서 하나의 배열로 만들어야 하기 때문이다. 게다가 override가 있으면 parent aggregation을 끊어야 한다.

따라서 fluo는 multi provider 배열을 한 번 계산하면 freeze된 readonly 배열로 저장한다. 반환할 때는 복사본을 돌려주어 외부 코드가 내부 캐시 배열을 직접 변형하지 못하게 한다.

requestScopeVerdictPlanCache

특정 token을 resolve할 때 request scope가 필요한지 여부를 캐시한다.

TXT
token -> boolean

이 캐시는 HTTP adapter 같은 상위 레이어에서 특히 중요하다. 어떤 controller나 handler를 실행하기 전에, 그 graph 안에 request-scoped provider가 있는지 알고 싶을 수 있다. 매 요청마다 dependency graph를 DFS로 다시 훑는 것은 낭비다.

hasRequestScopedDependency(token)은 provider graph를 따라가며 request scope가 필요한지 계산하고, 그 boolean verdict를 캐시에 저장한다. provider 자체가 request scope일 수도 있고, alias나 dependency chain을 따라가다 request-scoped provider에 도달할 수도 있다.

effectiveProviderPlanCache

Alias chain을 따라간 최종 provider를 캐시한다.

TXT
token -> NormalizedProvider | undefined

useExisting provider는 자기 자신이 값을 만들지 않고 다른 token으로 위임한다. 따라서 scope mismatch 검사를 하거나 provider graph를 분석하려면, alias token 자체가 아니라 최종적으로 도달하는 concrete provider를 알아야 한다.

effectiveProviderPlanCache는 이 결과를 저장한다. 같은 alias chain을 반복해서 따라가지 않아도 되고, alias cycle도 visited set으로 감지할 수 있다.

TS
private resolveEffectiveProvider(
  token: Token,
  visited = new Set<Token>(),
  chain: Token[] = [],
): NormalizedProvider | undefined {
  const cacheable = visited.size === 0 && chain.length === 0;
  // ... alias chain을 따라가며 최종 provider를 찾는다
}

이 네 캐시는 미리 warm-up되지 않는다. 실제 조회가 발생하는 순간 필요한 항목만 lazy하게 채워진다. DI container는 대부분의 애플리케이션에서 전체 provider graph를 한 번에 다 쓰지 않는다. Demand-driven cache는 이 특성과 잘 맞는다.

3. lookupProvider: 로컬 우선, 부모 체인 폴백

단일 provider lookup은 가장 기본적인 planning 동작이다.

TS
private lookupProvider(token: Token): NormalizedProvider | undefined {
  const cached = this.readCachedPlan(this.providerLookupPlanCache, token);

  if (cached) {
    return cached.value;
  }

  const local = this.registrations.get(token);
  const provider = local ?? this.parent?.lookupProvider(token);

  return this.writePlanCache(this.providerLookupPlanCache, token, provider);
}

로직은 단순하다. 먼저 현재 컨테이너의 registrations를 본다. 있으면 local provider가 이긴다. 없으면 parent에게 같은 질문을 넘긴다. 이것이 request child override가 동작하는 기본 원리다. child에 같은 token이 등록되어 있으면 child가 우선하고, 없으면 root provider가 보인다.

이 lookup은 “visible provider”를 찾는 작업이지, provider를 실행하는 작업은 아니다. 여기서는 instance cache를 건드리지 않는다. 단지 resolution 단계에서 사용할 NormalizedProvider record를 찾아 줄 뿐이다.

undefined를 캐시하는 점도 다시 중요하다. 다음과 같은 상황을 생각해 볼 수 있다.

TXT
request child -> parent -> root

어떤 token이 세 계층 어디에도 없다면, 매번 root까지 올라갔다가 실패하는 것은 낭비다. providerLookupPlanCache는 “현재 lineage에서 이 token은 보이지 않는다”는 결론을 저장한다. 이후 root에 provider가 등록되면 root revision이 바뀌고, child lineage도 바뀌므로 이 결론은 자동으로 폐기된다.

이 함수가 짧은 이유는 앞 단계의 registration policy가 엄격하기 때문이다. 같은 token에 single provider와 multi provider를 섞는 충돌은 등록 시점에 막힌다. lookup 단계는 이미 token 의미가 애매하지 않다고 가정하고 단일 provider만 찾으면 된다.

4. collectMultiProviders: 오버라이드 인식 누적

Multi provider는 단일 lookup과 다르다. 하나의 token에 여러 provider가 붙을 수 있고, parent와 child의 provider 배열을 합쳐야 한다. 플러그인, interceptor, middleware chain 같은 구조에서는 이 누적 순서가 public contract가 된다.

TS
private collectMultiProviders(token: Token): NormalizedProvider[] {
  const cached = this.readCachedPlan(this.multiProviderPlanCache, token);

  if (cached) {
    return [...cached.value];
  }

  const local = this.multiRegistrations.get(token);
  let providers: readonly NormalizedProvider[];

  if (this.multiOverriddenTokens.has(token)) {
    providers = Object.freeze([...(local ?? [])]);
  } else {
    const fromParent = this.parent ? this.parent.collectMultiProviders(token) : [];
    providers = Object.freeze(local ? [...fromParent, ...local] : [...fromParent]);
  }

  this.writePlanCache(this.multiProviderPlanCache, token, providers);
  return [...providers];
}

기본 규칙은 parent 먼저, local 나중이다. Root에 등록된 multi providers가 먼저 배열에 들어가고, child에서 추가한 provider가 뒤에 붙는다. 이 순서가 중요한 이유는 multi provider가 보통 “순서 있는 실행 목록”으로 사용되기 때문이다.

반면 override()가 등장하면 의미가 달라진다. multiOverriddenTokens에 token이 들어 있으면 parent chain을 더 이상 누적하지 않는다. 현재 컨테이너의 local multi providers만 반환한다. 이것이 “append”와 “replace”의 차이다.

이 정책은 테스트나 request-local customization에서 중요하다. 어떤 요청 안에서만 plugin 목록을 완전히 바꾸고 싶을 때, parent 목록을 계속 끌고 오면 의도한 격리가 깨진다. 반대로 단순히 하나를 추가하고 싶을 때는 parent 목록을 보존해야 한다.

캐시 저장 시 Object.freeze()를 사용하는 것도 작은 안전장치다. 내부 캐시 배열은 불변으로 보관하고, 호출자에게는 return [...providers]로 복사본을 준다. Resolution Planning의 결과가 accidental mutation으로 오염되지 않게 하기 위한 선택이다.

5. cacheFor: 스코프에 따른 캐시 라우팅

Provider를 찾았다면 다음 질문은 이것이다.

이 provider의 인스턴스 Promise를 어느 Map에 저장해야 하는가?

이 결정을 담당하는 함수가 cacheFor()다.

TS
private cacheFor(provider: NormalizedProvider): Map<Token, Promise<unknown>> {
  if (provider.scope === Scope.DEFAULT) {
    if (this.requestScopeEnabled && this.registrations.has(provider.provide)) {
      return this.requestCacheForWrite();
    }

    return this.root().singletonCache;
  }

  if (!this.requestScopeEnabled) {
    throw new RequestScopeResolutionError(
      `Request-scoped provider ${formatTokenName(provider.provide)} cannot be resolved outside request scope.`,
      {
        token: provider.provide,
        scope: 'request',
        hint: 'Wrap the resolve call inside a request-scoped child container created via container.createRequestScope().',
      },
    );
  }

  return this.requestCacheForWrite();
}

결정 트리는 다음처럼 정리할 수 있다.

TXT
provider.scope === DEFAULT:
  if current container is request child and provider is local:
    use request cache
  else:
    use root singleton cache

provider.scope === REQUEST:
  if current container is not request scope:
    throw RequestScopeResolutionError
  use request cache

Default scope provider는 일반적으로 singleton으로 취급되어 root singleton cache에 저장된다. request child에서 parent의 singleton provider를 resolve하더라도 root cache를 공유한다. 그래야 request마다 singleton이 새로 만들어지는 일이 없다.

하지만 예외가 있다. request-scope child에 local로 등록된 default provider는 root singleton cache가 아니라 child의 request cache에 저장된다. 코드 주석에서는 이것을 “Singleton-in-request-scope” footgun으로 설명한다.

This is intentional — it allows test and override scenarios to inject short-lived values without polluting the global cache — but the divergence from the declared scope is a known footgun.

즉 이 동작은 버그가 아니라 의도된 절충이다. 테스트나 요청 단위 override에서는 전역 singleton cache를 오염시키지 않는 local replacement가 필요하다. 다만 사용자가 request child를 두 번째 root처럼 사용해 singleton provider를 직접 등록하면, 이름과 달리 request lifetime에 묶인 값이 된다.

Request-scoped provider는 더 엄격하다. root container에서 request provider를 resolve하려 하면 cache miss가 아니라 RequestScopeResolutionError가 난다. Request scope는 단순한 cache bucket이 아니라 child container라는 구조적 boundary이기 때문이다.

Multi provider도 같은 개념을 별도 helper로 적용한다.

TS
private multiCacheFor(provider: NormalizedProvider): Map<NormalizedProvider, Promise<unknown>> {
  if (provider.scope === Scope.DEFAULT) {
    if (this.requestScopeEnabled && this.hasLocalMultiProvider(provider)) {
      return this.multiRequestCacheForWrite();
    }

    return this.root().multiSingletonCache;
  }

  if (!this.requestScopeEnabled) {
    throw new RequestScopeResolutionError(...);
  }

  return this.multiRequestCacheForWrite();
}

Single provider cache는 token을 key로 쓰지만, multi provider cache는 NormalizedProvider object를 key로 쓴다. 같은 token 아래 여러 provider entry가 존재할 수 있기 때문이다. Token 하나에 여러 인스턴스가 매달리는 구조에서는 provider record 자체가 identity가 된다.

6. shouldResolveFromRoot: 위임 판단

cacheFor()가 cache map을 고르는 함수라면, shouldResolveFromRoot()는 request child에서 root singleton resolution을 위임할지 판단하는 함수다.

TS
private shouldResolveFromRoot(provider: NormalizedProvider): boolean {
  return provider.scope === Scope.DEFAULT
    && this.requestScopeEnabled
    && !this.registrations.has(provider.provide);
}

이 조건은 다음 문장으로 읽을 수 있다.

나는 request-scope container이고, 이 provider는 singleton(default scope)이며, 내 local registration이 아니다.

즉 provider가 parent/root에서 온 singleton이라면 현재 child에서 만들지 말고 root로 보내야 한다. 그래야 모든 request child가 같은 singleton identity를 공유한다.

예를 들어 root에 ConfigService가 등록되어 있고 request child에서 UserController를 resolve한다고 하자. UserControllerConfigService를 필요로 하더라도, request child는 ConfigService를 자기 cache에 새로 만들면 안 된다. root singleton cache에 있는 값을 써야 한다.

반대로 request child가 override({ provide: ConfigService, ... })로 local replacement를 등록했다면 this.registrations.has(provider.provide)가 true가 된다. 이 경우 root로 위임하지 않는다. 현재 request boundary 안에서만 보이는 override가 사용된다.

이 작은 predicate가 request scope의 핵심 균형을 만든다.

shouldResolveMultiProviderFromRoot()도 같은 구조를 multi provider에 적용한다. 다만 multi provider는 local 여부를 token map이 아니라 hasLocalMultiProvider(provider)로 판단한다.

7. hasRequestScopedDependency: Pre-flight 스코프 검사

상위 runtime은 어떤 token을 resolve하기 전에 request scope가 필요한지 알고 싶을 수 있다. 예를 들어 HTTP adapter는 controller graph 안에 request-scoped provider가 있다면 요청마다 child container를 만들어야 한다. 매번 무조건 child를 만드는 것도 가능하지만, 필요 여부를 미리 알 수 있다면 더 정확한 실행 계획을 세울 수 있다.

이때 사용하는 API가 hasRequestScopedDependency(token)이다.

TS
hasRequestScopedDependency(token: Token): boolean {
  const cached = this.readCachedPlan(this.requestScopeVerdictPlanCache, token);

  if (cached) {
    return cached.value;
  }

  return this.writePlanCache(
    this.requestScopeVerdictPlanCache,
    token,
    this.providerGraphRequiresRequestScope(token, new Set<Token>()),
  );
}

실제 판단은 providerGraphRequiresRequestScope()가 수행한다.

TS
private providerGraphRequiresRequestScope(token: Token, visited: Set<Token>): boolean {
  if (visited.has(token)) {
    return true;
  }

  visited.add(token);

  try {
    const provider = this.lookupProvider(token);
    const multiProviders = this.collectMultiProviders(token);

    if (!provider && multiProviders.length === 0) {
      return this.unregisteredClassRequiresRequestScope(token, visited);
    }

    if (provider && this.normalizedProviderRequiresRequestScope(provider, visited)) {
      return true;
    }

    return multiProviders.some((multiProvider) =>
      this.normalizedProviderRequiresRequestScope(multiProvider, visited),
    );
  } finally {
    visited.delete(token);
  }
}

이 DFS는 몇 가지 경우를 모두 다룬다.

  1. provider 자체가 request scope인지 확인한다.
  2. provider의 inject entries를 따라간다.
  3. alias provider라면 useExisting target으로 계속 이동한다.
  4. multi providers가 있으면 각 entry를 확인한다.
  5. 등록되지 않은 class token이라면 class metadata의 scope와 inject 정보를 본다.
  6. optional dependency가 없으면 request scope 필요 없음으로 처리한다.

순환 그래프를 만나면 보수적으로 true를 반환한다. 이는 “순환이면 request scope가 필요하다”는 의미라기보다, pre-flight 분석에서 더 내려가 판단할 수 없는 graph를 안전한 쪽으로 분류한다는 뜻이다. 이후 실제 resolve 단계에서는 별도의 circular dependency detector가 더 정확한 에러를 낸다.

이 verdict cache는 특히 반복 호출에서 효과가 있다. 같은 controller나 provider token에 대해 request scope 필요 여부를 매번 DFS로 계산하지 않아도 된다. 단, provider graph가 바뀌면 requestScopeVerdictPlanCache도 다른 plan cache와 함께 무효화된다.

8. 에러 분류

Planning 단계에서는 provider 정의를 읽고, 컨테이너에 등록 가능한지, 이후 resolution 단계에서 안전하게 사용할 수 있는지를 검증한다. 이 단계에서 발생하는 에러는 대부분 “인스턴스를 만들 수 없음”이 아니라 “의존성 그래프를 구성하거나 해석할 수 없음”을 의미한다.

InvalidProviderError

Provider 정의 자체가 유효하지 않을 때 발생한다. 예를 들어 provider object에 provide token이 없거나, useClass, useValue, useFactory, useExisting 같은 strategy가 여러 개 동시에 선언된 경우가 여기에 해당한다.

이 에러는 가능하면 registration 시점에 터져야 한다. 잘못된 provider가 graph 안에 들어오면 이후 lookup, scope planning, instance creation이 모두 애매해지기 때문이다.

ContainerResolutionError

컨테이너가 이미 dispose된 뒤 register/override/resolve/createRequestScope를 시도하거나, resolve 과정에서 token에 대응되는 provider를 찾지 못할 때 발생한다.

두 경우는 성격이 조금 다르다. Dispose 이후 작업은 lifecycle boundary 위반이다. Missing provider는 graph visibility 또는 registration 누락 문제다. 하지만 둘 다 “현재 컨테이너 상태에서는 resolution을 계속할 수 없다”는 점에서 같은 계열의 에러로 묶인다.

DuplicateProviderError

동일 token에 대해 충돌하는 provider가 등록될 때 발생한다. 대표적으로 single provider와 multi provider를 같은 token에 혼용하거나, override() 한 번 안에서 같은 single token을 여러 provider로 대체하려는 경우가 있다.

이 에러가 registration/override 단계에서 엄격하게 발생해야 resolver가 단순해진다. Resolver는 token이 single인지 multi인지 매번 추측하지 않는다. 앞 단계에서 충돌을 막았기 때문에, resolution 단계는 정해진 의미대로 빠르게 분기할 수 있다.

ScopeMismatchError

Scope 관계가 안전하지 않을 때 발생한다. 예를 들어 request-scope child container에 singleton provider를 직접 register()하려 하면 이 에러가 난다. Root singleton 등록은 request scope를 만들기 전에 root에 있어야 한다.

또 다른 경우는 singleton provider가 request-scoped provider에 의존하는 상황이다. 이 검사는 실제 instance creation 직전에 수행되지만, 본질적으로는 lifetime graph의 안전성 문제다. 긴 lifetime의 객체가 짧은 request lifetime 값을 붙잡지 못하게 막는다.

RequestScopeResolutionError

Request-scoped provider를 request boundary 없이 resolve하려고 할 때 발생한다. Provider scope가 request인데 현재 container가 request-scope child가 아니라면, fluo는 cache miss로 처리하지 않고 명확한 에러를 던진다.

이 에러는 request scope가 단순한 provider option이 아니라 child container라는 구조임을 보여 준다. Request-scoped provider는 container.createRequestScope()로 만든 컨텍스트 안에서 resolve되어야 한다.

마치며

Resolution Planning은 fluo DI의 조용한 성능 기반이다. lineageRevision 기반의 자동 캐시 무효화, 4가지 plan cache, local-first provider lookup, parent-aware multi aggregation, scope별 cache routing이 맞물려 resolve 비용을 줄인다.

동시에 이 레이어는 안전장치이기도 하다. Provider graph가 바뀌면 낡은 plan cache를 버리고, request scope가 필요한 graph는 pre-flight에서 감지하며, singleton과 request boundary가 섞이지 않도록 cache route를 분리한다. cacheFor()의 singleton-in-request-scope 동작은 override 유연성을 위한 의도적 설계지만, 이해하지 못하면 발을 밟기 쉬운 함정이기도 하다.

정리하면 Planning은 “아직 객체를 만들지 않는 단계”지만, 실제 객체 생성의 의미를 거의 결정한다. 어떤 provider를 볼 것인지, multi list를 어떻게 합칠 것인지, alias를 어디까지 따라갈 것인지, 어느 cache에 저장할 것인지가 모두 여기서 정해진다.

다음 파트에서는 이 계획이 실제 실행으로 바뀌는 지점을 본다. resolveWithChain(), 순환 의존성 감지, instantiate()의 4가지 provider 타입 분기를 따라가며 resolve() 호출이 어떻게 new MyClass(...deps)까지 이어지는지 살펴본다.

더 읽어보기

댓글

댓글을 불러오는 중...