JAVA Spring 개발자의 Nest+Graphql 도전

체력방전
CrocusEnergy
Published in
41 min readSep 17, 2020

시작하며

공부를 하면서 Nest 는 Spring 과 닮은 면이 좀 있다고 느꼈다. 원래 Spring 기반의 framework 를 사용해 개발 해 온 사람이라면 Node 로 백엔드 서버를 시작하기에 장점이 있다고 본다. Nest 공식 사이트 에 참고 할 자료가 많으니 참조하길 바란다.

사전작업

Nest github 에서 Sample code 를 받아볼 수 있다. 이것을 먼저 다운받기 바란다. NEST 하위의 sample 아래에 순번이 매겨져 있는데 이 번호를 차례차례 따라가며 학습 내용을 정리할 예정이다.

Node 버전 관리

% node -v
v14.9.0

위와 같이 node 버전을 확인 할 수 있다. 이 버전을 관리하는 nvm 이라는 매니저 프로그램이 있는데 이것으로 현재 로컬에 활용 가능한 node 버전을 확인 할 수 있다.

% nvm ls
v6.4.0
v8.4.0
-> v14.9.0
system
default -> stable (-> v14.9.0)
node -> stable (-> v14.9.0) (default)
stable -> 14.9 (-> v14.9.0) (default)
iojs -> N/A (default)
lts/* -> lts/erbium (-> N/A)
lts/argon -> v4.9.1 (-> N/A)
lts/boron -> v6.17.1 (-> N/A)
lts/carbon -> v8.17.0 (-> N/A)
lts/dubnium -> v10.22.0 (-> N/A)
lts/erbium -> v12.18.3 (-> N/A

아래와 같이 실행 해 주면 현재 node 의 stable 버전을 사용 할 수 있다.

% nvm install stable
% nvm alias default stable

Nodemon 사용

Nodemon 을 사용할지 말지는 개인의 기호에 따라 다르다.

필자의 경우 Javascript 는 vscode 를 써야하는 것 아닌가 라는 이상한 생각을 가지고 vscode 를 사용해서 실습을 진행했는데, vscode 에서 자체 제공하는 debugging 기능이 이유는 모르겠지만 zsh 을 기본으로 사용하지 않아 node 버전이 nvm 과 맞지 않는 등 이상한 에러가 많아서 nodemon 을 사용해야겠다는 생각을 하게 되었다.

nodemon 은 소스코드 변경이 일어날 경우 자동으로 서버를 restart 해준다. nodemon 을 사용하면서부터 이전에 ‘npm restart run’ 아무리 해줘도 변경한 코드가 적용되지 않는 이상한 현상도 함께 사라졌다. (dist 폴더를 한 번 삭제하니 다음부터 정상 동작했는데 이유는 잘 모르겠다.)

첨언) 문서를 작성하는 중에 vscode 에서 프로젝트 running 시키는 방법을 알게 되었으니 Nodemon 을 잘 설치 해 주면 될 것 같다.

실행 방법은 샘플 별 실행 방법 쪽에 같이 녹여서 설명하겠다.

샘플별 실행 방법

먼저는 필요한 package 설치가 우선되어야 한다. 아래 코드를 실행하자

$ cd sample/sample-directory-name
$ npm install

아래 코드를 실행 하고 잘 실행 되는지 확인하자.

$ npm install -g nodemon
$ nodemon --watch src/ src/main.ts

그 다음 해당하는 샘플 폴더 내부의 package.json 파일의 scripts 부분에 start:dev 내용을 추가 또는 변경 해 준다.

"scripts":{
"start:dev": "nodemon --watch src/ src/main.ts"
}

그리고 다음과 같이 실행하면 지속적으로 변경사항을 반영하는 서버를 dev mode 로 실행 할 수 있다.

$ npm run-script start:dev{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: <https://go.microsoft.com/fwlink/?linkid=830387>
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**"
],
"program": "./src/main.ts",
"outFiles": [
"./dist/**/*.js"
],
"runtimeExecutable": "nodemon",
"restart": true,
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"runtimeVersion": "14.9.0",
"cwd": "${workspaceFolder}/sample/01-cats-app",
}
]
}

