Services를 의존성 주입하여 사용하기

4.2. Services를 의존성 주입하여 사용하기

의존성 주입이라는 단어가 생소하게 느껴지는 분들도 계실거 같은데요, 우선 의존성 주입이라는게 무엇인지부터 알아보도록 하겠습니다.

의존성 주입(Dependency Injection) 이란 디자인 패턴의 한 종류입니다. 컴포넌트와 서비스간에 결합도를 낮추어 코드의 재사용성을 높이기 위한 방법으로 사용되며, 이미 백앤드 프레임워크인 스프링이 널리 사용되면서 제어의 역전(IoC)와 함께 의존성 주입 또한 널리 알려지게 되었습니다. 그럼 의존성 주입을 통한 이점은 무엇이 있을까요?

  • 컴파일시가 아닌 실행시에 객체가 주입되어 모듈간의 결합도가 낮아진다.
  • 코드 재사용을 높여 모듈을 여러곳에서 사용할 수 있다.
  • 테스트 편의성이 높아진다.

컴파일시가 아닌 실행시점에 객체가 주입된다는게 무슨 의미이며 이것이 왜 결합도를 낮춘다는 걸까요? 아래와 소스를 살펴볼까요?

@Component({
  ...
})
export class sampleComponent() {
  var pizzaService = new PizzaService(new Pizza(), new Logger());
}

소스에 문제가 있는건 아닙니다. 다만 위와 같이 서비스를 선언하여 사용하게 되면, 우리는 PizzaService의 명세를 알고 있어야 한다는 문제가 생깁니다. 예를들어, PizzaService를 생성할 때 필요한 인자가 줄어들거나 늘어나면, 이를 사용하고 있는 모든 컴포넌트들은 위와 같은 선언문을 찾아서 전부 수정해줘야 합니다. 결합도가 높은 어플리케이션에서 발생할 수 있는 문제이며, 이렇게 되면 재사용성도 낮아지고 테스트도 점점 어렵게 됩니다.

또한 서비스를 컴포넌트가 많아질수록 메모리를 과도하게 사용하는 문제가 생깁니다. PizzaService가 수십(혹은 수백)개의 컴포넌트에서 사용된다면 그 만큼의 서비스 객체를 할당해 줘야합니다.

마지막으로 사용자가 공유하고자하는 상태 값이 공유가 되지 않습니다. 서로 다른 서비스 객체를 할당하여 사용하기 때문에, 여러 컴포넌트에서 같이 사용해야하는 데이터를 보관할 방법이 없다는 의미입니다.

이러한 부분들을 해결하기 위해, 앵귤러는 싱글톤(Singleton) 으로 의존성 주입을 제공해줍니다. 즉, 어플리케이션에서 서비스 인스턴스를 단 하나만 가지고 있게 된다는 뜻이며, 컴포넌트들이 생성되고 사라지는 동안 서비스는 페이지가 리프레쉬 되지 않는 이상 상태값을 유지할 수 있으며, 서비스를 새로 생성하는데 오버로드가 발생하지 않는다는 장점이 있습니다.

그러면 의존성 주입을 위해 원하는 객체를 싱글턴으로 만들고, 이를 주입하려면 어떻게 해야할까요? 우선 서비스를 하나 만들어 보도록 하겠습니다.

@Injectable()

<예제> pizza.service.ts

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

@Injectable()
export class PizzaService {

  getPizzaList() { // mock data
    return [
        {id: 1, name: 'Potato Pizza'},
        {id: 2, name: 'Cheese Pizza'}
      ];
  }
}

눈치가 빠르신 분들은 벌써 특징을 발견하셨을 겁니다. 위 소스에서 주목해야할 부분은 @Injectable()라는 데코레이터 입니다. 서비스로 만들고자 하는 class@Injectable() 을 사용하면, 앵귤러가 해당 서비스(혹은 클래스)를 저장해놓고, 컴포넌트 등에서 사용하자 할때 이를 싱글턴 형태로 사용할 수 있도록 해줍니다.

서비스 선언은 이것으로 끝입니다. 생각보다 너무 간단하죠? 그럼 컴포넌트에서는 이 서비스를 어떻게 주입받아 사용할 수 있을까요? sampleComponent를 다시 작성해보도록 하겠습니다.

import { PizzaService } from './pizza.service';

@Component({
  ...,
  providers: [PizzaService]
})
export class sampleComponent() {

