/ ANGULAR

Angular 강좌(13) - Service

Angular 강좌는 여러 절로 구성되어 있습니다.


Service

이번 포스트는 Angular의 Service에 대해서 알아보겠습니다. 먼저 Service의 기본적인 사항들을 알아본 후 이를 통해 데이터를 공유하는 Service Mediator Pattern으로 넘어가면 될 듯 하네요.

이 Service는 Angular에만 존재하는 개념이 아닙니다. 객체지향 프로그래밍을 다뤄보신 분은 익히 들어본 개념입니다. 특히 Spring과 같은 Framework이나 MVC Pattern을 다뤄보신 분들이라면 쉽게 이해하실 수 있는 내용입니다.

우리는 Angular를 하고 있으니 여기에 맞춰 설명을 하자면 Component는 View를 표현하고 관리하는게 주된 역할입니다. 즉, 데이터를 받아와서 View에 출력한다던지 View의 값이 변경되면 그걸 또 어떻게 처리한다던지하는 View와 밀접한 로직을 Component class가 가지고 있게 됩니다.

만약 View를 처리하는 로직 이외의 별도의 로직이 필요하면 그 로직은 어디에 두는것이 좋을까요? 로그인 처리를 할때 필요한 인증로직이라던지 혹은 서버와의 데이터 통신을 위한 REST 서버의 호출같은 로직을 그냥 필요할 때마다 Component안에 집어 넣어서 처리하는게 좋을까요?

우리는 CBD(Component Based Development)를 하고 있습니다. 각각의 Component는 자신의 주된 관심사에 집중하게끔 코드를 작성해야 합니다. 객체지향설계에서 얘기하는 SRP(Single Responsibility Principle)을 생각하시면 됩니다. Component안에 다른 관심사가 존재하면 Component의 독립성이 보장되지 못하고 결국 중복 코드가 발생하며 Component의 재사용과 유지보수에 문제가 발생하게 되겠죠.

그래서 위에서 얘기한 별도의 로직들은 다른 곳에서 관리할 필요가 있습니다. Service라는 걸 이용해서 이 로직들을 작성하고 다른 Component에서 이 Service를 가져다가 사용하는 식으로 관리를 하면 SoC(Separation of Concern)원칙에 잘 들어맞을 거 같습니다.

이렇게 Component와 Service를 분리해서 작성하고 Component에서 Service를 사용하는 건 좋은데 사용할 때 문제가 하나 있습니다. Dependency라는게 생기는 거죠. 쉽게 단위 코드로 Component에서 Service를 사용하는 예를 한번 보죠. 아래는 Component class입니다.

MyService service = new MyService();
service.getUserAuth('moon9342');

pseudocode 입니다. Component class안에서 직접 Service 객체를 생성해서 이용하는 경우입니다. 이런 경우 우리 Component는 Service에 의존하게 됩니다. 이걸 Dependency Relationship(의존관계)이 존재한다 라고 표현하기도 합니다. 이 때 Component class의 입장에서 Service 객체를 Dependency라고 표현합니다.

이렇게 의존관계가 성립되면 Service가 변경되었을 때 우리 Component는 그에 따른 영향을 받을 수 밖에 없습니다. 연관관계가 강하게 성립되어서 서로 독립적으로 사용하는게 힘들어지는것이고 재사용이나 유지보수에 문제가 생기게 되겠네요.

이 문제를 해결하는 Design Pattern이 바로 DI(Dependency Injection)입니다. 우리 Service객체(Dependency)를 사용하는 객체인 Component에게 주입해서 사용하는 것입니다. 주입하는 방법은 일반적으로 constructor를 이용하는 방법과 setter를 이용하는 방법이 있는데 Angular는 constructor injection을 지원합니다.

즉, Component가 직접 Service를 new keyword로 생성하는 것이 아니라 Angular Framework이 Service를 Component가 사용할 있도록 Service객체를 생성해서 Component에게 넣어주는 방식입니다. 이걸 IoC(Inversion of Control)라고 합니다. Angular Framework은 IoC Container입니다.

이론적인 배경을 간단히 설명했으니 이제 Service를 우리 예제에 추가해보겠습니다. Angular application은 Module의 집합입니다. Module은 크게 Feature ModuleShared Module이 있다는 얘기 혹시 기억하시나요? 어디에 Service를 생성하느냐 하는 문제인데 사실 case-by-case입니다. 특성상 여러 Feature Module에서 사용하는 공통 로직의 개념이면 따로 Shared Module을 만들어서 그 안에 Service를 포함시키는게 좋습니다. 하지만 우리 예제처럼 bookSearch Module에서만 사용할 생각이면 해당 Module안에 포함시키는게 더 좋은 선택이겠죠.


Service 생성

