본문 바로가기

4. 서비스와 의존성 주입 (1)

0. 서비스

서비스란 어플리케이션에서 공통으로 사용하는 데이터나 기능을 묶어 정의한 단위입니다. 가령 여러 컴포넌트에서 공통으로 쓰이는 데이터는 서비스로 정의하여 접근할 수 있도록 한다면 모듈화와 코드의 재사용성이 높아질 것입니다. 일반적으로 뷰 컴포넌트에서는 뷰와 관련한 로직만을 남기고 그외의 서버와 관련한 로직 등은 서비스에 정의하는 것이 좋습니다.

 

서비스는 의존성 주입이 가능한 클래스로 @Injectable 데코레이터 하단에 클래스를 정의하면 이 클래스는 곧 의존성 주입이 가능한 클래스입을 명시하는 것입니다.

@Injectable()
export class MyService {}

앵귤러는 서비스 인스턴스를 의존성 주입을 사용하여 생성하는데 그 이유는 여기에서 찾을 수 있었습니다.

1. 의존성 주입을 사용하는 이유

서비스 인스턴스를 생성자 함수로 new 키워드를 사용해서 직접 인스턴스를 생성할 수도 있습니다.

@Component({
  // ...중략...
})
export class AppComponent {
  myService:MyService;
  
  constructor(){
    this.myService = new MyService();
  }
  
  getUser(){
    return this.myService.getUser();
  }
}

이렇게 생성자 함수에서 클래스 인스턴스를 생성하면 두 클래스는 의존 관계에 있다고 하는데 특히 컴포넌트에서 직접 인스턴스를 생성하고 이를 사용하는 것을 의존성이 긴밀한 결합이라고 부릅니다. 그러나 긴밀한 결합에는 단점이 있습니다.

 

의존성이 긴밀한 경우, 컴포넌트에서 인스턴스를 직접 생성하기 때문에 넘겨야 할 인자 등 인스턴스 구조를 파악하고 있어야 합니다. 또한 서비스 클래스를 수정했을 때 해당 서비스를 사용하는 모든 컴포넌트를 수정해야만 하는 번거로움이 있습니다.

 

기왕이면 컴포넌트가 사용하는 서비스 구조를 몰라도 원하는 위치에 서비스를 사용할 수 있었으면 좋겠습니다.

 

따라서 앵귤러는 의존성 주입을 활용하여 컴포넌트에서는 어떤 서비스 인스턴스가 필요한지 명시만 하고, 앵귤러가 이 것을 보고 인스턴스를 생성해주도록 요청하는 방식을 사용하는데 이것을 의존성이 느슨한 결합이라고 합니다.

 

이 때 앵귤러에게 어떤 인스턴스를 주입할 것인지 지시하는 것을 프로바이더(provider)가 담당하고, 앵귤러가 인스턴스 주입을 인젝터에게 요청하게 되면 인젝터는 프로바이더를 확인 후 인스턴스를 생성합니다.

 

인젝터는 앵귤러가 프로바이딩된 서비스 클래스를 확인 후 인스턴스 생성을 요청하는 대상이며 key-value 쌍으로 키와 서비스 인스턴스를 매칭하여 인스턴스를 관리합니다. 예를 들어 key가 'my'이고 'my'에 해당하는 서비스 클래스를 MyService라고 지칭하고 싶을 땐 다음과 같이 작성합니다.

 

key에 해당하는 값은 provide로 의존성 인스턴스의 타입(토큰이라고 부름)을 가리켜 인젝터 컨테이너에서 토큰에 해당하는 인스턴스가 존재하는지 검색하는 용도로 사용됩니다. 이미 인젝터 컨테이너에 인스턴스가 존재한다면 두 번 생성하지 않을 것입니다. useClass는 의존성 인스턴스를 생성할 대상 클래스입니다..

@Component({
  providers: [
    {
      provide: 'my',
      useClass: MyService
    }
  ]
})

우리가 위에서 providers: [MyService]라고 배열 형식으로 클래스를 정의하는 것은 사실 토큰값으로 MyService 자체를 사용한다는 의미입니다.

 

provider는 클래스를 DI하는 것 외에 기본값 혹은 어떤 로직을 거쳐 정제된 값을 프로바이딩할 수도 있는데 3. provider의 종류에서 자세히 설명하겠습니다.

 

아래는 의존성 주입을 활용한 서비스 인스턴스를 생성하는 코드입니다.

