[NestJS] Providers 개념 및 실습
지난 시간에 [NestJS] Controller 개념 및 실습 을 살펴보았다. 이번 시간은 NestJS 의 Provider 를 살펴보도록 하겠다.
Provider 의 역할은 앞서 살펴 보았던 Controller 외에 Service, Repository, Factory, Helper 등의 Dependency를 Nest Core가 Register 할 수 있도록 등록하는 곳이라 이해하면 쉬울 듯 하다. 이런 Dependency들을 등록하기 위해서는 @Injectable() 이라는 Decorator로 선언하여 사용할 수 있다 아래는 Controller 에 주입되는 Injectable 로 선언된 Dependency들을 도식화 한 Tree 구조이다.
이 Dependency들의 설계의 원칙은 아래 SOLID 원칙을 따라가길 추천한다고 한다.
🤭 소프트웨어 설계의 5가지 원칙 — SOLID
객체 지향 설계 기법으로 알려져 있지만 꼭 객체 지향 소프트웨어 설계에 한정되는 것은 아니고 절차적 프로그래밍 기법으로 적용할 수 있다. 유연하고 확장성 있도록 시스템 구조를 설계를 하기 위함인데 이 원칙은 Java 프로그래밍에서 많이 쓰이기도 한다. NestJS 도 마찬가지로 이러한 SOLID 원칙를 지향한다.
1. 단일 책임 원칙 (SRP; Single Responsibility Principle)
객체는 단 하나의 책임 만을 가져야 하고 어떤 클래스를 변경해야 하는 이유는 오직 하나뿐 이어야 한다. 같은 이유로 변화하는 것끼리 묶고, 다른 이유로 변화하는 것끼리는 분리하라.
2. 개방-폐쇄 원칙 (OCP: Open-Closed Principle)
기존의 코드를 변경하지 않으면서(closed) 기능을 추가(Open)할 수 있어야 한다. 소프트웨어 엔티티가 확장에 대해 개방(Open)되어야 하지만, 변경에 대해서는 폐쇄(Closed)되어야 한다. 클래스 자체를 변경하지 않고도(Closed) 그 클래스를 둘러싼 환경을 바꿀 수 있어야 한다.
3. 리스코프 치환 원칙 (LSP; Liskov Substitution Principle)
부모 클래스와 자식 클래스 사이의 행위가 일관성이 있어야 한다. 부모 클래스의 인스턴스를 자식 클래스의 인스턴스로 대체해도 프로그램의 의미는 변화되지 않는다. 서브타입은 언제나 자신의 기반 타입(base type)으로 교체할 수 있어야 한다.
4. 인터페이스 분리 원칙 (ISP; Interface Segregation Principle)
인터페이스를 클라이언트에 특화되도록 분리시켜라. 클라이언트는 자신이 사용하지 않는 메소드에 의존 관계를 맺으면 안된다.
5. 의존 역전 원칙 (DIP; Dependency Inversion Principle)
의존 관계를 맺을 때, 변화하기 쉬운 것 또는 자주 변화하는 것보다는 변화하기 어려운 것, 거의 변화가 없는 것에 의존하라. 자주 변경하는 구체 클래스 대신 인터페이스나 추상 클래스에 의존하라.
Services
그럼 간단하게 앞서 살펴 보았던 Providers 라는 개념을 통해 Service Dependency를 만들어 보도록 하자
src/user/user.service.ts
import { Injectable } from '@nestjs/common';
import { UserDto } from './dto/user.dto';@Injectable()
export class UserService {
private users: UserDto[] = [
new UserDto('lee1', '이정주'),
new UserDto('kim1', '김명일'),
]; findAll() : Promise<UserDto[]> {
return new Promise((resolve) =>
setTimeout(
() => resolve(this.users),
100,
),
);
} findOne(id: string) : UserDto | object {
const foundOne = this.users.filter(user => user.userId === id);
return foundOne.length ? foundOne[0] : { msg: 'nothing' };
} saveUser(userDto: UserDto) : void {
this.users = [...this.users, userDto];
}
}
Injectable 데코레이터를 통해 Singleton 의 Dependency가 생기게 되는데 Controller 에 존재했던 로직을 Service 영역으로 책임과 역할을 수행하도록 로직을 옮겼다. 위의 선언된 각 Method들은 데이터를 조작하고 생성하고 조회하는 단순한 역할을 담당한다.
그렇다면, 이 Injectable로 선언된 Dependency는 Nest Runtime 시 Singleton 의 객체로 메모리상에 존재하게 되는데 어떻게 Controller 에서 사용할지가 궁금할 것이다. 코드를 보자
src/user/user.controller.ts
import { Body, Controller, Get, Param, Post, Res } from '@nestjs/common';
import { UserDto } from './dto/user.dto';
import { UserService } from './user.service';@Controller('user')
export class UserController {
// 의존성(Dependency) 주입
**constructor(private userService: UserService) {
this.userService = userService;
}** @Get('list')
findAll(): Promise<UserDto[]> {
return this.userService.findAll();
} @Get(':userId')
findOne(@Param('userId') id: string): any | object {
return this.userService.findOne(id);
} @Post()
saveUser(@Body() userDto: UserDto): string {
this.userService.saveUser(userDto);
return Object.assign({
data: { ...userDto },
statusCode: 201,
statusMsg: `saved successfully`,
});
}
}
자 이제 Controller의 소스를 보게 되면 생성자가 존재하고 해당 생성자에는 UserService 라는 타입의 userService argument 를 받아 UserController 내부의 멤버 변수에 주입하게 된다.
이 상황을 전문 용어로 DI (Dependency Injection) 이라고 말한다. Angular 컨셉이라고 하는데 사실 딱 떠오르는 Framework는 Spring Framework 이 생각 났다. 그리고 해당 Scope 가 Singleton 이 기본이지만 의존성의 Scope 를 통해 Life Cycle을 바꿀 수 있다. 클라이언트의 요청이 있을 때 마다 새로운 객체를 생성할 수 있는 모드도 존재한다. 이 Scope 는 REQUEST 로 설정하여 사용 가능하다. 하지만 성능을 위해서 DEFAULT 인 Singleton 을 되도록 사용하도록 권고하고 있고 특수한 상황에서만 쓰는 것이 바람직하다. 아래 각 Scope 는 설명이 되어있으니 확인해보자.
선언 예시
import { Injectable, Scope } from '@nestjs/common';// 요청마다 UserService는 새로운 객체를 생성하게 된다.
@Injectable({ scope: Scope.REQUEST })
export class UserService {
... 생략 ...
}
단 위와 같은 상황의 경우 Controller 또한 Scope 가 Request 로 설정 되어 있어야 한다. 왜냐하면, Controller는 Singleton 인데 Service는 새로운 객체를 생성하는 Scope 타입 이면 말이 되지 않기 때문이다.
따라서 아래와 같이 변경해야한다.
@Controller({
path: 'user',
scope: Scope.REQUEST,
})
export class UserController {
... 생략 ....
}
IoC 개념 ( Inversion Of Control)
제어의 역전이라는 어려운 말을 내포하고 있는 이 IoC는 NestJS 에서도 여전히 사용하고 있는데 이 말은 쉽게 말해 @Module 에 등록된 각각의 의존선(Dependency)들을 관리하는 특정 컨테이너가 있고 이러한 의존성을 주입해야하는 상황에서 제어의 주도권이 IoC 가 가지고 있어 다른 의존 객체에게 필요한 의존성을 주입하는 역할을 한다는 이야기다. 좀 개념 자체가 생소하고 어려울 수 있지만 쉽게 말해 필요에 의해 의존성들을 관리해주는 역할을 한다.
// IoC에 의해 의존성(Dependency) 주입이 되고 있음
.... 생략 ....
constructor(private userService: UserService) {
this.userService = userService;
} .... 생략 ....
다음 시간 [NestJS] Modules 개념 및 실습 을살펴 보도록 하겠다.