2022. 2. 10. 13:27ㆍAngular/Information
의존성 주입을 위해서는 프로바이더가 있어야 한다는 것을 이전 글을 통해 알았을 것이다.
이 프로바이더에 대해 사용하는 방법과 여러 종류를 알아보고자 한다.
프로바이더(Provider), 사용 방법
프로바이더는 컴포넌트 레벨과 모듈 레벨에서 사용할 수 있다.
Angular 6 이후에는 서비스 레벨에서 @Injectable 데코레이터에도 사용할 수 있다.
// @NgModule 프로바이더
@NgModule({
...
providers: [FooService]
})
// @Component 프로바이더
@Component({
...
providers: [FooService]
})
// @Injectable 프로바이더
@Injectable({
providedIn: 'root'
})
@Injectable 데코레이터에서의 provideIn은 각 레벨에서 프로바이더를 설정한 것과 동일한 효과를 보여준다.
provideIn 프로퍼티를 root로 설정했다면 루트 모듈에 설정한 것과 동일하다.
이 값을 특정 모듈로도 설정할 수 있다.
// @Injectable 프로바이더
@Injectable({
providedIn: 'UserModule'
})
모듈에 프로바이더를 등록한 서비스는 해당 모듈의 모든 구성요소(루트 모듈인 경우에는 어플리케이션 전역이다.)에 주입할 수 있다.
서비스는 인젝터의 주입 범위 내에서 언제나 싱글턴 패턴으로 생성된다.
그러나 컴포넌트 인젝터는 독립적으로 동작하게 된다.
예를 들어 루트 모듈의 인젝터가 제공하는 서비스가 있을 때 같은 서비스를 컴포넌트의 프로바이더에 등록하게 되면 2개의 서비스가 생성되게 되고, 2개의 서비스를 각각 다른 컴포넌트에서 주입하게 된다.
아래 이미지를 보면서 이 말을 풀어보도록 하자.
ParentComponent와 ChildComponent 모두에게 같은 토큰(의존성으로 주입될 인스턴스를 검색하거나 생성할 때 사용하는 키)의 프로바이더가 등록되어 있고 모두 같은 인스턴스를 요청하는 경우, 각각 자신의 인젝터가 해당 컴포넌트에 다른 서비스를 생성하여 주입한다.
하지만, 프로바이더를 갖고 있지 않은 AppComponent가 주입을 요청할 경우 AppModule의 인젝터가 생성하여 주입할 것이다.
위 내용에 대해 참고하며 프로바이더를 등록할 때 여러개의 서비스를 생성하여 어플리케이션의 오류를 발생시키지 않도록 조심해야 한다.
프로바이더는 사용 방법에 따라 3가지로 분류될 수 있다.
첫번째, 클래스 프로바이더(Class Provider)
클래스 프로바이더는 가장 일반적인 프로바이더로 클래스 인스턴스를 주입하기 위한 것이다.
providers 프로퍼티에 아래와 같이 제공할 인스턴스의 클래스 리스트로 구성된 배열 값을 갖게 된다.
providers: [FooService]
이는 아래 코드가 축약된 표현이다.
providers: [{
provide: FooService, // 토큰
useClass: FooService // 의존성 인스턴스를 생성할 클래스
}]
가장 일반적으로 쓰이는 이유는 서비스가 생성될 때 클래스로 생성되기 때문이다.
providers에게 설정된 객체의 프로퍼티중 첫번째인 provide는 인젝터가 컨테이너에서 검색하거나 생성될 때 사용할 토큰 값을 뜻하는 것이고, useClass는 생성될 때 사용할 클래스다.
그렇다면 컴포넌트에서 FooService를 사용하기는 하지만, 대체 클래스를 사용해야 할 경우를 살펴보자.
먼저 원본 클래스를 살펴보면 아래와 같은 구조를 갖고 있다.
// foo.service.ts
import { Injectable } from '@angular/core';
@Injectable()
export class FooService {
foo() { return 'Boo'; }
}
FooService를 대체할 SecondFooService는 아래와 같다.
// second-foo.service.ts
import { Injectable } from '@angular/core';
@Injectable()
export class SecondFooService {
foo() { return 'Second boo'; }
}
SecondFooService를 FooService로 변환하고 싶다면 컴포넌트의 프로바이더를 아래와 같이 변경해주면 된다.
providers: [{
// 의존성 인스턴스의 타입(토큰, Token)
provide: FooService,
// 의존성 인스턴스를 생성할 클래스
useClass: SecondFooService
}]
위 코드는 SecondFooService 클래스로 생성한 인스턴스를 FooService 란 이름의 토큰으로 인젝터의 컨테이너에 등록하고 검색할 것 이라는 의미이다.
즉, 컴포넌트에서 의존성 주입을 요청할 때 FooService 타입의 인스턴스를 요청하게 되면 인젝터는 FooSerivce란 토큰으로 인스턴스를 검색하여 SecondFooService 클래스로 생성된 FooService 타입의 인스턴스를 주입할 것이다.
FooService와 SecondFooService는 같은 인터페이스를 공유하지 않았는데도 가능한 이유는 덕 타이핑(Duck typing)에 의해서다.
같은 메소드를 가지고 있기 때문에 같은 타입으로 인정되는 것이다.
FooService의 인스턴스를 주입받아 사용하던 컴포넌트가 SecondFooService의 인스턴스를 주입받아 사용하도록 변경되었지만, 컴포넌트는 문제 없이 동작할 것이다.
이는 느슨한 결합으로 되어 있어 그렇다.
하지만 프로바이더를 컴포넌트 레벨에 등록했기 때문에 FooService 타입의 SecondFooService 인스턴스는 컴포넌트 레벨로 생성된다.
만약 다른 모듈이나 컴포넌트에서 동일한 프로바이더가 존재하게 된다면 SecondFooService 인스턴스는 중복 생성되어 여러개의 인스턴스가 존재하게 된다.
SecondFooService 인스턴스를 싱글턴으로 공유하여 사용할 수 있도록 루트 모듈에 등록해주면 이 현상을 회피할 수 있다.
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { FooService } from './foo.service';
import { SecondFooService } from './second-foo.service';
@NgModule({
declarations: [ AppComponent ],
imports: [ BrowserModule ],
providers: [{
provide: FooService,
useClass: SecondFooService
}],
bootstrap: [ AppComponent ]
})
export class AppModule { }
두번째, 값 프로바이더(Value Provider)
위에서 알아본 프로바이더는 클래스를 주입할 때 필요했던 내용이다.
이제는 값을 주입할 때 사용할 프로바이더를 알아보고자 한다.
객체 리터럴과 같은 값을 의존성 주입하기 위한 설정이다.
// app.config.ts
// 주입 대상의 토큰
export class AppConfig {
apiUrl: string;
apiPort: number;
token: string;
}
// 주입 대상의 값
export const MY_APP_CONFIG: AppConfig = {
apiUrl: 'http://...',
apiPort: 8224,
token: 'q1w2e3r4t5y6'
};
주입할 값을 설정해주고, providers에 등록만 해주면 된다.
providers: [{
provide: AppConfig, // 토큰
useValue: MY_APP_CONFIG // 의존성 인스턴스를 생성할 값
}]
클래스 인스턴스를 주입할 때에는 useClass, 값을 주입할 때에는 useValue를 사용하면 된다.
...
export class AppComponent {
constructor(private appConfig: AppConfig) {
console.log(appConfig);
}
}
만약 주입할 값이 객체 리터럴 타입이 아니어도 문제 없이 가능하다.
다만, 토큰을 식별할 수 없을테니 토큰은 문자열로 구성해주면 되고, 주입할 값 또한 자신이 원하는 값을 넣으면 된다.
providers: [
{ provide: 'API_URL', useValue: 'http://...' },
{ provide: 'API_PORT', useValue: 8224 },
{ provide: 'isProduction', useValue: false }
]
사용할 때에는 @Inject 데코레이터를 이용하면 사용 가능하다.
...
export class AppComponent {
constructor(
@Inject('API_URL') public apiUrl: string,
@Inject('API_PORT') public apiPort: number,
@Inject('isProduction') public isProduction: boolean
) {
console.log(apiUrl);
console.log(apiPort);
console.log(isProduction);
}
}
위와 같이 사용은 가능하나, 토큰의 값이 문자열이기 때문에 다른 컴포넌트의 프로바이더와 중복적으로 선언되어 오류가 발생할 여지가 있다.
다른 컴포넌트의 프로바이더와 중복적으로 발생되지 않게 관리한다고 하여도, 외부 라이브러리와 중복되어 오류가 발생할 수 있다.
따라서 문자열로 토큰을 지정하는 방식이 아닌 인젝션 토큰(Injection Token) 방식으로 토큰을 지정해야 한다.
인젝션 토큰(Injection Token)
인젝션 토큰은 클래스(Class)가 아닌 의존성(no-class dependency) 토큰을 주입받기 위해 사용된다.
즉, 값 프로바이더를 사용할 때 객체 리터럴이 아닌 일반 값을 주입하고자 한다면 토큰을 문자열로 작성하게 되는데, 그렇게 진행했다가는 중복 위험이 있다
클래스가 아닌 인터페이스(Interface)로 설계하여 생성한 인터페이스를 토큰으로 주입하게 될 경우에는 오류가 발생하게 된다.
Angular의 기본 언어인 타입스크립트(TypeScript)는 빌드시 트랜스파일링(Transfiling) 과정을 거쳐 자바스크립트(JavaScript)로 변환되게 된다.
하지만, 자바스크립트는 인터페이스를 지원하지 않는다.
따라서 변환된 자바스크립트 파일에서 인터페이스는 사라지게 된다.
사라진 인터페이스를 Angular에서 토큰으로 등록할 수 없으니 오류가 발생하게 된다.
이러한 이유로 인젝션 토큰을 사용하기도 한다.
// app.config.ts
import { InjectionToken } from '@angular/core';
/**
* 값을 주입할 때(객체 리터럴)
*/
// 주입 대상의 토큰
export interface AppConfig {
apiUrl: string;
apiPort: number;
token: string;
}
// 주입 대상의 값
export const MY_APP_CONFIG: AppConfig = {
apiUrl: 'http://...',
apiPort: 8224,
token: 'q1w2e3r4t5y6'
};
// AppConfig 타입의 InjectionToken APP_CONFIG 생성
export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');
// 프로바이더
export const AppConfigProvider = {
provide: APP_CONFIG, // InjectionToken
useValue: MY_APP_CONFIG
};
/**
* 값을 주입할 때
*/
export const isProduction: boolean = false;
// Boolean 타입의 InjectionToken IS_PROD 생성
export const IS_PROD = new InjectionToken<boolean>('isProduction');
// 프로바이더
export const IsProductionProvider = {
provide: IS_PROD, // InjectionToken
useValue: isProduction
};
InjectionToken 클래스를 이용하여 인터페이스 AppConfig와 불리언 값을 갖고 있는 isProduction을 생성했다.
InjectionToken 내에 파라미터는 개발자를 위한 설명(description for developer)이다.
사용시에는 아래와 같이 사용해주면 된다.
// app.component.ts
import { Component, Inject } from '@angular/core';
import { AppConfig, APP_CONFIG, AppConfigProvider, IS_PROD, IsProductionProvider } from './app.config';
@Component({
selector: 'app-root',
template: '{{ appConfig | json }}, {{ isProduction }}'
providers: [ AppConfigProvider, IsProductionProvider ]
})
export class AppComponent {
constructor(
@Inject(APP_CONFIG) public appConfig: AppConfig,
@Inject(IS_PROD) public isProduction: boolean
) {
console.log(appConfig, isProduction);
}
}
세번째, 팩토리 프로바이더(Factory Provider)
의존성 인스턴스를 생성할 때 어떠한 로직을 거쳐야 한다면 팩토리 프로바이더를 이용해야 한다.
예를 들어 생성할 인스턴스를 조건에 따라 결정해야 하는 경우이다.
결제 시스템을 개발한다고 해보자.
개발 과정에서 결제 테스트를 진행할텐데, 이때마다 돈이 실제로 출금되고, 이를 다 취소하려면 이것 또한 귀찮은 일이 될 것이다.
그래서 개발자들은 가짜 서비스(MockService)를 만들어 이용하곤 한다.
// payment.ts
// 결제 정보 생성 클래스
export class Payment {
constructor(
public isRealPayment: boolean
) {}
}
// payment.service.ts
// 결제 정보 생성 서비스
import { Injectable } from '@angular/core';
import { Payment } from './payment';
@Injectable()
export class PaymentService {
// 실제 결제 정보를 생성하여 반환
getPaymentData(): Payment {
return new Payment(true);
}
}
// mock-payment.service.ts
// 테스트용 가상 결제 정보 생성 서비스
import { Injectable } from '@angular/core';
import { Payment } from './payment';
@Injectable()
export class MockPaymentService {
// 테스트용 가상 결제 정보를 생성하여 반환
getPaymentData(): Payment {
return new Payment(false);
}
}
// payment.service.provider.ts
import { MockPaymentService } from './mock-payment.service';
import { PaymentService } from './payment.service';
// 팩토리 함수
const paymentServiceFactory
= (isDev: boolean) => {
return isDev ? new MockPaymentService() : new PaymentService();
}
// 팩토리 프로바이더
export const PaymentServiceProvider = {
// 최종적으로 생성될 인스턴스의 타입
provide: PaymentService,
// 인스턴스 생성을 담당할 팩토리 함수
useFactory: paymentServiceFactory,
// 팩토리 함수에 주입할 값 프로바이더의 토큰
deps: ['isDev']
};
// 팩토리 함수에 주입할 값의 프로바이더
export const IsDevProvider = {
// 팩토리 함수에 주입할 값의 토큰
provide: 'isDev',
// 팩토리 함수에 주입할 값
useValue: false
};
useFactory를 이용해 인스턴스 생성을 담당할 팩토리 함수를 지정해주면 된다.
paymentServiceFactory 함수는 불리언 값 isDev를 받아 가짜 서비스를 제공할지, 실제 서비스를 제공할지 결정한다.
이 값은 deps 프로퍼티에 제공할 프로바이더 토큰을 지정하는 것으로 팩토리 함수에 자동 주입된다.
다시 말해 deps 프로퍼티는 팩토리 함수에 주입할 의존성의 토큰을 배열로 설정한다.
// app.component.ts
import { Component } from '@angular/core';
import { IsDevProvider, PaymentServiceProvider } from './payment.service.provider';
import { PaymentService } from './payment.service';
@Component({
selector: 'app-root',
template: '{{ paymentService.getPaymentData() | json }}',
providers: [
IsDevProvider, // 팩토리 함수에 주입할 값의 프로바이더
PaymentServiceProvider // 팩토리 프로바이더
]
})
export class AppComponent {
constructor(public paymentService: PaymentService) {
console.log(paymentService.getPaymentData());
}
}
'Angular > Information' 카테고리의 다른 글
[Angular] 의존성 주입(Dependency Injection, DI) 데코레이터 (0) | 2022.02.11 |
---|---|
[Angular] DI를 도와주는 인젝터(Injector), 인젝터 트리(Injector Tree) (0) | 2022.02.09 |
[Angular] Service, Dependency Injection(DI)을 왜 사용할까? (0) | 2022.02.09 |
[Angular] ngFor에 필요한 trackBy Directive로 만들기 (0) | 2022.02.08 |
[Angular] all error catch class 만들기 (0) | 2022.02.06 |