본문 바로가기

Backend/NestJS

NestJS - 대충 서비스 만들어보기 (2)

지난 포스팅에서 생성만 했던 nest-ad 프로젝트에서 이어서 가겠습니다.

 

- 리소스 생성

 

NestJS 에선 cli 로 필요한 리소스를 쉽게 생성할 수 있습니다. 먼저 nest-cli 가 어떤 것들을 지원하는지 살펴보겠습니다.

 

> nest --help

 

 

이전 포스팅에서 package.json 의 scripts 를 간략히 볼 때 나왔던 build, start 등의 커맨드도 보입니다.

프로젝트에서 필요한 리소스를 구성하는데 주로 사용할 커맨드는 아무래도 generate 입니다. alias 인 g 를 쓸 수도 있네요.

그 아래는 생성할 수 있는 컬렉션들을 보여주고 있습니다.

 

저는 간단한 광고 서비스 서버를 만들고 싶으니 Ads 라는 도메인에 대해 먼저 Controller 부터 생성해보고자 합니다.

 

> nest g controller Ads

 

 

 

src 하위에 ads 라는 디렉토리가 생성되고 그 내부에 ads controller 와 spec 코드가 같이 생성된 걸 볼 수 있습니다.

메인 app module 에 추가도 알아서 해주는군요. 이런식으로 service, module, dto, entity 등을 하나하나 생성해서 구성해도 되지만

생성할 수 있는 컬렉션 중 Resource 로 만들면 기본적으로 필요한 것들을 전부 자동으로 생성해줍니다.

 

> nest g resource Ads

 

 

 

아까 Controller 를 생성할 때랑 다르게 Resource 를 생성하면 NestJS 에서 어떤 Transport layer 를 생성할지 물어봅니다.

현재 만들려는건 간단한 광고 서비스 서버니 REST API 로 시작합니다.

 

 

Ads 에 대해 CRUD 를 수행하는 entry point 도 생성할거냐고 추가로 묻습니다. 생성해줍니다.

 

 

생성에 실패했습니다. 아까 Controller 를 생성하고 그대로뒀기때문에 해당 리소스가 이미 ads 디렉토리를 차지하고 있어 Conflict 이

발생했기 때문입니다. 아까 만들어진 ads 디렉토리를 제거하고, nest-cli 를 통해 다시 생성합니다.

 

 

ads 하위에 module, controller, service, entity, dto 등이 전부 생성되었습니다.

아 참고로 저는 아까 처음 만들었던 Controller 관련된걸 제거하면서 app.module.ts 의 controllers 와 providers 에 남아있던 코드까지

전부 제거했습니다. (기존 app.controller.ts, appl.service.ts 포함)

 

import {
  Controller,
  Get,
  Post,
  Body,
  Patch,
  Param,
  Delete,
} from '@nestjs/common';
import { AdsService } from './ads.service';
import { CreateAdDto } from './dto/create-ad.dto';
import { UpdateAdDto } from './dto/update-ad.dto';

@Controller('ads')
export class AdsController {
  constructor(private readonly adsService: AdsService) {}

  @Post()
  create(@Body() createAdDto: CreateAdDto) {
    return this.adsService.create(createAdDto);
  }

  @Get()
  findAll() {
    return this.adsService.findAll();
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.adsService.findOne(+id);
  }

  @Patch(':id')
  update(@Param('id') id: string, @Body() updateAdDto: UpdateAdDto) {
    return this.adsService.update(+id, updateAdDto);
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return this.adsService.remove(+id);
  }
}

 

아까 CRUD entry point 도 같이 생성했기 때문에, ads.controller.ts 파일을 보면 위와 같이 생성된 것을 볼 수 있습니다.

Controller 데코레이터를 통해 ads 라는 경로로 라우팅되면서 각 CRUD operation 에 해당하는 GET, POST, PATCH, DELETE 데코레이터가 붙었네요. 이렇게 데코레이터로 표현해주는게 가독성에 상당한 도움이 되는 것 같습니다.