이렇게 사용하면 23번 줄의 cwd 만 샘플 경로에 따라 변경해주면 샘플코드 마다 nodemon 으로 실행 할 수 있다.

기초지식

샘플이 분명 존재하지만 프로잭트 생성 등에 대해 아주 모르는 상태에서 학습을 진행하기는 어려울 것 같아 기본 cli 명령을 여기에 정리한다. Nest 서버를 만들 때 가장 근간이 되는 부분은 Module , Controller , Provider 라고 할 수 있다. package.json 의 scripts 부분에 넣은 내용처럼 Nest 서버의 시작점은 main.ts 이다. (파일 이름은 달라질 수 있다.) 이 파일의 내용은 아래와 같다.

async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
console.log(`Application is running on: ${await app.getUrl()}`);
}
bootstrap();
  • 8번 줄에서 bootstrap 명령을 수행하며 서버가 실행된다.
  • 2번 줄에서 AppModule 을 등록한다.

01 샘플의 AppModule 을 살펴보자

import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';
import { CoreModule } from './core/core.module';
@Module({
imports: [CoreModule, CatsModule],
})
export class AppModule {}

CatsModule 과 CoreModule 을 import 하는 최종적인 모듈로 볼 수 있다. CatsModule 과 CoreModule 을 각각 확인 해 보자

@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {}
@Module({
providers: [
{ provide: APP_INTERCEPTOR, useClass: TransformInterceptor },
{ provide: APP_INTERCEPTOR, useClass: LoggingInterceptor },
],
})
export class CoreModule {}

위와 같은 방식으로 controller 와 service 를 사용 할 수 있도록 등록 해 준다. 등록된 providers 은 각각에서 의존성 주입(dependency injection)이 가능해진다.

실제 명령어

아래 명령으로 쉽게 서버를 생성 하고 실행 해 볼 수 있다. (localhost:3000 에 접속으로 확인)

$ npm i -g @nestjs/cli
$ nest new project-name
$ cd project-name
$ npm run start

아래 cli 명령을 수행하면 controller , service, module 이 자동으로 project 에 등록이된다.

$ nest g controller controller-name
$ nest g service service-name
$ nest g module module-name

다음은 Overview 내용을 따라 실습했던 내용이다. 천천히 따라 해 볼 분들은 따라해도 좋을 것 같다.

Overview

first step

개발 환경 구성의 프로젝트 생성 항목과 같은 내용

Controllers

아래 명령으로 controller 생성이 가능하다.

nest g controller controller-name

controller 추가 해 주고 app.module.ts 에 아래와 같이 포함시켜 주면 해당 컨트롤러의 api 를 사용 할 수 있다.

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

아래는 내용을 요약한 예제 컨트롤러 코드이다.

import { Controller, Get, Post, Redirect, Query, Param, Header, HttpCode } from '@nestjs/common';@Controller('cats')
export class CatsController {
@Post()
@Header('Cache-Control', 'none')
@HttpCode(204)
create(): string {
return 'This action adds a new cat';
}
@Get()
findAll(): string {
return 'This action returns all cats';
}

@Get('ab*cd')
findAll2() {
return 'This route uses a wildcard';
}

@Get('docs')
@Redirect('<https://docs.nestjs.com>', 302)
getDocs(@Query('version') version) {
if (version && version === '5') {
return { url: '<https://docs.nestjs.com/v5/>' };
}
}

@Get(':id')
findOne(@Param() params): string {
console.log(params.id);
return `This action returns a #${params.id} cat`;
}
}
  • 3번 줄: localhost:3000/cats 와 같이 사용 가능하다. controller 의 prefix 설정
  • 6번 줄: Post , Get 등 Request Method 선언
  • 7번 줄: Response 의 헤더 설정
  • 8번 줄: HttpCode 변경 가능 (Post 의 경우 원래 201 번, Postman 으로 test 했을 때 결과 스트링을 보지 못했는데 이유는 불명)
  • 18번 줄: function 호출의 prefix 설정 가능, ‘*’ 를 사용하면 wild card 로 인식
  • 24번 줄: 호출을 redirect 할 수 있음
  • 25번 줄: version 이라는 param 을 받는다. get url 로 생각하자면 ‘{host}?version=5’ 와 같이 사용가능
  • 27번 줄: 호출 Param 값에 따라 redirect 하는 url 을 다르게 설정 가능
  • 31번 줄: path param 설정 방법 34번 줄과 같이 사용 가능

