NestJS 파헤치기3 - Providers

재로미·2022년 10월 24일
1

nestjs

목록 보기
3/5

Overview: App Architecture

NestJS 앱을 생성하면 기본적으로 app.controller.ts, app.module.ts, app.service.ts 등으로 보일러플레이트가 구성되어 있고 이를 기본으로 애플리케이션이 구동된다.

지난 포스트 말미에서 다룬 것처럼 Nest App을 최초 생성했을 때 아래와 같이 파일이 생성된다.

src
 |- main.ts                    # Nest 앱의 실행파일
 |- app.module.ts              # 실행파일에서 등록, 사용하는 root 모듈파일
 |- app.controller.ts          # Nest 앱의 root 기본 컨트롤러
 |- app.service.ts             # Nest 앱의 root 기본 비즈니스 서비스
 

기본적인 역할들은 주석에 명시해놓았지만 그래도 처음 보면 이해하기 어렵다. 하나씩 개념과 그 의의를 살펴보자. 이번 포스트에서는 이전 Controller에 이어 Providers에 대해 공부해보겠다.

Providers

Controller는 HTTP 요청을 처리하고 더 복잡한 작업을 Provider에게 위임해야 한다. Provider는 Module에서 provider로 선언된 일반 자바스크립트 클래스이며 종류로는 service, repository, factory 등이 있다.

nestjs provider 그림

@Injectable()이라는 데코레이터를 통해 의존성을 주입할 수 있다는 것이 큰 특징이다. 이는 객체가 서로 다양한 관계를 만들 수 있다는 것으로, 이 객체는 singleton으로 단일 인스턴스가 만들어져 controller의 생성자에서 매개변수로 주입받고, 모듈로 연결되어 Nest App 런타임에 위임될 수 있다. 지난 Overview 때 다룬 아래 세가지 개념이 provider를 충분하게 이해하기 위해서 필요하다.

  • 계층형 구조(Layered Architecture)
  • 제어 역전(IoC, Inversion of Control)
  • 의존성 주입(Dependency Injection)

Services

계층형 구조(Layered Architecture)

Provider의 대표는 Service라고 할 수 있다. 이 서비스를 이해하려면 layered architecture 혹은 다층구조(n-tier architecture)를 이해해야 한다.

하나의 소프트웨어를 잘 구축하기 위한 소프트웨어 설계원칙 'SOLID'에는 개발 응집도를 높이고 결합도를 낮추는 것이 있다. 이 원칙을 지키고자 나온 디자인 아키텍처 중 하나가 계층형 구조이며, 로직을 관심사 별로 분리하여 크게 세계층으로 나누어 접근한다.
계층 아키텍처 그림

  • Presentation
    - 사용자 인터페이스 혹은 외부와의 통신을 담당함
    • NestJS에서는 Controller가 그 역할을 함
  • Application
    - presentation과 data 사이를 연결하는 중간 계층이며, 여기에서 비즈니스 로직을 구현함
    • NestJS에서는 Service가 그 역할을 함
  • Data
    - 데이터베이스에 데이터를 읽고 쓰는 역할 담당
    • NestJS에서는 Repository가 그 역할을 함

한편, DDD(Domain Driven Architecture)에서는 4 layered Architecture를 기반으로 하는데, DDD를 채택한 NestJS app을 만든다면 Data layer가 Domain layer와 Infra layer로 세분화된다. 자세한 개념은 DDD의 계층화 아키텍처, jeb1225 velog를 참고하자

이런식으로 세가지 tier로 나누어 소프트웨어를 구축한다고 했을 때 Provider는 2-tier로써 중간 다리의 역할을 하는 것으로 이해할 수 있다.

제어 역전(Inversion of Control)

제어역전을 한마디로 표현하자면
"개발자 개인 대신 프레임워크가 제어를 한다"
라고 할 수 있다.

좋은 객체지향적 설계는 구체적인 개념에 의존하지 않고 추상적인 개념에 의존해야 한다. typescript가 지원해주는 interface를 사용하면, 동작해야 하는 기능은 추상적으로 잡되, 해당 interface가 가져야 할 로직적인 규약을 명시 할 수 있다.