그럼 한번 정상적으로 동작하는지 먼저 체크해보죠.

 

> npm run start:dev

 

 

브라우저를 통해 ads 경로로 GET 접근을 해보면 정상 응답합니다. 여기서부터 하나하나 살을 붙여나가면 될 것 같네요.

살을 붙이기에 앞서 생성된 리소스들에 대해 부가적인 것들을 간단히 살펴보겠습니다.

 

- 라우팅 패스 와일드 카드(*) 사용

 

데코레이터 중 @Controller 나 @Get, @Post 등에는 라우팅 패스를 지정할 수 있는데, 이때 와일드 카드도 지정할 수 있습니다.

 

@Controller('ads')
export class AdsController {
  constructor(private readonly adsService: AdsService) {}

  ...

  @Get('/tmp/tm*p')
  findAll() {
    return this.adsService.findAll();
  }
  
  ...
}

 

아까는 findAll 메서드의 @Get 데코레이터에 인자를 넣지 않았기 때문에 localhost:3000/ads 요청에 대해선 findAll 로 들어왔지만,

위처럼 변경하면 localhost:3000/ads 요청에 대해선 404 Not Found 응답을 합니다.

반면 localhost:3000/tmp/tmp 또는 localhost:3000/tmp/tm.p 등과 같은 경로로 정규 표현식의 와일드카드처럼 동작해 요청을

처리할 수 있게 됩니다. 물론 localhost:3000/tmp/tm#p 와 같은 특수 기호에 대해선 동작하지 않습니다.

그냥 이런게 가능하다 정도지 사실 라우팅 패스에 와일드 카드를 쓸 일은 거의 없을 것 같습니다.

 

- 요청 및 응답 객체

 

일반적으로 Express 에서 req, res 객체를 다루듯이 NestJS 에서도 해당 객체들을 다룰 수 있습니다.

 

import { ..., Req, Res } from '@nestjs/common';

@Controller('ads')
export class AdsController {
  constructor(private readonly adsService: AdsService) {}

  ...

  @Get()
  findAll(@Req() req, @Res() res) {
    console.info(req);
    const ads = this.adsService.findAll();
    return res.status(200).send(ads);
  }
  
  ...
}

 

Req, Res 데코레이터는 다른 데코레이터들과 마찬가지로 @nestjs/common 이 제공합니다.

위처럼 작성후 요청을 보내보면, req 객체가 어떻게 구성되어있는지 많은 정보들을 확인할 수 있습니다.

res 객체를 사용한 응답 구성은 Express 에서 흔히 보던 방법입니다. 똑같이 할 수 있다는걸 보였을 뿐 이 객체들을 직접 다룰 일이 당장은 많지 않을 것 같습니다.

 

res.status 와 같이 res 객체를 사용해 응답 코드를 지정할 수도 있겠지만, NestJS 에선 역시나 이또한 데코레이터로 기능을 제공합니다.

 

import { ..., HttpCode } from '@nestjs/common';

@Controller('ads')
export class AdsController {
  constructor(private readonly adsService: AdsService) {}

  ...

  @HttpCode(202)
  @Get()
  findAll() {
    return this.adsService.findAll();
  }
  
  ...
}

 

HttpCode 데코레이터를 사용해 해당 요청에 대한 응답 코드를 바꿀 수 있습니다. 위처럼 정상 응답에 대한 코드를 202 로 바꾸고,

요청을 보내면 아래와 같은 응답을 확인할 수 있습니다.

 

> curl -I localhost:3000/ads

 

 

- 리다이렉션

 

서버에선 간혹 요청을 처리하고선 클라이언트를 다른 페이지로 이동하고 싶을 때가 있습니다.