@Component({
  // ...중략...,
  providers: [MyService]
})
export class AppComponent {
  constructor(private myService:MyService){}
  
  getUser(){
    return this.myService.getUser();
  }
}

2. 모듈 인젝터와 엘리먼트 인젝터

서비스는 모듈과 컴포넌트에서 프로바이딩할 수 있습니다. 서비스를 어디에 프로바이딩하느냐에 따라 해당 서비스 인스턴스의 의존성을 주입하는 인젝터가 달라져 인스턴스에 접근 가능한 스코프가 달라지는데 각각 어떻게 프로바이딩하는지, 그리고 인젝터 트리를 공부하였습니다.

2.1 모듈에서 프로바이딩

먼저 모듈에서 서비스를 프로바이딩하는 방법으로는 @NgModule()의 providers에서 프로바이딩하는 방법과 서비스 인스턴스의 @Injectable()의 메타데이터인 providedIn으로 프로바이더를 설정하는 방법 두가지가 있습니다.

@Injectable({
  providedIn: 'root'
})
export class MyService {}
@NgModule({
  //...중략...
  providers: [MyService]
})
export class AppModule {} // === root module

두 방법은 모두 루트 모듈에 프로바이딩하는 방법입니다. 다른 점은 @Injectable() 데코레이터의 providedIn을 사용한 방법은 서비스 인스턴스가 트리 쉐이킹되어 더욱 효율적입니다.

2.2 컴포넌트에서 프로바이딩

서비스는 모듈 뿐만 아니라 컴포넌트 레벨에서도 프로바이딩할 수 있습니다. @Component() 데코레이터의 providers에서 이를 정의할 수 있습니다. 이 때 주입되는 서비스 인스턴스는 컴포넌트의 생명주기를 따르게 되어 컴포넌트가 마운트될 때 함께 생성되고, 파괴될 때 함께 파괴됩니다.

@Component({
  //...중략...
  providers: [MyService]
})
export class AppComponent {
  constructor(private myService:MyService) {}
}

2.3 인젝터 트리와 DI 구조

앵귤러에서 인젝터는 두 종류가 존재합니다 각각 모듈 인젝터와 노드(엘리먼트) 인젝터로 불립니다.

 

2.1과 2.2에서 각각 어디에 프로바이딩하느냐에 따라서 다른 종류의 인젝터에 속하게 됩니다. 2.1의 모듈에서 주입한 인스턴스는 모듈 인젝터에, 2.2의 컴포넌트에서 프로바이딩한 인스턴스를 노드 인젝터에 생성됩니다.

 

모듈 인젝터와 노드 인젝터는 다음의 차이점이 있습니다.

  모듈 인젝터 노드 인젝터
위치 모듈 컴포넌트
개수 모듈 당 하나 컴포넌트 당 하나
생성 시점 모듈이 로드되는 시점 컴포넌트가 생성되는 시점

각각의 인젝터는 서로 트리 구조를 이루고 있어 각각 모듈 인젝터 트리, 노드 인젝터 트리로 불립니다.

 

앵귤러는 이 두개의 트리를 탐색하면서 주입할 서비스 인스턴스를 탐색하는데 이 탐색 과정을 resolution이라고 합니다.

 

모듈 인젝터 트리는 앵귤러 어플리케이션에서 단 하나만 존재하며, 노드 인젝터 트리는 컴포넌트당 인젝터가 존재하므로 DOM 트리에 기반한 부모-자식 컴포넌트 관계에 따라 형성됩니다. 이를 다이어그램으로 잘 표현한 그림입니다. [출처]

(좌) 모듈 인젝터 트리 (우) 노드 인젝터 트리

그림에서 볼 수 있듯이 노드 인젝터 트리는 컴포넌트 관계별로 트리가 형성되었고, 모듈 인젝터 트리의 경우 지연 로딩되는 모듈부터 차례로 루트 모듈 > 플랫폼 모듈 > null 모듈 순으로 이루어져 있어 최종적으로는 null 모듈이 트리의 root가 됩니다.

[참고] (2)장에도 나오지만 null 모듈까지 resolution이 진행되면 결국 주입할 인스턴스 토큰 검색에 실패했다는 것을 의미하여 Error(혹은 @Optional() 인 경우 null)가 발생합니다.

이 자료는 서비스의 resolution과 resolution modifier까지 모든 설명을 담고 있는데 해당 내용은 (2)장에서 보충하겠습니다. 따라서 이 자료는 앞으로 앵귤러 프로그래밍을 하실 때 두고두고 보시기에 적극 추천드립니다.

