Angular 고급 DI 마스터하기: InjectionToken의 명시적 주입과 자동 주입 패턴 비교

Adam Kim·2026년 1월 14일

angular

목록 보기
94/102

Angular의 의존성 주입(DI) 시스템은 @Injectable 데코레이터를 사용하는 것 이상으로 훨씬 깊이 있는 기능을 제공합니다. 특히 InjectionToken을 활용하면, 클래스의 구현과 소비를 완전히 분리하여 최고 수준의 디커플링을 달성할 수 있습니다.

이 글에서는 InjectionToken을 사용하여 @Injectable 없는 클래스를 주입 가능하게 만드는 두 가지 강력한 패턴을 비교 분석합니다. 하나는 소비자가 명시적으로 Provider를 등록하는 방식이고, 다른 하나는 토큰 자체가 자신을 제공하여 소비자가 아무 설정도 할 필요 없는 방식입니다.

DI의 기반: 주입할 클래스 정의

두 패턴을 비교하기 전에, 우리가 주입할 대상인 순수 TypeScript 클래스를 먼저 정의하겠습니다. 이 클래스는 내부적으로 inject()를 사용하여 HttpClient에 의존합니다.

data-processor.ts

import { HttpClient } from '@angular/common/http';
import { inject } from '@angular/core';
import { Observable } from 'rxjs';

// @Injectable이 없는 순수 클래스. 두 패턴에서 공통으로 사용됩니다.
export class DataProcessor {
  private http = inject(HttpClient);
  private endpoint = 'https://jsonplaceholder.typicode.com/todos/1';

  fetch(): Observable<any> {
    console.log('Fetching data from DataProcessor...');
    return this.http.get(this.endpoint);
  }
}

패턴 1: 명시적 Provider를 이용한 수동 등록 방식

이 패턴은 "책임의 분리"를 강조합니다. 클래스 파일은 클래스와 함께 주입 "레시피"(Provider)를 제공하고, 애플리케이션 설정 파일(app.config.ts)은 그 레시피를 사용할지 여부를 명시적으로 결정하고 등록합니다.

1. 토큰과 Provider 정의 (data-processor-explicit.ts)

import { InjectionToken, Provider } from '@angular/core';
import { DataProcessor } from './data-processor'; // 공통 클래스 import

// 1. DI 시스템에서 사용할 고유 키(Token)
export const DATA_PROCESSOR_TOKEN = new InjectionToken<DataProcessor>('DataProcessor');

// 2. 주입 방법을 정의하는 레시피(Provider)
export const DATA_PROCESSOR_PROVIDER: Provider = {
  provide: DATA_PROCESSOR_TOKEN,
  useClass: DataProcessor
};

이 파일은 DATA_PROCESSOR_TOKEN과 DATA_PROCESSOR_PROVIDER를 함께 내보냅니다.

2. app.config.ts에서 Provider 등록

애플리케이션이 이 서비스를 사용하려면, providers 배열에 DATA_PROCESSOR_PROVIDER를 직접 추가해야 합니다.

app.config.ts (패턴 1)

import { ApplicationConfig } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
// Provider를 직접 import 합니다.
import { DATA_PROCESSOR_PROVIDER } from './data-processor-explicit';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(),
    
    // 명시적으로 Provider를 등록하는 과정이 필수입니다.
    DATA_PROCESSOR_PROVIDER,
  ],
};

패턴 2: providedIn: 'root'를 이용한 자동 등록 방식

이 패턴은 "최고의 편의성"과 "완벽한 캡슐화"를 추구합니다. 토큰 자체가 자신을 루트 인젝터에 등록하는 방법을 알고 있으므로, 소비자는 아무런 DI 설정을 할 필요가 없습니다. @Injectable({ providedIn: 'root' })와 동일한 철학입니다.

1. 스스로를 제공하는 토큰 정의 (data-processor-auto.ts)

import { InjectionToken } from '@angular/core';
import { DataProcessor } from './data-processor'; // 공통 클래스 import

// 토큰 생성자에서 Provider 정보를 모두 정의합니다.
export const DATA_PROCESSOR_TOKEN_AUTO = new InjectionToken<DataProcessor>(
  'DataProcessor Auto',
  {
    // ⭐️ 이 토큰이 루트 인젝터에 자동으로 등록되도록 합니다.
    providedIn: 'root',
    
    // ⭐️ 이 토큰이 요청될 때 실행될 팩토리 함수입니다.
    factory: () => new DataProcessor(),
  }
);

이제 별도의 Provider 객체가 필요 없으며, 이 파일은 오직 DATA_PROCESSOR_TOKEN_AUTO 하나만 내보냅니다.

2. app.config.ts - 설정 불필요!

이 패턴을 사용할 때, app.config.ts에서는 DataProcessor와 관련된 어떤 설정도 추가할 필요가 없습니다.

app.config.ts (패턴 2)

import { ApplicationConfig } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(),
    // DataProcessor 관련 설정이 전혀 필요 없습니다.
  ],
};

컴포넌트에서의 사용법: 두 패턴 모두 동일

놀랍게도, 소비자인 컴포넌트의 코드는 어떤 패턴을 사용하든 완전히 동일합니다. 오직 어떤 토큰을 import하는지만 다를 뿐입니다.

app.component.ts

import { Component, inject } from '@angular/core';
// 패턴 1을 사용한다면:
// import { DATA_PROCESSOR_TOKEN } from './data-processor-explicit';
// 패턴 2를 사용한다면:
import { DATA_PROCESSOR_TOKEN_AUTO } from './data-processor-auto';

@Component({ ... })
export class AppComponent {
  // 어떤 토큰을 사용하든 주입 코드는 동일합니다.
  private processor = inject(DATA_PROCESSOR_TOKEN_AUTO); // 또는 DATA_PROCESSOR_TOKEN

  constructor() {
    this.processor.fetch().subscribe(console.log);
  }
}

어떤 패턴을 언제 사용해야 할까?

특징패턴 1 (명시적 Provider)패턴 2 (자동 등록 토큰)
설정 방식소비자가 app.config.ts에 Provider를 수동 등록providedIn: 'root'를 통해 자동 등록 (설정 불필요)
결합도상대적으로 높음 (설정 파일이 Provider를 알아야 함)최소화 (소비자는 토큰만 알면 됨)
명시성매우 높음. app.config.ts만 보면 어떤 서비스가 제공되는지 명확히 알 수 있음상대적으로 낮음 ("마법"처럼 보일 수 있음)
주요 사용 사례- 앱의 최상단에서 DI 구성을 중앙 집중적으로 관리하고 싶을 때
- 환경별로 다른 구현체(useClass, useValue 등)를 쉽게 교체해야 할 때
재사용 가능한 라이브러리를 만들 때
- 모듈이나 서비스가 "그냥 작동(just work)"하도록 만들고 싶을 때

결론

두 패턴 모두 @Injectable 데코레이터를 사용하지 않고도 클래스를 주입 가능하게 만드는 강력한 방법입니다.

  • 명시적 Provider 패턴은 모든 DI 설정을 중앙에서 통제하고 싶을 때 유리하며, 코드의 흐름을 이해하기 쉽다는 장점이 있습니다.
  • 자동 등록 토큰 패턴은究極의 디커플링과 재사용성을 제공하여, 특히 라이브러리 제작자에게 필수적인 기술입니다.

프로젝트의 아키텍처와 요구사항에 따라 적절한 패턴을 선택하면 더 깔끔하고, 유연하며, 확장 가능한 Angular 애플리케이션을 구축할 수 있습니다.

profile
Angular2+ Developer

0개의 댓글