Sub-Domain Routing

@Controller({ host: 'admin.example.com' })
export class AdminController {
@Get()
index(): string {
return 'Admin page';
}
}

Header 의 host 를 admin.example.com 로 설정하여 request 를 보내면 이 API 를 호출 할 수 있다. path 를 키값으로 설정하면 controller 의 prefix 를 설정 할 수 있다.

이부분에서 hostparam 에 대한 내용이 나오는데 여러가지 테스트를 해봤지만 어떤 역할을 하는지 알아 내지 못해서 기입하지 않았습니다. 궁금하신 분은 공식페이지 참고 해 주세요

Asynchronicity

이것은 AJAX 같이 프론트에서 비동기적으로 호출하는 함수를 의미하는 것으로 보인다. 비동기적이라는 것은 처음 화면을 렌더링하면서 필요한 모든 데이터를 가져오는 것이아니라, 사용자의 필요에 따라 호출 될 경우 필요한 데이터를 가져오거나 처리하는 동작 방식을 이야기한다. 이것을 이해하기 위해 다음 자바스크립트 async와 await 사이트를 참고하였다.

Request payloads

아래와 같이 DTO 객체를 선언 할 수 있다.

export class CreateCatDto {
name: string;
age: number;
breed: string;
}

이것을 Controller 에서 다음과 같이 받도록 설정 할 수 있다.

@Post()
create(@Body('createCatDto') createCatDto: CreateCatDto): string {
console.log('createDto->' + createCatDto.name)
return 'This action adds a new cat';
}

request 를 보낼 때 body 를 아래와 같이 보내주면 이 DTO 를 주고받을 수 있게 된다.

{
"createCatDto" : {
"name" : "Ruby",
"age" : "2"
}
}

Appendix: Library-specific approach

import { Controller, Get, Post, Res, HttpStatus } from '@nestjs/common';
import { Response } from 'express';
@Controller('cats')
export class CatsController {
@Post()
create(@Res() res: Response) {
res.status(HttpStatus.CREATED).send();
}
@Get()
findAll(@Res() res: Response) {
res.status(HttpStatus.OK).json([]);
}
}

응답값을 return 으로 넘기지 않고 json 과 같이 명시적으로 가공할 수 있어 보기에 좀 더 분명하게 이해할 수 있는 장점이 있는 방식으로 보인다. 기호에 따라 사용하면 좋을것 같다.

Providers

공식홈페이지의 설명을 먼저 보자

  1. Providers are a fundamental concept in Nest. Many of the basic Nest classes may be treated as a provider — services, repositories, factories, helpers, and so on. The main idea of a provider is that it can inject dependencies; this means objects can create various relationships with each other, and the function of “wiring up” instances of objects can largely be delegated to the Nest runtime system. A provider is simply a class annotated with an @Injectable() decorator.

서비스, 레포지토리, 팩토리, 헬퍼 등 자바 사용자라면 왠지 친근한 이 용어들이 가리키는 대상이 Nest 에서는 Providers 로 묶인다는 내용이다. Providers 란 의존성 주입이 가능한 것들을 가리킨다고한다. @Injectable() 이라는 녀석을 annotate 해주면 된다고 한다.

이번에도 역시 생성하는 cli 를 제공한다.

nest g service cats

다음과 같이 파일들을 생성 해 주자

cat.interface.ts

export interface Cat {
name: string;
age: number;
breed: string;
}

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;
}
}

cats.controller.ts

import { Controller, Get, Post, Redirect, Query, Param, Header, HttpCode, Body, Res, HttpStatus } from '@nestjs/common';
import { CreateCatDto } from '../dto/create-cat.dto';
import { CatsService } from '../service/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();
}
}

그 밖의 provider

