[NestJS] Providers 개념 및 실습

JeungJoo Lee
CrocusEnergy
Published in
9 min readSep 17, 2020

지난 시간에 [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)

의존 관계를 맺을 때, 변화하기 쉬운 것 또는 자주 변화하는 것보다는 변화하기 어려운 것, 거의 변화가 없는 것에 의존하라. 자주 변경하는 구체 클래스 대신 인터페이스나 추상 클래스에 의존하라.

참고> 소프트웨어 설계 기법: SOLID란?

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 개념 및 실습 을살펴 보도록 하겠다.

--

--