여기서 하진 않겠지만, 예를 들면 광고 서버의 경우엔 클릭 이벤트가 발생했을 때 관련 로직들을 처리한 후 본래 해당 컨텐츠의 페이지로 

리다이렉트 시키는게 일반적인 동작입니다. 이럴 경우 Express 에선 res 객체의 redirect 메서드를 사용했지만, NestJS 에선

역시나 Redirect 데코레이터를 사용할 수 있습니다.

 

import { ..., Redirect, Query } from '@nestjs/common';

@Controller('ads')
export class AdsController {
  constructor(private readonly adsService: AdsService) {}

  ...
  
  @Redirect('https://nestjs.com', 302)
  @Get('click')
  click(@Query('id') id) {
    if (id === '100') {
      return { url: 'https://docs.nestjs.com/' };
    }
  }
  
  ...
}

 

간단한 예시 코드입니다. /ads/click 경로로 요청이 들어오면, Redirect 데코레이터를 통해 https://nestjs.com 페이지로 보냅니다.

그런데 저 메서드 안에 로직들은 뭘 하는거냐면, 로직 결과에 따라 동적으로 리다이렉트 페이지를 변경하는 코드입니다.

/ads/click 요청 중 query parameter 로 id 값이 100 으로 들어온 경우엔 보내고자하는 페이지의 주소를 url 에 담은 리터럴 객체를 반환하면 해당 주소로 리다이렉트 되는걸 확인할 수 있습니다.

 

localhost:3000/ads/click / localhost:3000/ads/click?id=100 두 요청에 대한 리다이렉트를 직접 확인해보시면 될 것 같습니다.

 

- MicroService

 

이건 지금 자세히 보려는건 아니고, 아까 nest-cli 를 통한 Resource 생성시 REST API, GraphQL, WebSockets 외에 목록에 있었던Microservice 를 선택했을 땐 어떻게 생성되는지만 확인해봤습니다.

 

import { Controller } from '@nestjs/common';
import { MessagePattern, Payload } from '@nestjs/microservices';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

@Controller()
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @MessagePattern('createUser')
  create(@Payload() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }

  @MessagePattern('findAllUsers')
  findAll() {
    return this.usersService.findAll();
  }

  @MessagePattern('findOneUser')
  findOne(@Payload() id: number) {
    return this.usersService.findOne(id);
  }

  @MessagePattern('updateUser')
  update(@Payload() updateUserDto: UpdateUserDto) {
    return this.usersService.update(updateUserDto.id, updateUserDto);
  }

  @MessagePattern('removeUser')
  remove(@Payload() id: number) {
    return this.usersService.remove(id);
  }
}

 

module, service, entity, dto 등은 동일한데 controller 의 코드가 다른것을 볼 수 있습니다.

REST API 는 CRUD operation 에 대해 GET, POST, PATCH, DELETE method 로 나타내게 되어 있으므로 각 명칭과 동일한 데코레이터로 구성되어있었지만, 일반적인 MSA 구성에선 (꼭 그런건 아니지만) 이벤트 기반 구조를 취하는 경우가 많으므로 MessagePattern 데코레이터로 구성되어있는 것을 볼 수 있습니다.

 

MessagePattern 데코레이터를 통해 이벤트 핸들러를 등록하여 해당 이벤트가 발생했을 때 로직을 처리하는 방식입니다.

반대로 이벤트를 send 하거나 emit 하는 것도 구현 가능하며, 스트림 완료에 대한 알람도 받을 수 있습니다.

아까 Microservice Resource 를 생성할 때 전송 계층에 non-HTTP 라고 써있었듯이 전송에는 TCP, KAFKA, REDIS, GRPC 등을 사용합니다. MicroService 에 대해선 이정도까지만 보고 나중에 좀 자세히 들여다보도록 하겠습니다.

 

자동 생성된 dto 나 entity 들은 여기서 더 살을 붙여나가면서 당연히 수정해야하기에 이후 포스팅에서 살펴봅니다.