이를 통해 직접적이고 구체적으로 클래스를 인스턴스화하는 행동을 최소화 하며 프레임워크에서 개발자가 짜 놓은 코드를 필요할 때 알아서 실행시키게끔 제어를 가져가는 것이 IoC의 핵심 개념이자 provider의 역할이라고 할 수 있다.

의존성 주입(Dependency Injection)

위 IoC가 프레임워크가 제어권을 가져간다 라는 것이었다면, DI(의존성 주입)는 나아가 프레임워크가 주체가 되어 필요한 클래스와 서로간의 관게를 프레임워크가 관리해준다는 개념이다.

IoC와 DI의 차이를 헷갈려할 수 있는데, 좀 더 직관적으로 접근하자면 IoC는 추상적인 개념으로, 이를 구현한게 DI라고 할 수 있다.

궁극적으로 NestJS는 이 Dependency Injection을 통해 IoC를 구현한 프레임워크인 것이다.

이제 공식 docs의 예제를 따라 service를 만들고 사용해보자. NestJS의 Service는 아래 Nest CLI로 빠르게 생성할 수 있다.

$ nest g service cats --no-spec

애플리케이션 응용 및 이해

// cats.service.ts

import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';

@Injectable()
export class CatsService {
	private readonly cats: Cat[] = [];

	create(cat: Cat) {
		this.cats.push(cat);
	
	findAll(): Cat[] {
		return this.cats;
	}
}
// interfaces/cat.interface.ts
export interface Cat {
  name: string;
  age: number;
  breed: string;
}

위 CatsService는 cats라는 하나의 속성과 create, findAll이라는 두가지 메서드를 가진 클래스이다. 또 Cat이라는 interface를 정의하고 이를 import 받아서 사용하고 있다. @Injectable()이라는 데코레이터가 눈에 띄는데, 이는 빌드 시 메타 데이터를 첨부하여 CatsService가 Nest IoC 컨테이너에서 관리할 수 있는 클래스임을 선언하는 것이다.

Service를 다음과 같이 구현함으로써 controller에서는 service를 연결할 수 있을 것이고, 형태는 다음과 같이 된다.

// cats.controller.ts
import { Controller, Get, Post, Body } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';

@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Post()
  async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
  }

  @Get()
  async findAll(): Promise<Cat[]> {
    return this.catsService.findAll();
  }
}

위에서 CatsController의 생성자에 CatsService가 인스턴스로 주입되는 것을 주목하자. 의존성을 주입하기 위한 방법에는 크게 3가지가 있고 Nest docs에서는 초보자를 위해 1번, 생성자를 이용한 의존성 주입 사용을 권장한다.

  1. 생성자를 이용한 의존성 주입(Constructor Injection)
  2. 수정자를 이용한 의존성 주입(Setter Injection)
  3. 필드를 이용한 의존성 주입(Field Injection)
    • Nest에서는 속성 기반 주입(Property-based Injection)으로 @Inject() 데코레이터 사용함

이렇게 정의하고 생성한 프로바이더(서비스)가 서비스의 소비자(컨트롤러)를 확보했으므로 주입이 원활히 진행 될 수 있도록 서비스를 Nest에 등록해야 한다. 따라서 app.module.ts 파일을 편집하고 @Module() 데코레이터의 providers 배열에 서비스를 추가해서 이를 수행한다.

// app.module.ts
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
import { CatsService } from './cats/cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class AppModule {}

우선은 이렇게 등록을 하는구나 정도로만 알고 넘어가자. 다음에 다룰 주제가 이 Module인데, Module을 이해하기 위해서는 Provider의 기본 개념들인 Layered Architecture(계층형 구조), IoC(Inversion of Control: 제어 역전)와 DI(Dependency Injection: 의존성 주입) 개념을 잘 숙지하자.

References:

profile
정확하고 체계적인 지식을 가진 개발자 뿐만 아니라, 가진 지식을 사람들과 함께 나눌 수 있는 계발자가 되고 싶습니다

0개의 댓글