다음의 명령어로 order-service를 생성하도록 하겠습니다.
nest new order-service
nest generate module order
nest generate controller order
nest generate service order
E-commerce 어플리케이션에서 order-service가 담당하는 부분은 다음과 같습니다.
1) 유저가 order를 할 경우 catalog-service와 통신을 통해 order 생성
2) 유저가 order에 대한 정보를 알고 싶을 경우 order를 유저에게 반환
3) 유저가 order 정보를 수정할 경우 catalog-service와 통신을 통해 order 수정
4) 유저가 order를 취소할 경우 status값을 CANCEL_ORDER로 변경
5) 유저가 order를 재주문할 경우 status값을 RE_ORDER로 변경
그러면 상기 목적들을 중점적으로 order-service를 구현하도록 하겠습니다.
order-service도 마찬가지로 catalog-service와 비슷하므로 필요한 부분은 카피하면서 수정해나가는데 우선 rabbitmq에 관한 설정들은 하지않고 구현을 마친 후에 rabbitmq를 도입하도록 하겠습니다.
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(7200);
}
bootstrap();
import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common';
import { OrderDto } from 'src/dto/order.dto';
import { RequestCreate } from 'src/vo/request.create';
import { RequestUpdate } from 'src/vo/request.update';
import { ResponseOrder } from 'src/vo/response.order';
import { OrderService } from './order.service';
@Controller('orders')
export class OrderController {
constructor(private readonly orderService: OrderService,) {}
@Get('health_check')
public async status() {
return await "It's working order-service";
}
@Post('create')
public async create(@Body() requestCreate: RequestCreate) : Promise<ResponseOrder> {
const orderDto = new OrderDto();
orderDto.productId = requestCreate.productId;
orderDto.productName = requestCreate.productName;
orderDto.qty = requestCreate.qty;
orderDto.unitPrice = requestCreate.unitPrice;
orderDto.totalPrice = (Number(requestCreate.qty) * Number(requestCreate.unitPrice));
orderDto.userId = requestCreate.userId;
return await this.orderService.create(orderDto);
}
@Get(':orderId')
public async getOrder(@Param('orderId') orderId: string): Promise<ResponseOrder> {
return await this.orderService.getOrder(orderId);
}
@Get(':userId/getOrder')
public async getOrdersByUserId(@Param('userId') userId: string): Promise<ResponseOrder[]> {
return await this.orderService.getOrdersByUserId(userId);
}
@Patch(':orderId')
public async updateOrder(
@Param('orderId') orderId: string,
@Body() requestUpdate: RequestUpdate
): Promise<ResponseOrder> {
const orderDto = new OrderDto();
orderDto.orderId = orderId;
orderDto.qty = requestUpdate.qty;
return await this.orderService.updateOrder(orderDto);
}
@Post(':orderId/cancel')
public async cancelOrder(@Param('orderId') orderId: string): Promise<ResponseOrder> {
return await this.orderService.cancelOrder(orderId);
}
@Post(':orderId/reorder')
public async reOrder(@Param('orderId') orderId: string) {
return await this.orderService.reOrder(orderId);
}
}
우선 컨트롤러에 대한 부분을 구현하였고, 이를 바탕으로 vo폴더, dto폴더, entity폴더를 만들어 request, response관련 객체, dto객체, entity객체들을 만들어 주고 mysql, 검증 관련 라이브러리들도 추가해주도록 하겠습니다.
npm i class-validator class-transformer
npm install --save @nestjs/typeorm typeorm mysql
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { OrderModule } from './order/order.module';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'password',
database: 'orders',
autoLoadEntities: true,
synchronize: true,
}),
OrderModule
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { OrderController } from './order.controller';
import { OrderService } from './order.service';
@Module({
imports: [TypeOrmModule.forFeature([OrderEntity])],
controllers: [OrderController],
providers: [OrderService]
})
export class OrderModule {}
import { IsNotEmpty, IsNumber, IsString } from "class-validator";
export class RequestCreate {
@IsString()
@IsNotEmpty()
readonly productId: string;
@IsString()
@IsNotEmpty()
readonly productName: string;
@IsNumber()
@IsNotEmpty()
readonly stock: number;
@IsNumber()
@IsNotEmpty()
readonly unitPrice: number;
@IsNumber()
@IsNotEmpty()
readonly totalPrice: number;
@IsString()
@IsNotEmpty()
readonly userId: string;
}
import { IsNotEmpty, IsNumber } from "class-validator";
export class RequestUpdate {
@IsNumber()
@IsNotEmpty()
readonly stock: number;
@IsNumber()
@IsNotEmpty()
readonly totalPrice: number;
}
import { IsNotEmpty, IsNumber, IsString } from "class-validator";
export class ResponseOrder {
@IsString()
orderId: string;
@IsString()
productId: string;
@IsString()
productName: string;
@IsNumber()
stock: number;
@IsNumber()
unitPrice: number;
@IsNumber()
totalPrice: number;
@IsString()
userId: string;
@IsBoolean()
status: string;
}
import { IsNumber, IsString } from "class-validator";
export class OrderDto {
@IsString()
orderId: string;
@IsString()
productId: string;
@IsString()
productName: string;
@IsNumber()
stock: number;
@IsNumber()
unitPrice: number;
@IsNumber()
totalPrice: number;
@IsString()
userId: string;
@IsBoolean()
status: string;
}
import { Column, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class OrderEntity {
@PrimaryGeneratedColumn()
id: number;
@Column()
orderId: string;
@Column()
productId: string;
@Column()
productName: string;
@Column()
stock: number;
@Column()
unitPrice: number;
@Column()
totalPrice: number;
@Column()
userId: string;
@Column()
status: string;
}
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { OrderDto } from 'src/dto/order.dto';
import { OrderEntity } from 'src/entity/order.entity';
import { ResponseOrder } from 'src/vo/response.order';
import { Repository } from 'typeorm';
import { v4 as uuid } from 'uuid';
@Injectable()
export class OrderService {
constructor(@InjectRepository(OrderEntity) private orderRepository: Repository<OrderEntity>) {}
public async create(orderDto: OrderDto): Promise<ResponseOrder> {
try {
const orderEntity = new OrderEntity();
orderEntity.orderId = uuid();
orderEntity.productId = orderDto.productId;
orderEntity.productName = orderDto.productName;
orderEntity.qty = orderDto.qty;
orderEntity.unitPrice = orderDto.unitPrice;
orderEntity.totalPrice = orderDto.totalPrice;
orderEntity.userId = orderDto.userId;
orderEntity.status = 'CREATE_ORDER';
await this.orderRepository.save(orderEntity);
const responseOrder = new ResponseOrder();
responseOrder.orderId = orderEntity.orderId;
responseOrder.productId = orderEntity.productId;
responseOrder.productName = orderEntity.productName;
responseOrder.qty = orderEntity.qty;
responseOrder.unitPrice = orderEntity.unitPrice;
responseOrder.totalPrice = orderEntity.totalPrice;
responseOrder.userId = orderEntity.userId;
responseOrder.status = orderEntity.status;
return responseOrder;
} catch(err) {
throw new HttpException(err, HttpStatus.BAD_REQUEST);
}
}
public async getOrder(orderId: string): Promise<ResponseOrder> {
try {
return await this.orderRepository.findOne({ where: { orderId: orderId }});
} catch(err) {
throw new HttpException(err, HttpStatus.BAD_REQUEST);
}
}
public async getOrdersByUserId(userId: string): Promise<ResponseOrder[]> {
try {
return await this.orderRepository.find({ where: { userId: userId }});
} catch(err) {
throw new HttpException(err, HttpStatus.BAD_REQUEST);
}
}
public async updateOrder(orderDto: OrderDto): Promise<ResponseOrder> {
try {
const orderEntity = await this.orderRepository.findOne({ where: { orderId: orderDto.orderId }});
const responseOrder = new ResponseOrder();
orderEntity.qty = orderDto.qty;
orderEntity.totalPrice = (Number(orderDto.qty) * Number(orderEntity.unitPrice));
await this.orderRepository.save(orderEntity);
responseOrder.orderId = orderEntity.orderId;
responseOrder.productName = orderEntity.productName;
responseOrder.qty = orderEntity.qty;
responseOrder.unitPrice = orderEntity.unitPrice;
responseOrder.totalPrice = orderEntity.totalPrice;
return responseOrder;
} catch(err) {
throw new HttpException(err, HttpStatus.BAD_REQUEST);
}
}
public async cancelOrder(orderId: string): Promise<ResponseOrder> {
try {
const orderEntity = await this.orderRepository.findOne({ where: { orderId: orderId }});
const responseOrder = new ResponseOrder();
orderEntity.status = 'CANCEL_ORDER';
await this.orderRepository.save(orderEntity);
responseOrder.orderId = orderEntity.orderId;
responseOrder.status = orderEntity.status;
return responseOrder;
} catch(err) {
throw new HttpException(err, HttpStatus.BAD_REQUEST);
}
}
public async reOrder(orderId: string): Promise<ResponseOrder> {
try {
const orderEntity = await this.orderRepository.findOne({ where: { orderId: orderId }});
const responseOrder = new ResponseOrder();
orderEntity.status = 'RE_ORDER';
await this.orderRepository.save(orderEntity);
responseOrder.orderId = orderEntity.orderId;
responseOrder.status = orderEntity.status;
return responseOrder;
} catch(err) {
throw new HttpException(err, HttpStatus.BAD_REQUEST);
}
}
}
이렇게 order-service의 crud를 위한 모듈들을 구현했고 결과를 확인해보겠습니다.
order-serivce에 대한 결과값들이 잘 나옴을 확인할 수 있습니다. 이제 catalog-service와 message기반으로의 연동이 필요합니다. 고려해볼 사항은 다음과 같습니다.
1) order-service에서는 order를 생성 혹은 재주문할 경우 qty(주문할 제품의 갯수)라는 값이 필요합니다. 즉, order를 생성할 경우 catalog의 stock(제품 재고수)가 qty만큼 차감이 되어야겠죠.
2) order-service에서 order를 생성할 경우 qty가 catalog의 stock의 값보다 크다면 주문할 제품의 양이 없다는 것이므로 이에 대한 오류메시지가 필요합니다.
3) order-service에서 order를 취소할 경우 order가 없는 것으로 간주되므로 catalog의 stock수는 qty만큼 증감되어야 합니다.
우선 고려해볼 사항은 이 정도인 것 같습니다. 1, 2, 3번의 경우들은 전부 catalog-service와 order-service의 데이터 동기화에 대한 문제가 존재하는데요, 이 데이터 동기화에 대한 문제를 rabbitMQ의 메시지를 도입하여 해결해보도록 하겠습니다.