Orders 관련 서비스 작성
1. Controller API Endpoint 추가
주문을 생성하는 API를 설계하도록 하겠습니다. 먼저 Controller에 Endpoint를 설정해줍니다. 단순하게 설정할 것이므로 경로는 루트 경로를 따르겠습니다.
POST와 PATCH, PUT 등은 바디를 파라미터로 사용해야 합니다. 그렇기 때문에 DTO도 설정해줍니다. 바디와 같이 데이터가 서버로 요청이 올 때는 데이터를 외부에 호출시키지 않고 숨긴 상태로 전달하는 것이 좋습니다. 그래서 데이터 전송 객체(Data Transfer Object, DTO)가 사용됩니다. 완성된 코드는 아래와 같습니다.
// orders.controller.ts
@Post()
async createOrder(@Body() request: CreateOrderRequest) {
return this.ordersService.createOrder(request);
}
2. DTO 추가하기
Orders 프로젝트의 src 디렉터리에 dto 폴더를 만들어줍니다. 그리고 그 안에 create-order.request.ts를 만듭니다. 이때, 해당 데이터 객체가 유효한지 판별해야 하므로 class-validator를 설치해줍시다.
yarn add class-validator class-transformer
# npm i class-validator class-transformer
그 이후 객체를 정의해주어야 합니다. 객체의 프로퍼티와 값은 무엇인지, 또한 내가 설정한 타입에 걸맞게 입력이 되었는지, 프로퍼티 값은 원하는 대로 입력되었는지 검사하는 것이 좋습니다.
// create-order.request.ts
import {
IsNotEmpty,
IsPhoneNumber,
IsPositive,
IsString,
} from 'class-validator';
export class CreateOrderRequest {
@IsString()
@IsNotEmpty()
name: string;
@IsPositive()
price: number;
@IsPhoneNumber()
phoneNumber: string;
}
이렇게 설정했을 때, 이름은 문자열에 필수로 입력되어야 하며, 물건의 값은 양수, 핸드폰 번호는 규격에 맞추어서 작성해야 하도록 DTO가 만들어져야 합니다. 이를 유효한지 확인하도록 class-validator를 사용하는 것입니다.
하지만, 이렇게만 하면 각각의 패키지는 NestJS에서 파이프로 사용하기 때문에 main.ts에서 모든 API에서 사용할 수 있도록 등록을 해야 했고, 여기서는 전역 파이프로 등록을 합니다.
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { OrdersModule } from './orders.module';
async function bootstrap() {
const app = await NestFactory.create(OrdersModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
이때 자세히 보면 포트가 하드코딩되어 있기 때문에 이를 위해 configService를 사용합니다.
// main.ts(orders)
const configService = app.get(ConfigService);
await app.listen(configService.get('PORT'));
그리고 이렇게 세팅했으므로 환경 변수에 PORT를 적어주어야 합니다. 바로 적어줍시다.
# .env
PORT=3000
그런데, 앞서 환경변수를 module에서 유효성 검사를 했던 것 기억하시나요? 환경변수를 하나 추가했으므로 이에 대한 검증을 위해 유효성 검사 스키마 객체에 프로퍼티를 추가해주어야 합니다.
// orders.module.ts
validationSchema: Joi.object({
MONGODB_URI: Joi.string().required(),
PORT: Joi.number().required(),
}),
프로퍼티를 추가했으므로, 본격적으로 API를 구축하러 가봅시다.
백엔드에서는 데이터를 다루기 때문에 당연하게 레포지토리를 사용해야 하므로 레포지토리를 생성자에 넣어줍니다. 그리고 단순히 DB에 데이터를 넣으므로 레포지토리의 create 메서드를 사용합니다.
import { Injectable } from '@nestjs/common';
import { CreateOrderRequest } from './dto/create-order.request';
import { OrdersRepository } from './orders.repository';
@Injectable()
export class OrdersService {
constructor(private readonly ordersRepository: OrdersRepository) {}
async createOrder(request: CreateOrderRequest) {
return this.ordersRepository.create(request);
}
}
이렇게 한 후 docker compose up으로 컨테이너를 구동시키면 에러가 발생합니다. class-validator를 설치했는데 class-validator가 정의되지 않았다고 나와 있습니다. 그 이유는, 새로운 의존성을 추가할 때마다 애플리케이션을 재빌드해야 하기 때문입니다. 그래서 다시 빌드해주도록 명령어를 입력해줍니다.
docker compose up --build -V
3. API 빌드하기
이제 애플리케이션도 성공적으로 빌드하였으니, api를 테스트할 준비가 완료되었습니다. 그 전에, 먼저 orders controller로 다시 가서 orders 컨트롤러 데코레이터에 orders를 놓아서 공통적으로 orders에서 API를 접속할 수 있도록 세팅해줍니다.
// orders.controller.ts
@Controller('orders')
이제 POST 메서드로 http://localhost:3000/orders에 요청을 보내봅시다. POST에서는 Body값을 DTO로 사용하므로 요청 Body를 적어주어야 합니다.
요청을 보내니 201 상태코드와 함께 생성된 로우 값을 반환하는 것을 볼 수 있습니다.
하지만, 예를 들어 price를 적지 않는 등 유효하지 않은 데이터를 요청을 보낸다면,
이처럼 유효한 값이 아니므로 유효성 검사를 통해 에러를 발생시키게 됩니다.
그 다음으로는 이미 존재하는 주문을 반환하는 API를 설계하여 우리의 데이터베이스가 실제로 주문을 적절히 저장하고 있는지 확인해보겠습니다. GET 메서드를 통해서 주문 정보를 조회하는 API를 설계합니다. 엔드포인트는 orders 라우터의 루트 경로로 하겠습니다.
// orders.controller.ts
@Get()
async getOrders() {
return this.ordersService.getOrders();
}
이제 서비스를 작성합니다.
// orders.service.ts
async getOrders() {
return this.ordersRepository.find({});
}
조건 없이 모든 주문을 가져오기 때문에 find의 인자인 filterQuery에는 빈 객체를 전달하여 마무리합니다. 이제 요청을 보내면 우리가 등록한 주문에 대한 배열이 반환되어야 합니다.
이제 애플리케이션에서 마이크로서비스를 연결할 준비가 모두 마무리 되었습니다. 이제 billing 애플리케이션을 RabbitMQ 마이크로서비스로 구축해보겠습니다.
마이크로서비스 구축
1. RabbitMQ 관련 패키지 설치
터미널에서 마이크로서비스를 위한 의존성을 추가해주어야 합니다. @nestjs/microservices를 추가합니다.
yarn add @nestjs/microservices
# npm i @nestjs/microservices
또한, RabbitMQ와의 connection을 위하여 amqlib, amqp-connection-manager를 필요로 하기 때문에 설치해줍니다.
yarn add amqplib amqp-connection-manager
2. 소스코드 작성
RabbitMQ를 사용하기 위해서 라이브러리 디렉터리로 이동합니다. rmq 폴더를 common/src에 만들어서 리소스 관련 파일들을 만듭니다.
모듈 생성
// rmq.module.ts
import { Module } from '@nestjs/common';
@Module({})
export class RmqModule {}
서비스 생성
이 서비스는 공용 RabbitMQ 함수들을 캡슐화 해줍니다. 처음으로 추가할 메서드는 getOptions로 각각의 마이크로서비스가 초기화될 때마다 호출할 메서드입니다. 이렇게 함으로써 우리가 사용할 RabbitMQ 마이크로서비스에 대한 모든 옵션들을 알아낼 수 있도록 하나로 묶을 수 있습니다.
// rmq.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class RmqService {
getOptions(queue: string, noAck = false) {}
}
그래서 우리가 초기화하고자 하는 RabbitMQ의 Queue에 대한 이름을 알아야 하며 noAck이라는 프로퍼티를 만들어 기본값을 false로 지정합니다. Rabbitmq에서 메시지를 큐로부터 삭제하기 전에 수동으로 메시지를 확인할 수 있습니다. 기본적으로 NestJS에서는 자동으로 모든 메시지를 확인해주기 때문에, T수동으로 확인하고자 한다면 별도로 세팅을 지정해주어야 합니다.
// rmq.service.ts
getOptions(queue: string, noAck = false): RmqOptions {
return {
transport: Transport.RMQ,
options: {
urls: [this.configService.get<string>('RABBIT_MQ_URI')],
queue: this.configService.get<string>(`RABBIT_MQ_${queue}_QUEUE`),
noAck,
persistent: true,
},
};
}
이 메서드에서는 객체를 반환값으로 설정했습니다. 반환할 값의 타입을 지정해주어야 하는데 RabbitMQ 옵션(RmqOptions)으로 지정해줍시다.
또한, 반환할 옵션 객체의 속성들을 지정해 줍니다. rabbitmq로 전달하기 때문에 transport속성의 값으로 Transport.RMQ를 지정해줍니다.
options를 설정합니다. RabbitMQ 마이크로서비스에 옵션을 제공하기 위해서 configService를 사용합니다. 옵션으로 RabbitMQ가 브로커들로부터 listen할 urls 리스트를 제공하고, 이 리스트는 환경변수인 'RABBIT_MQ_URI'를 통해 가져옵니다. 그 다음 우리가 만들고 있는 queue의 이름을 지정합니다. 위의 경우와 같이 'RABBIT_MQ_${queue}` 환경변수를 가져와 옵션으로 전달합니다. 마지막으로 noAck 속성을 지정하고, 메세지 리스트를 유지하기 위해서 persistent 속성의 값을 true로 저장합니다.
3. billing 애플리케이션 main.ts 설정
이제 RabbitMQ 마이크로서비스를 초기화하기 위한 공용 함수가 완성됐으므로 billing 애플리케이션으로 이동합니다.
첫 번째로 할 것은 RabbitMQ 마이크로서비스에 대하여 접근할 수 있도록 하는 것입니다. main.ts파일로 이동하여 관련 설정을 해줍시다.
main.ts에서 rabbitmq service를 사용하기 위해 app.get을 사용하여 가져옵니다. 이때, 경로를 @app/common으로 하면 오류가 발생하기 때문에, 공용 라이브러리의 index.ts로 이동하여 export 해주도록 합니다.
// lib/common/src/index.ts
...
export * from './rmq/rmq.service'
마지막으로, rmq 모듈에서 rmq service를 프로바이더로 지정하고, export 목록에도 기입해줍니다.
// rmq.module.ts
@Module({
providers: [RmqService],
exports: [RmqService],
})
export class RmqModule {}
다시 main.ts로 돌아와서 rabbitmq 마이크로서비스를 연결해줍시다. 연결할 때는 앞서 만들었던 getOptions 객체를 사용해줍니다.
const rmqService = app.get(RmqService);
app.connectMicroservice(rmqService.getOptions('BILLING'));
getOptions 메서드에서 queue를 매개변수로 하기에 'BILLING'을 큐로 사용하기 위해 인자로 넣어 메세지를 확인하도록 하겠습니다. noAck은 기본값인 false로 두어서 우리가 받을 acknowledge 메시지를 수동으로 전달받을 수 있도록 하겠습니다. 실패가 발생하는 경우 메시지를 다시 확인하고자 합니다.
그리고 app.listen 대신에 app.startAllMicroservices를 사용하여 마이크로서비스를 사용합니다.
await app.startAllMicroservices();
이렇게 하여 RabbitMQ 마이크로서비스를 시작하기 위한 모든 것들을 완수했습니다. billing.module.ts에도 까먹지 않고 Rmq Module을 임포트해줍시다. 앞선 Rmq Service와 같은 이유로 index.ts에 export 해주는 것이 좋습니다. 경로가 매우 복잡하기 때문에 @app/common으로 줄이기 위함이 목적입니다.
// billing.module.ts
import { Module } from '@nestjs/common';
// import { RmqModule } from '../../../libs/common/src/rmq/rmq.module';
import { RmqModule } from '@app/common';
import { BillingController } from './billing.controller';
import { BillingService } from './billing.service';
@Module({
imports: [RmqModule],
controllers: [BillingController],
providers: [BillingService],
})
export class BillingModule {}
// lib/common/src/index.ts
...
export * from './rmq/rmq.module';
그리고 billing 애플리케이션에 사용할 환경변수를 생성하기 위해 .env 파일을 만듭니다.
처음으로 만들 환경변수는 RABBIT_MQ_URI 입니다. RabbitMQ가 실제로 동작할 수 있을 때까지 아직 빈 값으로 두겠습니다. 그 다음으로는 RABBIT_MQ_BILLING_QUEUE을 설정해줍니다. 이렇게 변수명을 지은 이유는 rmq.service에서 queue 속성의 값으로 `RABBIT_MQ_${queue}_QUEUE`라고 정했기 때문에 상호작용하기 위해 해당 표현식으로 변수를 적었습니다. 큐는 billing으로 사용하기 때문에 바로 값을 billing으로 할당합니다.
# billing/.env
RABBIT_MQ_URI=
RABBIT_MQ_BILLING_QUEUE=billing
이제 애플리케이션을 구동하기 위해서 orders 애플리케이션 때와 같이 도커파일을 만들어 애플리케이션을 컨테이너화하고 실행하도록 할 수 있습니다. 그러면 billing 애플리케이션 디렉터리에 Dockerfile을 만듭시다!
FROM node:alpine As development
WORKDIR /usr/src/app
COPY package*.json ./
RUN yarn install
COPY . .
RUN yarn run build
FROM node:alpine as production
ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}
WORKDIR /usr/src/app
COPY package*.json ./
RUN yarn install --only=production
COPY . .
COPY --from=development /usr/src/app/dist ./dist
CMD ["node", "dist/apps/billing/main"]
도커파일을 만들어냈기 때문에 도커 컴포즈에 추가해줍시다. docker-compose.yml에서 billing 서비스를 추가합니다.
billing:
build:
context: .
dockerfile: ./apps/billing/Dockerfile
target: development
command: yarn start:dev billing
env_file:
- ./apps/billing/.env
depends_on:
- mongodb-primary
- mongodb-secondary
- mongodb-arbiter
volumes:
- .:/usr/src/app
- /usr/src/app/node_modules
rabbitmq도 사용하므로 컨테이너로 만들어주어야 합니다. 따라서 rabbitmq도 서비스로 작성해줍니다.
rabbitmq:
image: rabbitmq
ports:
- '5672:5672'
rabbitmq를 컨테이너화하기 전에 URI를 환경변수에 적용해줍시다.
RABBIT_MQ_URI=amqp://rabbitmq:5672
그리고, billing.module.ts에서 ConfigModule도 등록해주어야 합니다.
// billing.module.ts
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
validationSchema: Joi.object({
RABBIT_MQ_URI: Joi.string().required(),
RABBIT_MQ_BILLING_QUEUE: Joi.string().required(),
}),
}),
RmqModule,
],
controllers: [BillingController],
providers: [BillingService],
})
export class BillingModule {}
환경변수를 사용하기 위해 ConfigModule을 임포트하여 등록하고, 환경변수에 유효성 검사를 진행하여 검사를 합니다.
이제 도커 컴포즈를 통해 한 번에 컨테이너를 구동합니다. 새로운 서비스도 생겼거니와, 의존성도 추가했기 때문에 재빌드해주도록 합시다. 정상적으로 작동이 되었습니다 😆 다음에는 애플리케이션 간 연결을 해보겠습니다!
마치며
몽고디비보다 세팅이 쉬워서 금방 적용했습니다. 아직 RabbitMQ를 진정으로 사용해보지 못해서 아쉬워서 제대로 다음 포스트에서 시도해보려고 합니다. 영어 강의인데 너무 깔끔하고 다양한 내용을 배울 수 있어서 좋습니다 :)
'Node.js > NestJS' 카테고리의 다른 글
[NestJS - Microservice] NestJS로 마이크로서비스 구축해보기(RabbitMQ, MongoDB, Docker) - 2. MongoDB 추가, Orders 모듈 세팅 (0) | 2022.11.18 |
---|---|
[NestJS - Microservice] NestJS로 마이크로서비스 구축해보기(RabbitMQ, MongoDB, Docker) - 1. 프로젝트 세팅 (0) | 2022.11.14 |
[NestJS - Caching] 15분 안에 NestJS에서 캐싱 사용해보기 (2) | 2022.11.11 |
[NestJS] 12. 비동기 프로바이더 (0) | 2022.10.02 |
[NestJS] 11. 커스텀 프로바이더 (0) | 2022.10.02 |