공식 페이지의 provider 내용을 쭉 보다보면 custom provider, optional provider 등의 내용이 나온다. 요약하자면 provider injection 의 방법이 더 다양할 수 있다는 내용이고 좀 더 확장 해서 factory provider 사용법, provider 설정 같은것들을 설명하고 있다. 공식홈페이지 메뉴 중 Fundamentals 의 하위항목인 custom provider 를 확인하면 확인 할 수 있으니 궁금한 사람들은 찾아보면 될것같다.

샘플코드 보기

혹시라도 이후 git source 가 변경 될 수 있으므로 해당 log를 아래와 같이 남겨두겠다.

$ git log
commit 11890547bb204a294c9664584ff4b8e9d8a5e280 (HEAD -> master, origin/master, origin/HEAD)
Merge: ac413cd04 917b09ba5
Author: Kamil Mysliwiec <mail@kamilmysliwiec.com>
Date: Fri Aug 28 08:21:25 2020 +0200
Merge pull request #5338 from nestjs/renovate/mongoose-5.x

fix(deps): update dependency mongoose to v5.10.1

01-cats-app

01 번 샘플코드는 Nest 공식홈페이지의 Overview 항목에서 설명하고 있는 모든 구현체가 올려져 있다.

core.module.ts

01 번 샘플코드에서 가장 먼저 눈이 갔던 것이 core.module 구현체였는데, 이유는 interceptor 라는 녀석을 사용하고 있었기 때문이었다. Spring AOP 의 그 interceptor? 정답이다. Node 기반의 백엔드 서버로 Nest 가 처음이라 관점지향 프로그래밍이 어느정도 흔한지는 모르겠지만 왠지모를 반가움이 있었다.

실제로 공식홈페이지에서도 다음과 같이 설명한다.

‘’’Interceptors have a set of useful capabilities which are inspired by the Aspect Oriented Programming (AOP) technique. They make it possible to:’’’

어쨌든 interceptor 를 처음 보는 사람들도 있을테니 간단하게 이야기 하자면 사용자(Client side) 로 부터 받은 request 를 controller 로 routing 하기 전과 후에 필요한 어떤 작업을 할 수 있도록 해주는 구현체이다. AOP 는 관점 지향 프로그래밍이라고 불리는 개념으로 interceptor 와 같이 어떤 과정의 전후에 어떤 처리를 할 수 있도록 해주는 프로그래밍이라고 생각하면 되겠다. (사실 필자도 정확한 용어의 의미나 기원은 모르며 관심도 없다. 그냥 어떤 과정을 삽입 할 수 있는 프로그래밍? 정도로 생각하자)

이와 유사하게 Middleware 라는 것도 존재하는데 01 번 샘플에 구현체는 존재하지만 실제 사용되지는 않고있다. 그래도 여기서 interceptor 와 차이점을 이야기하면

  • @Module 데코레이터 안에 import 할 수 없다.
  • NestModule 을 implement 하는 Module에서 configure 함수를 구현하면서 등록할 수 있다.

위의 두가지 정도이다. Middleware 와 Interceptor 둘 다 Client side ↔︎ Route handler 사이의 동작을 정의하는 것으로 보아 역할이 겹치는데, 존재하는 이유는 express 와의 호환성을 위해서 라고 보여진다. spring 에 길들여 진 나같은 개발자라면 그냥 Interceptor 를 사용하자.

transform.interceptor.ts

아래 코드처럼 생겨먹은 녀석이다.

@Injectable()
export class TransformInterceptor<T>
implements NestInterceptor<T, Response<T>> {
intercept(
context: ExecutionContext,
next: CallHandler<T>,
): Observable<Response<T>> {
return next.handle().pipe(map(data => ({ data })));
}
}

8번 줄에서 pipe 를 통해 data 를 어떤식으로 가공하는것으로 이해가 됐지만 언뜻 이해가 되지 않아 귀찮지만 Debugging을 시도 해 봤다.

엥? 왜 파이프 method 가 없지? (생각해보니 handle() 은 함수니까 안보이는게 맞는 것 같다…) 아래는 공식홈페이지의 call handler 설명이다.

‘’’Call handler**#**

The second argument is a CallHandler. The CallHandler interface implements the handle() method, which you can use to invoke the route handler method at some point in your interceptor. If you don’t call the handle() method in your implementation of the intercept() method, the route handler method won’t be executed at all.