3. Provider의 종류 (작성중)

앞서 소개했던 프로바이더의 종류로 다음 세가지가 있습니다.

  • useClass
  • useValue
  • useFactory

useClass는 우리가 위에서 @Injectable() 하단에 정의한 클래스를 주입할 때 사용합니다. 위의 예제에서 충분히 사용법을 알아보았습니다.

3.1 useValue

useValue는 클래스 인스턴스가 아닌 다른 값을 주입할 때 사용합니다.

예를 들어 위의 상수값을 DI하기 위해서는 provide 키에 특정 토큰값을 지정하고 useValue 속성에 TERM_POLICY를 할당합니다. 

 

특이한 건 클래스가 아닌 값을 토큰으로서 지정하고 사용하고 싶을 땐 @Inject() 데코레이터를 사용해야 합니다. poiemaweb에서 다음과 같이 설명합니다.

주입 대상의 타입이 클래스이면 Angular에 의해 암묵적으로 클래스를 @Inject 데코레이터의 인자로 전달하기 때문에 @Inject 데코레이터를 선언하지 않아도 된다. 하지만 클래스 이외의 토큰은 명시적으로 @Inject 데코레이터를 선언하여야 한다.

3.2 인터페이스를 DI 토큰으로 사용하고 싶을 때

타입스크립트의 인터페이스를 토큰으로 사용하고 싶은 경우도 있습니다.

 

예를 들어 위의 예제에서 term은 title과 description을 포함한 인터페이스로 정의할 수 있습니다.

 

 

그리고 이 인터페이스를 토큰으로 사용하고 싶어서 아래와 같이 사용해보았습니다.

 

 

위의 예제는 오류가 날 것입니다. 타입스크립트가 자바스크립트로 변환될 때 인터페이스를 지원하지 않기 때문에 해당 토큰이 사라져 문제가 됩니다.

 

이럴 경우에는 특정 인터페이스로 토큰을 생성하는 InjectionToken클래스를 사용하여 프로바이더에서 사용할 수 있는 DI 토큰을 생성하는 것으로 해결할 수 있습니다.

 

3.3 useFactory

useFactory는 서비스를 DI할 때 특정 로직을 거친 후 DI하고 싶은 경우에 사용할 수 있습니다.

 

useFactory를 사용하여 정의한 프로바이더를 팩토리 프로바이더라고 부르는데 팩토리 프로바이더는 provide, useFactory 프로퍼티와 더불어 deps 프로퍼티를 지정할 수 있습니다.

 

deps 프로퍼티는 팩토리 함수에 필요한 인자들의 리스트를 가지고 있으며 각 요소는 DI된 서비스의 토큰입니다.

 

useFactory를 사용할 때에는 다음의 필수 조건이 있습니다.

  • useFactory 프로바이더를 재사용하기를 원할 때에는 별도의 프로바이더 변수를 만들고 이를 export합니다.
  • 팩토리 함수의 인자로 쓰이는 값(클래스 등)을 사전에 provide해야 합니다.
  • 프로바이더의 deps 프로퍼티 값(배열)의 각 토큰과 팩토리 함수의 인자의 타입이 일치해야 합니다.

다음은 useFactory를 사용하여 원하는 분류에 속한 강의들을 "|"로 연결한 문자열로 받아오는 예제입니다.

[출처] http://www.sahosofttutorials.com/Course/Angular7/144/

 

먼저 강의명과 분류를 포함한 Course 클래스를 생성합니다.

CourseService를 정의하여 모든 강의를 가져오는 메소드를 정의합니다.

 

이제 팩토리 프로바이더 변수를 생성하기 위해 팩토리 함수와 토큰을 생성하겠습니다.

 

예제는 특히 count라는 추가 인자를 받아서 count 개수만큼 보여주기 위하여 팩토리함수를 리턴하는 고차 함수로 정의하였습니다.

이제 위의 팩토리 프로바이더 변수를 @Component() 데코레이터의 providers에서 프로바이딩합니다.

4. [실습] 서비스 적용하기

이제 컴포넌트 구조가 복잡해져 값을 주고받기가 불편해진 실습 코드를 서비스로 로직을 분리하여 뷰는 뷰 로직만을 담고 있고, 여러 컴포넌트에서 쓰이는 로직은 서비스로 분리하여 DI해보겠습니다.

 

먼저 어떤 로직을 서비스로 분리해야 할지 설계합니다.

 

