심화 가이드
의존성 주입

의존성 주입

의존성 주입(Dependency Injection, DI)은 클래스 안에서 인스턴스를 직접 생성하지 않고, 외부에서 주입받는 방식으로 애플리케이션 전역에서 코드를 관리하고 공유하는 디자인 패턴입니다.

팁: 이 문서에서 다루는 내용을 읽기 전에 Angular 핵심을 먼저 보는 것이 좋습니다.

애플리케이션이 커지면서 개발자가 코드를 재사용하거나 서로 다른 부분에서 공유해야 하는 경우가 있습니다. 의존성 주입(DI)을 활용하면 클래스 안에서 의존성을 직접 생성하지 않고 외부에서 제공받을 수 있기 때문에 이런 요구사항을 해결할 수 있습니다. 그래서 애플리케이션의 각 부분을 재사용하기 쉬워지고 관리하기도 편해집니다.

의존성 주입은 다음과 같은 일반적인 문제를 해결할 수 있기 때문에 널리 사용됩니다:

  • 코드의 유지보수성 향상: 의존성 주입 패턴을 활용하면 관심사의 분리 원칙에 따라 코드를 분리하기 때문에 코드 중복을 방지하고 리팩토링하기 편합니다.
  • 확장성: 기능을 모듈화하면 여러 곳에서 재사용할 수 있기 때문에 확장하기 편합니다.
  • 테스트 편의성: 의존성 주입 패턴을 활용하면 실제 동작하는 구현 로직 대신 테스트 더블을 활용할 수 있기 때문에 유닛 테스트하기 편합니다.

Angular에서 의존성 주입이 어떻게 동작하나요?

의존성이란 클래스가 동작하는 데 필요하지만 클래스 안에서 직접 생성하지 않는 객체, 값, 함수, 서비스를 의미합니다. 의존성은 클래스 외부에서 제공하며, 이를 통해 애플리케이션의 각 부분 사이에 명확한 관계가 만들어집니다.

코드가 의존성 주입 시스템과 상호작용하는 방법은 두 가지입니다:

  • 값을 제공(provide) 해서 사용할 수 있게 만들 수 있습니다.
  • 그렇게 제공된 값을 의존성으로 요청해서 주입(inject) 받을 수 있습니다.

이때 "값"은 객체, 함수, 클래스 인스턴스를 포함한 모든 JavaScript 값을 의미합니다. 보통은 이런 것들이 의존성이 됩니다:

  • 설정값: 환경변수, API URL, 기능 스위치 플래그 등
  • 팩토리: 실행환경 상태에 따라 객체나 값을 생성하는 함수
  • 서비스: 어떤 로직을 구현하거나 비즈니스 로직, 상태를 관리하는 클래스

Angular 컴포넌트와 디렉티브는 기본적으로 의존성 주입 시스템의 일부이기 때문에, 의존성 객체를 주입받을 수 있고, 의존성으로 주입될 수도 있습니다.

서비스가 무엇인가요?

Angular에서 이야기하는 서비스(service) 란 @Service 데코레이터가 붙은 TypeScript 클래스이며, 이 클래스를 인스턴스로 생성하면 의존성으로 주입할 수 있습니다. 서비스는 애플리케이션 전역에서 데이터와 기능을 공유하는 용도로 가장 많이 사용됩니다.

일반적으로 서비스는 이런 역할을 합니다:

  • 데이터 요청: 서버에 데이터를 요청하거나 변형하는 과정을 추상화합니다.
  • 상태 관리: 컴포넌트나 화면에서 공유하는 상태값을 선언하고 관리합니다.
  • 인증, 권한 부여: 사용자 인증을 처리하고 토큰을 보관하며 접근 권한을 관리합니다.
  • 로그, 오류 처리: 오류 상태를 로그로 출력하거나 사용자에게 알리기 위한 API를 마련합니다.
  • 이벤트 처리, 전달: 컴포넌트가 처리하지 않는 이벤트를 공통으로 처리하거나, 옵저버 패턴(observer pattern)에 따라 컴포넌트에 이벤트를 전달합니다.
  • 유틸리티 함수: 데이터 형식 변환, 유효성 검사, 계산과 같은 재사용 유틸리티 함수를 제공합니다.

아래 코드는 AnalyticsLogger라는 서비스의 예제 코드입니다:

import {Service} from '@angular/core';

@Service()
export class AnalyticsLogger {
  trackEvent(category: string, value: string) {
    console.log('Analytics event logged:', {
      category,
      value,
      timestamp: new Date().toISOString(),
    });
  }
}

참고: @Service 데코레이터를 지정하면 서비스를 전역 범위에서 사용할 수 있도록 인스턴스를 싱글턴으로 생성합니다. 대부분의 경우 이런 사용 방식을 권장합니다.

참고: @Service 데코레이터는 @Injectable({providedIn: 'root'})를 간편하게 작성하는 단축 문법입니다.

inject()로 의존성 주입하기

의존성 객체는 Angular가 제공하는 inject() 함수로 주입할 수 있습니다.

아래 코드는 AnalyticsLogger와 Angular Router를 의존성 객체로 주입받아서 이벤트에 따라 화면을 전환하는 네비게이션 바 컴포넌트의 예제 코드입니다.

import {Component, inject} from '@angular/core';
import {Router} from '@angular/router';
import {AnalyticsLogger} from './analytics-logger';

@Component({
  selector: 'app-navbar',
  template: `<a href="#" (click)="navigateToDetail($event)">Detail Page</a>`,
})
export class Navbar {
  private router = inject(Router);
  private analytics = inject(AnalyticsLogger);

  navigateToDetail(event: Event) {
    event.preventDefault();
    this.analytics.trackEvent('navigation', '/details');
    this.router.navigate(['/details']);
  }
}

inject()는 어디에 사용할 수 있나요?

의존성 객체는 컴포넌트, 디렉티브, 서비스를 생성할 때 주입할 수 있습니다. inject 함수를 직접 사용하거나 constructor() 함수에서 프로퍼티로 선언하면 됩니다:

@Component(/* ... */)
export class MyComponent {
  // ✅ 클래스 필드 초기화로 주입
  private service = inject(MyService);

  // ✅ 생성자에서 주입
  private anotherService: MyService;

  constructor() {
    this.anotherService = inject(MyService);
  }
}
@Directive({...})
export class MyDirective {
  // ✅ 클래스 필드 초기화로 주입
  private element = inject(ElementRef);
}
import {Service, inject} from '@angular/core';
import {HttpClient} from '@angular/common/http';

@Service()
export class MyService {
  // ✅ 서비스에서 주입
  private http = inject(HttpClient);
}
export const authGuard = () => {
  // ✅ 라우팅 규칙 가드에서 주입
  const auth = inject(AuthService);
  return auth.isAuthenticated();
};

inject를 사용할 수 있는 코드 범위를 "의존성 주입 컨텍스트(injection context)"라고 합니다. 일반적으로는 컴포넌트, 디렉티브, 서비스의 생성자가 의존성 주입 컨텍스트 입니다.

자세한 내용은 inject API 문서를 참고하세요.

다음 단계

이제 Angular의 의존성 주입의 기본을 학습했습니다. 서비스를 직접 만들어 보세요.

다음 가이드 문서에서는 서비스를 생성하고 이런 내용을 다룹니다:

  • Angular CLI나 수동으로 서비스를 생성하는 방법
  • providedIn: 'root' 패턴이 동작하는 방식
  • 서비스를 컴포넌트나 다른 서비스로 의존성 주입하는 방법

Angular 애플리케이션에서 서비스를 사용하는 방법 중 가장 기본을 알아봅시다.