This approach means that the intercept() method effectively wraps the request/response stream. As a result, you may implement custom logic both before and after the execution of the final route handler. It’s clear that you can write code in your intercept() method that executes before calling handle(), but how do you affect what happens afterward? Because the handle() method returns an Observable, we can use powerful RxJS operators to further manipulate the response. Using Aspect Oriented Programming terminology, the invocation of the route handler (i.e., calling handle()) is called a Pointcut, indicating that it’s the point at which our additional logic is inserted.

Consider, for example, an incoming POST /cats request. This request is destined for the create() handler defined inside the CatsController. If an interceptor which does not call the handle() method is called anywhere along the way, the create() method won’t be executed. Once handle() is called (and its Observable has been returned), the create() handler will be triggered. And once the response stream is received via the Observable, additional operations can be performed on the stream, and a final result returned to the caller.’’’

글을 보고나니 pipe 라는 녀석은 handle() 호출시 반환되는 Observable 구현체의 함수이며 ‘route handler’ 실행 전 후에 원하는 실행 코드를 수행 할 수 있는 녀석이라는 것을 알 수 있었다.

pipe(): Observable<T>;
pipe<A>(op1: OperatorFunction<T, A>): Observable<A>;
pipe<A, B>(op1: OperatorFunction<T, A>, op2: OperatorFunction<A, B>): Observable<B>;
pipe<A, B, C>(op1: OperatorFunction<T, A>, op2: OperatorFunction<A, B>, op3: OperatorFunction<B, C>): Observable<C>;
pipe<A, B, C, D>(op1: OperatorFunction<T, A>, op2: OperatorFunction<A, B>, op3: OperatorFunction<B, C>, op4: OperatorFunction<C, D>): Observable<D>;
pipe<A, B, C, D, E>(op1: OperatorFunction<T, A>, op2: OperatorFunction<A, B>, op3: OperatorFunction<B, C>, op4: OperatorFunction<C, D>, op5: OperatorFunction<D, E>): Observable<E>;
pipe<A, B, C, D, E, F>(op1: OperatorFunction<T, A>, op2: OperatorFunction<A, B>, op3: OperatorFunction<B, C>, op4: OperatorFunction<C, D>, op5: OperatorFunction<D, E>, op6: OperatorFunction<E, F>): Observable<F>;
pipe<A, B, C, D, E, F, G>(op1: OperatorFunction<T, A>, op2: OperatorFunction<A, B>, op3: OperatorFunction<B, C>, op4: OperatorFunction<C, D>, op5: OperatorFunction<D, E>, op6: OperatorFunction<E, F>, op7: OperatorFunction<F, G>): Observable<G>;
pipe<A, B, C, D, E, F, G, H>(op1: OperatorFunction<T, A>, op2: OperatorFunction<A, B>, op3: OperatorFunction<B, C>, op4: OperatorFunction<C, D>, op5: OperatorFunction<D, E>, op6: OperatorFunction<E, F>, op7: OperatorFunction<F, G>, op8: OperatorFunction<G, H>): Observable<H>;
pipe<A, B, C, D, E, F, G, H, I>(op1: OperatorFunction<T, A>, op2: OperatorFunction<A, B>, op3: OperatorFunction<B, C>, op4: OperatorFunction<C, D>, op5: OperatorFunction<D, E>, op6: OperatorFunction<E, F>, op7: OperatorFunction<F, G>, op8: OperatorFunction<G, H>, op9: OperatorFunction<H, I>): Observable<I>;
pipe<A, B, C, D, E, F, G, H, I>(op1: OperatorFunction<T, A>, op2: OperatorFunction<A, B>, op3: OperatorFunction<B, C>, op4: OperatorFunction<C, D>, op5: OperatorFunction<D, E>, op6: OperatorFunction<E, F>, op7: OperatorFunction<F, G>, op8: OperatorFunction<G, H>, op9: OperatorFunction<H, I>, ...operations: OperatorFunction<any, any>[]): Observable<{}>;

Observable 코드 내부를 보니 이런 녀석들을 볼 수 있었다. 여러가지 Function 을 던질 수 있게 되어 있다.

logging.interceptor.ts 코드를 보니 좀 더 분명한 느낌이을 알 수 있었다.