메뉴를 추가/삭제하고 개수를 조절하는 로직은 여러 컴포넌트에서 재사용하고 뷰와는 관련없는 로직이므로 주문 로직을 서비스로 분리하는 것이 좋습니다.

 

또한 메뉴 모듈에서만 주문 로직을 쓴다면 modules/menu/services/에 클래스를 정의하겠지만

추후에 장바구니 모듈에서도 이 기능은 쓰일 수 있기 때문에 공유 모듈을 정의하여 여러 모듈에 임포트하겠습니다.

 

따라서 shared/라는 별도의 디렉토리를 생성하고 내부에 services/order.service.ts를 생성합니다.

 

order.service.ts는 주문리스트 데이터를 가지고있고, 메소드로는 메뉴를 주문리스트에 추가하는 로직, 개수 증가/감소 로직, 그리고 메뉴를 주문리스트에서 제거하는 로직을 정의하겠습니다.

[참고] 글쓴이는 나중에 NgRx와 셀렉터를 적용하기 위해 orderById, orderByArray 이 두 상태값을 두어 orderById는 productId를 key로, Order.OrderDetail 객체를 값으로 갖는 객체로 사용하고, orderByArray는 productId 리스트로 정의하였습니다.

이제 컴포넌트는 해당 로직을 들고 있을 필요 없이 OrderService를 DI받아서 사용할 수 있을 것입니다.

 

하지만 그전에 sharedModule에서 서비스를 프로바이딩하고ㅡ 각 모듈에 sharedModule을 임포해주어야합니다.

이제 sharedModule을 임포트하여 sharedModule 내 프로바이더를 사용할 수 있으니 주문 로직과 수량 조절 로직이 존재했던 컴포넌트에서 해당 로직을 제거하고 OrderService를 DI받습니다.

이전의 엄청 복잡했던 로직이 서비스로 분리되니 컴포넌트 클래스는 뷰 로직만을 담당하고, 서비스가 이외의 로직을 담당하게 되어 코드의 재사용성이 증가하고 복잡도가 줄었습니다!

 

이제 이 서비스를 사용하여 메뉴 상단의 주문 리스트에서 메뉴를 제거하는 버튼을 추가하고, menuModule처럼 cartModule에 sharedModule을 임포트하여 장바구니 컴포넌트 클래스를 완성하면 아래의 결과를 얻을 수 있습니다.

 

주문리스트 카드의 x버튼을 클릭하면 주문 리스트에서 삭제될 것입니다.

[좌] /menu, [우] /cart

어라, 그런데 한가지 우리가 예상하던대로 동작하지 않습니다.

 

주문하기 버튼을 클릭하면 /cart 화면으로 넘어가서 주문리스트가 출력되어야 할텐데 그림의 우측 화면처럼 출력되지 않습니다.

 

데이터가 공유되지 않는다는 건데 왜그럴까요?

 

서비스는 SharedModule에서 프로바이딩하고 있고, 이 모듈을 MenuModule과 CartModule에서 각각 임포트하였습니다.

 

이렇게 하면 인스턴스를 관리하는 모듈 인젝터가 달라지고, 결국 서로 다른 인스턴스가 두 개 존재하여 데이터가 공유되지 않습니다.

 

실제로 크롬 개발자 도구의 Memory 탭에서 /menu 화면과 /cart 화면의 스냅샷을 생성하여 비교하면 SharedModule이 하나 더 생성된 것을 볼 수 있습니다.

 

Snapshot1은 /menu 페이지의 스냅삿, Shapshot2가 /cart 페이지의 스냅샷이고, 이 둘을 Comparison으로 비교했을 때 #New에서 인스턴스가 추가로 하나 더 생성된 것을 볼 수 있습니다.

정상적인 동작이라면 주문 리스트는 어느 페이지에서든 유지되어야 합니다.

 

따라서 어플리케이션에서 OrderService 인스턴스는 한 개만 존재해야 할 것입니다.

 

이렇게 인스턴스가 중복해서 생성되는 문제는 서비스와 의존성 주입(2)에서 싱글톤 서비스를 공부하면서 해결하겠습니다:)

'Today I Learn > Angular 기초' 카테고리의 다른 글

6. RxJs와 NgRx (1)  (0) 2020.07.08
5. 서비스와 의존성 주입 (2)  (0) 2020.07.04
3. Modules  (0) 2020.06.26
2. Component와 Directive (2)  (0) 2020.06.20
1. Component와 Directive (1)  (2) 2020.06.19