다음의 코드를 이용해 우리 bookSearch Module에 서비스를 하나 추가합니다.

command 창을 열어서 다음과 같은 명령어를 실행시킵니다.

ng generate service HttpSupport

성공적으로 수행되면 현재 Module 폴더에 2개의 파일이 생성됩니다. 하나는 SPEC 파일이고 나머지 하나가 바로 Service 입니다.

다음은 http-support.service.ts 파일의 내용입니다.

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

@Injectable()
export class HttpSupportService {

  constructor() { }

}

주의해서 보셔야 하는 부분은 @Injectable decorator입니다. 해당 class가 다른 class에 주입(Injection)될 수 있다는걸 의미합니다. 아까도 설명했듯이 주입은 생성자를 이용하게 되고 주입과정은 Angular Framework이 담당합니다.

이제 이 안에 JSON 데이터를 가져오는 코드를 작성해야 합니다. 우리 예제의 list-box Component는 View를 rendering할 때 HttpClient를 이용해 JSON데이터를 가져와 Material Table로 화면에 바로 출력합니다. 이 부분을 변경해야겠죠. search-box Component에서 Search! 버튼을 클릭하면 HttpClient를 이용해서 데이터를 가져와서 그 데이터를 list-box가 사용할 수 있도록 처리해야 합니다.

원래는 Back End 프로그램도 하나 작성해서 RESTful 서비스 하는걸 예로 들어야 하는데 서버쪽 프로그램이 없으니 그냥 JSON 파일로 부터 데이터를 받는걸로 처리했습니다.

다음은 수정된 http-support.service.ts 파일의 내용입니다.

import { Injectable } from '@angular/core';
import { HttpClient } from "@angular/common/http";

interface IBook {
  bauthor: string;
  bdate: string;
  btranslator: string;
  bpublisher: string;
  btitle: string;
  bprice: number;
  bisbn: string;
  bimgurl: string;
}

@Injectable()
export class HttpSupportService {

  books: IBook[];
  constructor(private http: HttpClient) { }

  getJsonData() {
    this.http.get<IBook[]>('assets/data/book.json')
        .subscribe(res => {
           this.books = res;
           console.log(this.books);
        });
  }
}

interface IBook도 저런식으로 코드마다 등장해서는 안되겠죠. 원래 따로 빼서 관리해야 합니다. 하지만 예제를 좀 이해하기 쉽도록 그냥 중복해서 썻습니다. ^^;;

constructor(private http: HttpClient) { }

생성자로 인자가 하나 들어옵니다. 사실 이것도 HttpClient 타입의 객체가 우리 서비스 안으로 Injection되는 것입니다. 생성자에 인자를 받으면서 Access Modifier를 이용하면 class안에 속성으로 자동 지정됩니다. 여기서는 private으로 Injection된 HttpClient 객체를 받았습니다.

getJsonData() method가 호출되면 Injection받은 HttpClient 객체를 이용해서 파일로부터 JSON 데이터를 읽어들인 후 console에 정상적으로 읽었는지 출력합니다.


Service Injection

위에서 생성한 Service 객체를 search-box Component에 Injection한 후 사용해 보겠습니다.

다음은 search-box.component.ts 파일의 내용입니다.

import {
  Component, OnInit,
  Input, Output, EventEmitter
} from '@angular/core';

import { HttpSupportService } from "../http-support.service";

@Component({
  selector: 'app-search-box',
  templateUrl: './search-box.component.html',
  styleUrls: ['./search-box.component.css'],
  providers: [
    HttpSupportService
  ]
})
export class SearchBoxComponent implements OnInit {

  _bookCategory: string;
  //@Input() bookCategory:string;
  //@Input('bookCategory') mySelected:string;

  @Input()
  set bookCategory(value: string) {
    if( value != null ) {
      // 추가적인 작업이 들어올 수 있습니다.
      this._bookCategory = 'category: ' +value;
    } else {
      this._bookCategory = value;
    }

  }

  @Output() searchEvent = new EventEmitter();

  keyword = null;

  constructor(private httpSupportService:HttpSupportService) { }

  ngOnInit() {
  }

  setKeyword(keyword: string): void {
    this.keyword = keyword;
    this.searchEvent.emit({
      keyword : `${this.keyword}`,
      category: `${this._bookCategory.replace('category: ','')}`
    });

    this.httpSupportService.getJsonData();

  }

  inputChange(): void {

  }
}

기존 코드에서 변경된 부분을 살펴보면

import { HttpSupportService } from "../http-support.service";

기본적으로 import는 시켜줘야 사용할 수 있겠지요.

constructor(private httpSupportService:HttpSupportService) { }

constructor를 이용해 Service가 Injection되었습니다.

this.httpSupportService.getJsonData();

Injection받은 Service의 method를 호출하는 부분입니다.