console.log('Before...');const now = Date.now();
return next
.handle()
.pipe(tap(() => console.log(`After... ${Date.now() - now}ms`)));

아하… next.handle() 로 ‘route handler’ 가 수행되기 전에 ‘Before…’ 라는 로그를 남기고 후에 ‘After… ${Date.now() — now}ms’ 라는 로그를 남길 수 있구나.

Spring 에서 FilterChain 같은 느낌인데 이름은 interceptor 고… 그런 생각이 조금 들었지만 어쨌든 Interceptor 에 대한 개념 정리는 어느정도 된 것 같았다. (+vscode와 친밀도 상승)

Cats Controller , Service 등

먼저 service 코드를 보자

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;
}
}
  • 4번 줄 : Injectable annotation 을 사용하고있다. Dependency injection 을 위한 선언으로 이 annotation 이 있으면 bean 으로 등록되어 module 로 연결된 어디서든 호출 밑 사용이 가능하다.
  • 9, 13 번 줄 : service 의 핵심 실행 코드로 고양이를 cats 에 저장하고 저장된 cats 를 조회 할 수 있는 기능이다.

controller 코드를 보자

import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
import { Roles } from '../common/decorators/roles.decorator';
import { RolesGuard } from '../common/guards/roles.guard';
import { ParseIntPipe } from '../common/pipes/parse-int.pipe';
import { CatsService } from './cats.service';
import { CreateCatDto } from './dto/create-cat.dto';
import { Cat } from './interfaces/cat.interface';
@UseGuards(RolesGuard)
@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@Get()
async findAll(): Promise<Cat[]> {
return this.catsService.findAll();
}
@Get(':id')
findOne(
@Param('id', new ParseIntPipe())
id: number,
) {
// get by ID logic
}
}
  • 10번 줄 : Controller 의 prefix 를 정의할 수 있다. http://host:port/prefix 방식으로 호출된다.
  • 14, 20, 25 번 줄 : API method 를 정의 할 수 있다. 내부에 endpoint 를 string 형식으로 설정 할 수 있다. 25번 줄과 같이 사용하면 get 호출의 path 에 id 를 넣어 호출 할 수 있다. 현재 예제에서는 http://host:port/cats/1 과 같이 호출하면 ‘1’ 이 id 아규먼트에 할당되어 들어간다.

위의 Controller 소스코드는 일부러 import 구문까지 다 넣어뒀는데, 이는 Nest 공식 페이지에서 설명해주는 대부분 구현체가 이 소스코드에서 적용되기 때문이다. 먼저는 RolesGuard 라는 녀석을 살펴보자

roles.guard.ts

@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get<string[]>('roles', context.getHandler());
if (!roles) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
const hasRole = () =>
user.roles.some(role => !!roles.find(item => item === role));
return user && user.roles && hasRole();
}
}
  • 2번 줄 CanActivate 의 구현체이다. 이것은 Nest 에서 Guard 역할을 하기 위한 필수이다.
  • 6번 줄에서는 roles 라는 녀석을 구해온다. Controller 소스코드에 @Roles(‘admin’) 이런 녀석을 보았는가? reflecter 를 통해 admin 이라는 roles 를 가져온다. (더 자세히는 roles.decorator.ts 파일에서 roles 라는 metadata 를 설정해주는 것을 볼 수 있다.)
  • 11–15번 줄까지 request 의 user 객체로부터 roles 를 가져와서 user 가 가진 roles 중 하나라도 controller 에 설정된 role 과 같은것이 있다면 true 를 반환한다. (Authorized)

Nest 공식 홈페이지의 Guard 에 대한 설명에 따르면, 기존 Express 에서는 이러한 구현을 Middleware 에서 처리했으나 Middleware 는 다음에 처리할 Route handler 의 정보가 없기 때문에 이 정보를 가지고 있는 Guard 를 쓰도록 권장하는 것으로 보인다. Guard 는 Middleware 가 있을경우 Middleware 다음으로 실행 되며 나머지 Interceptor 등 보다는 앞서 실행된다.

전역으로 사용할 경우 main 코드에 다음을 추가 해 준다.

const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());