  constructor(private pizzaService: PizzaService) { }
}

우선 PizzaService를 import합니다. 이제 서비스를 주입받기 위해 @Component의 meta 영역에 providers를 추가하고 서비스명을 넣습니다. 그리고 최종적으로 constructorprivatepizzaService 를 선언해주면 해당 컴포넌트에서 PizzaService를 사용할 모든 준비가 끝나게 됩니다.

그럼 주입받은 서비스를 사용하려면 어떻게 할까요? 간단합니다!

Injector providers

Provider는 웹앱이 실행중일때 의존성 객체를 제공해주는 역할을 합니다. 즉, 위에서 언급한 @Injectable()을 통해 등록한 서비스를 원하는 시점에 주입받을 수 있습니다.

우리는 위에서 등록한 PizzaService@Component의 메타를 통해 주입받아 볼까 합니다. 문법은 아래와 같습니다.

providers: [{ provide: PizzaService, useClass: PizzaService}]

provide에 들어가는 인자값은 토큰(token)을 의미하는데, 이는 Provider에 등록된 의존값을 찾아내는 키(key) 역할을 합니다. 우리가 Map에서 사용하는 키와 역할은 같지만, 조금 더 유니크한 형태의 키값을 위해 이러한 토큰 형태를 사용하고 있습니다.

두번째 useClass에 들어가는 인자값은 공급자 정의 객체인데, 이는 결국 어떠한 객체를 만들어 낼 것인지에 대한 방법을 제공해준다고 할 수 있습니다.

이게 대체 무슨 의미일까요? 조금더더 깊게 이해하기 위해서는 객체지향 프로그래밍에서 자주 등장하는 상속의 개념을 떠올리시면 됩니다.

우리가 만약 A라는 클래스를 상속받는 B라는 클래스를 만들었다고 가정해봅시다. 그리고 이 B라는 클래스를 @Injectable()을 통해 서비스로 등록했습니다. 아래와 같이 말이죠.

@Injectable()
class A {
  ...
}

@Injectable()
class B extends A {
  ...
}

그리고 이 B 클래스를 주입받을 때 바로 아래와 같이 사용하면 됩니다.

providers: [{ provide: A, useClass: B}]

자 그럼 이제 다시 PizzaService로 돌아와 볼까요? 토큰과 이를 위한 구현클래스가 다를 경우엔 위와 같은 문법을 사용하면 어색함이 없지만, 우리가 사용하려는 경우처럼 토큰과 구현클래스가 같을 경우엔 뭔가 코드가 중복되는 느낌이 있습니다. 이런 경우에 우리는 다음과 같이 생략된 문법도 사용 가능합니다.

providers: [ PizzaService ]

자, 훨씬 깔끔해졌습니다. 이제 우리 소스에 providers를 적용시켜 볼까요?

import { Pizza } from './pizza';
import { PizzaService } from './pizza.service';

@Component({
  ...,
  providers: [ PizzaService ]   // providers
})
export class sampleComponent() {

  pizzaList: Pizza[];

  constructor(private pizzaService: PizzaService) { }

  getPizzaList(): void {
    this.pizzaList = this.pizzaService.getPizzaList();  // pizzaService usage
  }
}

소스를 이렇게 바꾸고나니 우리는 우선 PizzaService에 어떠한 인자를 넘겨 서비스를 생성해야할지 고민할 필요가 없어졌습니다. 서비스가 생성되는데 있어서 필요한 인자는 해당 서비스에 정의하면 됩니다. 단지 컴포넌트는 주입받은 서비스를 정의된 대로 자유롭게 사용하기만 하면 됩니다. 서비스 주입이 간단해 졌으니, 해당 서비스를 어떠한 컴포넌트에서도 사용하기 편해졌습니다. 즉, 재사용성이 늘었다는 것도 알 수 있습니다. 어디서든 호출하기 편해졌으니 테스트가 용이하다는 장점도 자연스럽게 따라옵니다.

이처럼 service@Injectable()을 통해 정의하고 providers를 통해 주입받아 사용하는 방법을 알아 보았습니다. 다음장에서는 지금까지 배운 의존성 주입을 통해 컴포넌트들로 부터 서비스를 분리해내고 이를 주입받아 사용하는 방식으로 변경해 보도록 하겠습니다.

results matching ""

    No results matching ""