@Component({
  selector: 'app-search-box',
  templateUrl: './search-box.component.html',
  styleUrls: ['./search-box.component.css'],
  providers: [
    HttpSupportService
  ]
})

Angular Framework에 어떤 class가 Injection이 되는지 알려줘야 합니다. Component의 Metadata부분에 providers를 이용해 처리해야 합니다.

실행해보면 정상적으로 console에 JSON데이터가 출력되는걸 확인하실 수 있습니다.


Injector

기본적으로 Angular Framework은 dependency객체를 어떻게 생성해야 하는지 알지 못합니다. 그래서 우리가 Component의 Metadata를 이용해서 providers에 그 정보를 명시했었지요. 이 정보를 근간으로 Injector가 의존객체를 생성하고 주입합니다.

정리하자면 Component가 생성될 때 Angualr는 Injection에 필요한 객체를 Injector에 요청합니다. 이 Injector는 이미 생성한 객체들을 담고 있는 Container를 유지하고 있는데 이 안에 객체가 있으면 바로 주입하고 그렇지 않으면 의존객체를 생성한 후 주입하게 됩니다.

그림으로 표현하면 다음과 같습니다.

angular-injector

( 이미지 출처 : https://angular.io/guide/architecture )

여기서 주의해야 할 점이 있는데 각각의 Component 각자 하나씩의 Injector를 가지고 있습니다. Component는 tree형식으로 구성되니 Injector 역시 tree형태로 구성이 되게됩니다. 만약 Injection요청에 대한 내용이 현재 Component의 providers부분에 명시되어 있지 않으면 부모 Component의 providers에서 검색하게 됩니다. 이렇게 부모로 타고 올라가면서 의존객체를 찾게 되는것이죠. 만약 상위 Component에서 의존객체를 생성해 놓았으면 하위 Component에서 따로 선언하지 않아도 사용이 가능합니다.

또한 Component의 providers에 등록해 놓을 수도 있지만 Module의 providers에도 등록할 수 있습니다. 이런 경우 해당 Module안에 있는 모든 Component들이 해당 의존모듈을 사용할 수 있게 되겠네요. 최상위 Component인 Root Component가 가지고 있는 Root Injector는 Application 전역에서 사용가능한 의존모듈을 가지고 있게 되겠네요.


Provider

위에서 설명했듯이 Module안에 providers로 등록한 의존객체는 Module안에서 사용이 가능합니다. Component에서 등록한 의존객체는 자신과 자식 Component에서 사용이 가능하지요.

이렇게 보면 Module과 Component에 등록하는게 크게 차이가 없어 보이지만 Module에 등록하는 경우 의존객체는 하나의 객체가 생성되서 사용됩니다. 즉, Singleton 형태로 사용된다는 것이죠. 반면 Component에 등록된 의존객체는 해당 Component가 생성될 때 마다 의존객체가 따로 생성되게 됩니다.

따라서 정보공유를 목적으로 하는 Service Mediator Pattern을 이용할 경우 일반적으로 Module에 의존객체를 등록해서 사용하는것이 좋습니다.

이 provider에 대해서 조금만 더 알아보도록 하죠.

Component안에서 의존객체를 등록하려면 다음의 코드를 이용합니다.

@Component({
  selector: 'app-search-box',
  templateUrl: './search-box.component.html',
  styleUrls: ['./search-box.component.css'],
  providers: [
    HttpSupportService
  ]
})

우리는 지금 의존객체라는 표현을 쓰면서 객체만이 주입되는식으로 표현했는데 실제 객체뿐만 아니라 Value도 주입할 수 있습니다. 일단 먼저 의존객체를 주입하는 방식에 대해서 코드를 조금만 상세히 표현해 보겠습니다. 위의 코드는 사실 밑의 코드의 축약형 입니다.

@Component({
  selector: 'app-search-box',
  templateUrl: './search-box.component.html',
  styleUrls: ['./search-box.component.css'],
  providers: [
    {
      provide: HttpSupportService,    // 데이터 타입
      useClass: HttpSupportService    // 실제 객체를 생성하기 위해 필요한 class명
    }
  ]
})

provide의 값과 useClass의 값이 같을 경우 축약형으로 표현할 수 있습니다. provide는 만들어지는 객체의 데이터 타입입니다. useClass는 실제 객체를 생성하기 위해 사용되는 class명이구요. 당연히 두개가 틀릴 수 있습니다. interface를 이용하거나 duck typing을 이용하면 서로 다른 데이터 타입과 class를 사용할 수 있습니다.

duck typing에 대해서는 여기를 클릭하시면 간단한 내용을 확인하실 수 있습니다.

이번에는 의존객체가 아닌 고정값을 주입하는 방법에 대해서 알아보겠습니다. 일반적인 예는 configuration 값을 주입받는 경우입니다. 간단하게 환경설정파일을 하나 만들어서 그 안에 configuration내용을 채워놓고 그 값을 주입받아 보겠습니다.

command 창을 열어 다음의 명령을 실행해서 하나의 class를 생성합니다. 현재 command 창의 working directory는 search-box Component의 위치입니다.

ng generate class jsonConfig

json-config.ts 파일이 생성됩니다. 해당 파일에 다음과 같이 우리가 사용하는 JSON 파일에 대한 경로와 파일명을 설정정보로 입력합니다.

export class JsonConfig {
  url: string;
  name: string;
}

export const JSON_DATA_CONFIG: JsonConfig = {
  url: 'assets/data/',
  name: 'book.json'
};

다음은 search-box.component.ts 파일의 내용입니다.

import {
  Component, OnInit,
  Input, Output, EventEmitter
} from '@angular/core';
import { HttpSupportService } from "../http-support.service";
import { JSON_DATA_CONFIG, JsonConfig } from "./json-config";


@Component({
  selector: 'app-search-box',
  templateUrl: './search-box.component.html',
  styleUrls: ['./search-box.component.css'],
  providers: [
    {
      provide: HttpSupportService,
      useClass: HttpSupportService
    },
    {
      provide: JsonConfig,
      useValue: JSON_DATA_CONFIG
    }
  ]
})
export class SearchBoxComponent implements OnInit {

  _bookCategory: string;
  //@Input() bookCategory:string;
  //@Input('bookCategory') mySelected:string;

  @Input()
  set bookCategory(value: string) {
    if( value != null ) {
      // 추가적인 작업이 들어올 수 있습니다.
      this._bookCategory = 'category: ' +value;
    } else {
      this._bookCategory = value;
    }

  }

  @Output() searchEvent = new EventEmitter();

  keyword = null;

  constructor(private httpSupportService:HttpSupportService,
              private jsonConfig:JsonConfig) { }

  ngOnInit() {
  }

  setKeyword(keyword: string): void {
    this.keyword = keyword;
    this.searchEvent.emit({
      keyword : `${this.keyword}`,
      category: `${this._bookCategory.replace('category: ','')}`
    });

    this.httpSupportService.getJsonData(this.jsonConfig.url, this.jsonConfig.name);

  }

  inputChange(): void {

  }
}

기존에 비해 몇가지 사항이 달라졌습니다. Value를 Injection받을 때 어떻게 처리해야 하는지를 유심히 보시면 됩니다.

service의 method를 호출할 때 주입값을 가지고 method를 호출하기 때문에 service의 코드도 변경해야 합니다.

다음은 http-support.service.ts 파일의 내용입니다.

import { Injectable } from '@angular/core';
import {HttpClient} from "@angular/common/http";

interface IBook {
  bauthor: string;
  bdate: string;
  btranslator: string;
  bpublisher: string;
  btitle: string;
  bprice: number;
  bisbn: string;
  bimgurl: string;
}

@Injectable()
export class HttpSupportService {

  books: IBook[];
  constructor(private http: HttpClient) { }

  getJsonData(url:string, name:string) {
    this.http.get<IBook[]>(`${url}${name}`)
        .subscribe(res => {
           this.books = res;
           console.log(this.books);
        });
  }
}

마지막으로 한가지가 더 남아있습니다. 의존객체를 생성할 때 객체를 그대로 사용하는게 아니라 특정 로직을 거쳐 의존객체를 생성해 사용할 수 있습니다. 객체지향에서 나왔던 Factory Pattern 생각하시면 됩니다. 이 부분은 여기서 따로 설명하지는 않겠습니다.


Optional Dependency

Optional Dependency는 의존객체의 주입이 필수가 아니라는 것을 의미합니다. @Optional decorator를 이용하면 의존객체가 존재하지 않더라도 프로그램 오류가 나지 않습니다.

단순히 생성자에서 의존객체를 주입받을 때 @Optional decorator를 명시하시면 됩니다. 물론 의존객체가 들어오지 않을때의 로직처리는 해 주어야 합니다.

constructor(private httpSupportService:HttpSupportService,
            @optional private jsonConfig:JsonConfig) { }

이번 포스트에서는 Angular의 Service에 대해서 알아보았습니다. 기본적인 Service의 사용방법을 먼저 숙지하신 후 이 Service를 이용해 데이터를 공유하는 방법으로 넘어가면 될 듯 보입니다. 다음 포스트는 Service Mediator Pattern을 이용한 Component간 데이터 공유에 대해서 알아보도록 하겠습니다.

End.


Angular 강좌는 아래의 책과 사이트를 참조했습니다. 조금 더 자세한 사항을 알고 싶으시면 해당 사이트를 방문하세요!!