Request 의 구현체에 User 가 어떻게 들어오는 지 보기위해 Debugging 을 해봤는데 결과는 undefined 였다.

이어지는 샘플들에서 구현되는 사항들이 아닐까 생각된다.

parse-int.pipe.ts

@Injectable()
export class ParseIntPipe implements PipeTransform<string> {
async transform(value: string, metadata: ArgumentMetadata) {
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new BadRequestException('Validation failed');
}
return val;
}
}

Controller 부분에 @Param(‘id’, new ParseIntPipe()) 의 ParseIntPipe 의 본모습이다. parseInt 에 실패하면 BadRequestException 을 throw 한다.

02-gateways

npm install 을 했을 때 ‘’’Error: Cannot find module ‘nan’’’’ 이런 에러가 있어서 ‘npm i -g nan’ 을 먼저 실행하고 npm install 을 실행하니 큰문제 없이 설치되는 듯 했다. 혹시 같은 문제가 있다면 이렇게 해보기 바란다.

websocket 관련 내용으로 추후 다루도록한다.

05-sql-typeorm

typeorm 의 간단한 구현코드이다. (정말 너무 간단했다. 이번에도 nan 이란 녀석이 없다고 나왔는데 -g 옵션이 안먹히는걸까?) 하나씩 천천히 살펴보자.

app.module.ts

아래 코드와 같이 Datasource 설정 해 줄 수 있다. database 는 미리 만들어져 있어야한다.

@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'password',
database: 'test',
autoLoadEntities: true,
synchronize: true,
}),
UsersModule,
],
})
export class AppModule {}

user.controller.ts

@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
create(@Body() createUserDto: CreateUserDto): Promise<User> {
return this.usersService.create(createUserDto);
}
@Get()
findAll(): Promise<User[]> {
return this.usersService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string): Promise<User> {
return this.usersService.findOne(id);
}
@Delete(':id')
remove(@Param('id') id: string): Promise<void> {
return this.usersService.remove(id);
}
}

user.module.ts

Module 안에서 TypeOrmModule 로 엔터티 class 를 import 해준다.

@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UsersService],
controllers: [UsersController],
})
export class UsersModule {}

user.service.ts

repository 선언이 따로 없이도 이런식으로 사용가능하다. 코드 가독성이 매우 좋게 짜여진다.

@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly usersRepository: Repository<User>,
) {}
create(createUserDto: CreateUserDto): Promise<User> {
const user = new User();
user.firstName = createUserDto.firstName;
user.lastName = createUserDto.lastName;
return this.usersRepository.save(user);
}
async findAll(): Promise<User[]> {
return this.usersRepository.find();
}
findOne(id: string): Promise<User> {
return this.usersRepository.findOne(id);
}
async remove(id: string): Promise<void> {
await this.usersRepository.delete(id);
}
}

23-graphql-code-first

graphql 의 경우, 샘플 코드만으로 유추가 어려운 측면이 있어 여러가지를 찾아봤는데 https://www.youtube.com/watch?v=eHn64NxMwJY 이 영상이 실제 구현에 어려웠던 점들을 해결하는데 도움을 주었다. 글을 보기 힘든 사람이라면 영상을 보며 따라해도 좋을것 같다.

graphql 의 경우 개념이해 부터 실구현까지 생각보다 여러가지 난관이 있었어서 sample code 만으로 이해하기가 어려워 직접 샘플을 만들어보았다. typeorm 과 graphql 까직 적용한 샘플 코드를 우선 필자의 개인 깃헙 에 올려 두었으니 참고바란다.

Graphql 은 API를 호출하는 입장에서 응답 값을 제어 할 수 있도록 해 주는 패키지이다. 처음 가장 삽질했던 것이 playground 라는 녀석이었는데, 원래 node 를 다루지 않고 nest 부터 시작한 나같은 사람은 어리둥절 할 수 밖에 없다. nest 공식홈페이지에 playground 에 대한 설명이

‘’’The playground is a graphical, interactive, in-browser GraphQL IDE’’’

이렇게 나와있는데 뒤에 IDE 만보고 필자가 든 생각은

‘graphql 을 작업하는 IDE 가 따로있나보다. 난 계속 vscode 써야지’

