2022. 2. 9. 14:42ㆍAngular/Information
이 글에 작성될 내용은 개발중 정말 기본적으로 사용해야 하는 내용이지만, 그럼에도 개발 기간이 촉박할 때 마다 이 개념을 무시한채 개발하였다.
다음에는 그러지 않기 위해 다시 한번 정리하여 개념을 바로잡으려 한다.
Angular에서의 Service, 무엇일까?
Angular에서 개발을 진행할 때 Angular-cli를 이용해 주로 만들던 파일들이 있을 것이다.
바로 Component와 Service이다.
# Service Generate
ng generate service greeting
# 축약어로 ng g s greeting
# Component Generate
ng generate component greeting
# 축약어로 ng g c greeting
Angular를 개발해본 사람이라면 알겠지만, 컴포넌트(Component)는 화면을 구성하는 뷰(View)를 생성하고 관리하는것이 주된 목적이다.
컴포넌트를 작성하다보면 컴포넌트의 주 관심사 외에 필요한 부가기능이 필요하게 된다.
예를 들면 로그를 작성한다던가, 서버와 통신하는 axios 등을 말하는 것이다.
이때 컴포넌트의 주 관심사 외에 부가적 기능을 컴포넌트 내에 작성하게 될 경우 부가기능이 변경되거나, 다른 컴포넌트에서도 필요로 할 때 문제가 발생된다.
부가기능이 변경되었을 때에는 컴포넌트 내에 작성한 부가기능을 모두 변경해주어야 하며, 다른 컴포넌트에서 필요로 할 때는 중복적인 코드가 들어가게 된다.
이는 유지보수 효율 및 개발자의 경험을 떨어뜨리게 하는 주된 원인이다.
또한 부가기능이 컴포넌트 내에 들어가 있을 때에는 재사용성이 떨어지며 컴포넌트의 주요 장점중 일부를 상실하게 된다.
컴포넌트의 주요 관심사 외에 부가적 기능은 대부분 개발중인 어플리케이션의 전역 관심사인 경우가 많다.
위에도 작성했지만, 로그를 작성하는 기능, 서버와 통신하는 axios 기능과 예를 들면 세금을 계산하는 비즈니스 로직이라던지 이런 기능들은 특정한 1개의 컴포넌트에서만 사용하는 것이 아닌 전역적으로 모든 어플리케이션에서 사용할 수 있는 로직이 되어야 한다.
이러한 경우 컴포넌트와 어플리케이션 전역의 관심사를 분리(SoC)해야 한다.
* 관심사의 분리
프로그램을 하나의 단일 Block으로 작성하지 말고 작은 조각으로 나누어 간단한 개별 작업을 완료할 수 있도록 하는 원칙이다. (즉 긴 복잡한 함수로 작성하지 말고 분리할 것.)
이 원칙을 지키게 될 경우의 기대값으로는 코드가 간결해지고, 작은 조각으로 나누었기에 재사용성이 올라간다는 것을 기대할 수 있다.
이 개념은 매우 중요하며, 향 후 개발을 진행하면서도 이 관심사의 분리를 잘 적용해주어야 코드의 가독성이 올라가며 유지보수, 리팩토링에 효율적이다.
(반드시 어플리케이션 전역과의 관심사를 분리하지 말고, 컴포넌트 내부에서도 관심사의 분리를 적용해주도록 하자.)
관심사의 분리를 적용하는 코드를 간단하게 설명해보도록 하겠다.
// 관심사의 분리가 적용되지 않은 코드
function getSumMinMax(ary: number[]): number {
const min = Math.min(...ary);
const max = Math.max(...ary);
return min + max;
}
위 함수를 보면 한개의 함수 내에서 최소값과 최대값을 구하고 이를 더하는 것을 볼 수 있다.
하나의 함수 내에 3개의 기능이 들어간 것으로 이는 관심사의 분리가 제대로 적용되지 않았다고 볼 수 있다.
// 관심사의 분리가 적용된 코드
function getMin(ary: number[]): number {
return Math.min(...ary);
}
function getMax(ary: number[]): number {
return Math.max(...ary);
}
function getXYSum(x: number, y: number): number {
return x + y;
}
위처럼 바꿔주는것이 좋다.
물론 예제처럼 정말 간단히 max, min을 구할 수 있다면 한개의 함수 내에서 사용해도 무관하지만 복잡한 로직이 들어가게 되면 이는 분리시켜주는 것이 좋다.
이 때 사용되는 것이 바로 서비스이다.
컴포넌트와 어플리케이션 전역의 관심사를 분리하여 어플리케이션 전역의 관심사를 외부에서 관리할 수 있다면, 컴포넌트는 자신의 관심사(뷰를 생성하고 관리하는 것)에 집중할 수 있어 코드 복잡도가 낮아지고, 서비스는 재사용이 가능하여 일관된 어플리케이션 코드를 작성할 수 있다.
의존성 주입(Dependency Inejection)을 왜 사용할까?
의존성 주입에 대해 알아보기 전, 의존성이 뭔지를 알아야한다.
의존성, 무엇인가?
앵귤러 내에서 서비스를 만들고, 이를 컴포넌트에서 사용한다고 해보자.
// foo.service.ts
import { Injectable } from '@angular/core';
@Injectable()
export class FooService {
foo() { return 'Boo'; }
}
// app.component.ts
import { Component } from '@angular/core';
import { FooService } from './foo.service';
@Component({
selector: 'app-root',
template: `
<button (click)="foo()">foo</button>
<p>{{ message }}</p>
`
})
export class AppComponent {
message: string;
fooService: FooService;
constructor() {
// 서비스의 인스턴스를 직접 생성한다.
this.fooService = new FooService();
}
foo() {
// 서비스의 사용
this.message = this.fooService.foo();
}
}
FooService를 만들고, AppComponent에서 이를 직접 생성 후 사용하는 방식으로 어플리케이션을 만들었다.
이때 컴포넌트는 FooService와 의존 관계(Dependency relationship)에 있다.
즉, 컴포넌트는 자신의 역할을 수행하기 위해 FooService가 반드시 필요하다는 것이다.
이를 '컴포넌트가 FooService에 의존하고 있다' 라고 말하며, 컴포넌트 관점에서 FooService를 의존성(Dependency) 이라고 한다.
위 예제에서 AppComponnet는 FooService에 의존하고 있으므로, AppComponent는 FooService의 생성 방법(constructor)을 반드시 알고있어야 하며, 만약 FooService의 기능이 변경되거나 생성 방법이 변경되었을 경우에 컴포넌트는 수정이 되어야 한다.
이처럼 의존성의 인스턴스를 생성하는 코드와, 의존성을 사용하는 코드가 컴포넌트 내에 존재하게 된다면 이는 컴포넌트와 의존성이 긴밀한 결합(Tight coupling)을 하고 있다고 말한다.
긴밀한 결합을 느슨한 결합(Loose coupling)으로 변경을 해주어야 할 필요성이 있다.
이 때 사용할 수 있는 패턴중 하나가 의존성 주입(Dependency Injection, DI) 패턴이다.
이 패턴을 사용하게 되면 긴밀한 결합 상태를 느슨한 결합 상태로 바꿔줄 수 있다.
의존성 주입 패턴 사용
Angular에서는 의존성 주입을 프레임워크 차원에서 지원하고 있다.
어플리케이션이 직접 인스턴스를 생성하는 방식이 아닌 Angular 프레임워크에게 의존성 인스턴스를 요구하고 프레임워크가 생성한 인스턴스를 컴포넌트가 받아 사용하는 방식이다.
Angular에서 지원하는 의존성 주입을 사용하기 위해서는 @Injectable 데코레이터에 메타데이터를 추가해주어야 한다.
서비스의 코드를 아래와 같이 바꿔주도록 하자.
// foo.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root' /* @Injectable 프로바이더 */
})
export class FooService {
foo() { return 'Boo'; }
}
@Injectable 데코레이터의 메타데이터 프로퍼티인 provideIn 은 Angular 6에서 새롭게 도입된 프로퍼티다.
'root'로 설정하게 될 경우에 루트 인젝터에게 서비스를 제공하도록 지시하는 것이다.
이는 어플리케이션 모든 구성요소에서 싱글턴 패턴으로 생성된 전역 서비스를 주입할 수 있도록 한다.
이제 이 서비스는 루트 인젝터에게 서비스를 제공하도록 지시하였으니, 어플리케이션 모든 구성요소에서 의존성 주입을 할 수 있게 되었다.
// app.component.ts
import { Component } from '@angular/core';
import { FooService } from './foo.service';
@Component({
selector: 'app-root',
template: `
<button (click)="foo()">foo</button>
<p>{{ message }}</p>
`
})
export class AppComponent {
message: string;
constructor(private fooService: FooService) {
console.log("FooService Genration : ", fooService instanceOf FooService); // true
}
foo() {
this.message = this.fooService.foo();
}
}
의존성 주입을 사용하기 전에는 컴포넌트가 직접 FooService의 인스턴스를 생성하였다.
하지만, 의존성 주입을 사용하고 난 뒤에는 컴포넌트가 직접 FooService 인스턴스를 생성하지 않고 있다.
필요한 의존성을 constructor의 파라미터로 선언하여 의존성 인스턴스를 Angular 프레임워크에 요구했을 뿐이다.
위와 같이 의존성 주입을 통해 생성하게 되면 프레임워크가 제어권을 갖는 주체로 동작하여 요구한 의존성 인스턴스를 생성하여 전달한다.
이를 제어권의 역전(Inversion of Control, IoC)이라고 한다.
서비스를 사용하는 구성요소(컴포넌트만이 사용 가능한 것이 아니다. 모든 구성요소에서 사용 가능하다.)는 더이상 의존성 인스턴스 생성에 대해 관여하지 않아도 된다.
Angular 프레임워크에서 의존성 인스턴스를 생성하여 각 구성요소에서 요구한 인스턴스를 생성하여 전달하여 줄 것이다.
프로바이더(provider), 무엇일까?
의존성 주입 패턴을 사용할 때 의존성 인스턴스를 어떻게 생성하는지에 대해서는 Angular가 알지 못한다.
그렇기 때문에 인스턴스를 생성하는 정보에 대해 Angular가 알 수 있도록 알려주어야 한다.
이러한 인스턴스를 생성하는 정보를 설정하여 의존성 인스턴트의 주입을 지시하는 것을 프로바이더(provider) 라고 부른다.
프로바이더는 서비스의 @Injectable 데코레이터의 메타데이터 객체의 provideIn 프로퍼티 뿐만 아니라 컴포넌트의 @Component 데코레이터, 모듈의 @NgModule 데코레이터의 메타데이터 객체의 providers 프로퍼티에도 등록할 수 있다.
프로바이더를 'root' 전역에 제공하는 것이 아닌 컴포넌트에 한해서만 제공한다고 했을 때에는 아래와 같이 변경하면 된다.
서비스에서 등록한 프로바이더 메타데이터를 삭제해주도록 한다.
// foo.service.ts
import { Injectable } from '@angular/core';
@Injectable()
export class FooService {
foo() { return 'Boo'; }
}
그리고 컴포넌트의 메타데이터에 추가해주면 된다.
// app.component.ts
import { Component } from '@angular/core';
import { FooService } from './foo.service';
@Component({
selector: 'app-root',
template: `
<button (click)="foo()">foo</button>
<p>{{ message }}</p>
`,
providers: [FooService]
})
...
위 코드의 providers 프로퍼티에 담겨 있는 내용은 축약된 코드로 FooService 타입의 인스턴스 주입을 요청 받으면 FooService 클래스에 의해 생성된 FooService 타입의 인스턴스를 주입할 것을 Angular에 지시한다.
축약된 코드를 풀어보면 아래와 같은 코드가 나온다.
// app.component.ts
import { Component } from '@angular/core';
import { FooService } from './foo.service';
@Component({
selector: 'app-root',
template: `
<button (click)="foo()">foo</button>
<p>{{ message }}</p>
`,
providers: [{
provide: FooService,
useClass: FooService
}]
})
...
providers 프로퍼티를 사용하게 되면 서비스의 주입 범위가 달라지게 된다.
이 내용은 인젝터 트리(Injector tree)에 관련되어 있다.
마치며
위 내용은 서비스와 DI에 대한 내용이었다.
인젝터와, 인젝터 트리. 그리고 프로바이더에 대한 상세한 설명은 서술되지 않았으나 그럼에도 Angular 개발시 가장 중요한 부분에 대해 알아보았다.
이 내용은 추 후 사용자 경험(User Experience, 유저 익스피리언스, 축약하여 UX)에도 연관이 있을 것이라고 생각된다.
(추측의 이유는 아직 이 내용이 사용자 경험에 큰 영향이 있는지에 대해 알아보고 있다.)
'Angular > Information' 카테고리의 다른 글
[Angular] 프로바이더(Provider), 자세히 알아보자 (0) | 2022.02.10 |
---|---|
[Angular] DI를 도와주는 인젝터(Injector), 인젝터 트리(Injector Tree) (0) | 2022.02.09 |
[Angular] ngFor에 필요한 trackBy Directive로 만들기 (0) | 2022.02.08 |
[Angular] all error catch class 만들기 (0) | 2022.02.06 |
[Angular] Decorator (0) | 2022.01.05 |