였다. 지금 생각하면 우습지만, 어쨌든 덕분에 시간을 많이 낭비한 셈이다. 12번 예제 코드를 실행하고 ‘http://localhost:3000/graphql’ 에 접속 해 보자. 아래와 비슷한 화면이 나온다면 성공이다. 이 playground 라는 녀석은 vscode, intellij 같이 완전 통합된 방식의 개발환경을 제공하지는 않지만, graphql 의 쿼리를 만드는데 있어서는 상당히 유용하게 쓰이는 IDE 이다.

POSTMAN 을 써 본 분들이라면 이런 layout 이 상당히 익숙할 것이다. POSTMAN 에서도 graphql API 를 테스트 해 볼 수 있다. 이 내용은 후술하도록하겠다. 23번 예제의 recipes.service.ts 파일을 보면 return 이 없는 dummy 임을 알 수 있다. 그래서 호출은 가능하지만 결과를 보기는 어렵다.

graphql 구현에 가장 중요한 것은 resolver 라는 녀석이다. graphql 예제에는 보통 controller 가 없는데, 이 resolver 가 controller 역할을 하게 된다. 아래는 recipes.resolver.ts 의 코드이다.

@Resolver(of => Recipe)
export class RecipesResolver {
constructor(private readonly recipesService: RecipesService) {}
@Query(returns => Recipe)
async recipe(@Args('id') id: string): Promise<Recipe> {
const recipe = await this.recipesService.findOneById(id);
if (!recipe) {
throw new NotFoundException(id);
}
return recipe;
}
@Query(returns => [Recipe])
recipes(@Args() recipesArgs: RecipesArgs): Promise<Recipe[]> {
return this.recipesService.findAll(recipesArgs);
}
@Mutation(returns => Recipe)
async addRecipe(
@Args('newRecipeData') newRecipeData: NewRecipeInput,
): Promise<Recipe> {
const recipe = await this.recipesService.create(newRecipeData);
pubSub.publish('recipeAdded', { recipeAdded: recipe });
return recipe;
}
@Mutation(returns => Boolean)
async removeRecipe(@Args('id') id: string) {
return this.recipesService.remove(id);
}
@Subscription(returns => Recipe)
recipeAdded() {
return pubSub.asyncIterator('recipeAdded');
}
}
  • 5, 14,19,28번 줄: Query 어노테이션은 일반적으로 조회 task 일 경우 사용한다고 보면 될 것같다. 실제 Database 에 변형이 가해질 경우 19,28번 줄의 Mutation 어노테이션을 사용 해 준다.
  • 33번 줄: Subscription 어노테이션은 recipeAdded 라는 이벤트가 publish 될 때마다 응답값을 받아오게 하는 기능이 있다.

이해를 돕기위한 테스트 방법

http://localhost:3000/graphql’ 에 접속해서 첫번째 텝에서 아래와 같이 입력하고 실행한다.

subscription {
recipeAdded{
description
}
}
  • 버튼을 눌러 다음 텝을 열고 아래와같이 입력한다.
mutation addRecipe($newRecipeData: NewRecipeInput!){
addRecipe(newRecipeData: $newRecipeData) {
description
}
}

가장 아래에 Query Variable 에 아래와 같이 입력하고 실행한다.

{
"newRecipeData": {
"title": "test",
"description": "asdfghjklzxcvbnmqwertyuiopasdfg",
"ingredients": ["1","2"]
}
}

또는 Postman 에서 다음과같이 API 호출을 해줘도 된다. (아래는 필자의 깃헙에 올린 API 대상으로 실험한 화면이다.)

마치며

회사에서 차기 솔루션 개발에 기술 스택으로 Nest + graphql 을 후보군으로 두어 생애 처음으로 Node 프로젝트를 해보았다. Toy 프로젝트로 Vue 페이지를 만들어본적은 있었지만 framework 를 사용해서 API 까지 만드는 실습을 하는것은 처음이라 어렵긴했지만 정말 재밌게 공부했던 것 같다.

이 글이 많이 읽힐지는 모르겠지만 혹시나 누군가 보게된다면 도움이 되길 바라며 문서를 마치겠다.

--

--