E-commerce Application(Nest js Microservice) - 9. order-service와 catalog-service 통신(2)

#1 시나리오

이전 포스트에서는 order-service -> catalog-service의 데이터 동기화를 다뤄 봤습니다. 우선 데이터 동기화 부분에선 잘 이루어지는 모습이 보였지만 catalog-service에서의 에러 케이스와 주문 취소의 경우를 다루지 못했습니다. 이번 포스트에선 에러 케이스 이후 status의 ERROR_ORDER로의 상태 변화, 주문 취소 이후 status의 CANCEL_ORDER로의 상태 변화 이 두 가지를 다뤄 보도록 하겠습니다.

이런 식으로 에러 케이스가 발생할 경우 ERROR_ORDER라는 메시지를 큐로 보내고 status의 값 또한 ERROR_ORDER로 변화시킵니다. order-service는 ERROR_ORDER라는 메시지를 기다리고 있으니 이 메시지를 받게 되면 사용자에게 error의 메시지를 반환시켜주도록 하겠습니다.

그리고 주문 취소의 경우는 이전에 작성해둔 흐름도를 따라가도록 하고 재주문의 경우 CREATE_ORDER와 비슷한 로직이니 그대로 따라가도록 하겠습니다.

#2 ERROR_ORDER 구현

우선 저는 이번 메시지 패턴을 구현하면서 rabbitmq의 queue를 2개를 썼습니다. 1개로 양방향 통신을 해보려고 여러 시도를 해봤지만 module import에러가 계속해서 발생해서 차라리 2개를 order -> catalog, catalog -> order 따로 사용해봤습니다.

그러면 catalog -> order로 향하는 메시지 큐를 연동하도록 하겠습니다. 메시지 큐는 이전과 같은 방식으로 만들었고, catalog_queue라는 이름으로 만들었습니다. 메시지 큐 연동은 이전 포스트와는 반대 방향으로 코드를 작성했습니다.

  • catalog-service - catalog.module.ts
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CatalogEntity } from 'src/entity/catalog.entity';
import { CatalogController } from './catalog.controller';
import { CatalogService } from './catalog.service';

@Module({
  imports: [
    TypeOrmModule.forFeature([CatalogEntity]),
    ClientsModule.register([{
      name: 'catalog-service',
      transport: Transport.RMQ,
      options: {
        urls: ['CATALOG_QUEUE_URL'],
        queue: 'catalog_queue',
        queueOptions: {
          durable: false,
        },
      },
    }]),
  ],
  controllers: [CatalogController],
  providers: [CatalogService]
})
export class CatalogModule {}
  • order-service - app.module.ts
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
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: '10a10a',
      database: 'orders',
      autoLoadEntities: true,
      synchronize: true,
    }),
    ClientsModule.register([{
      name: 'catalog-service',
      transport: Transport.RMQ,
      options: {
        urls: ['CATALOG_QUEUE_URL'],
        queue: 'catalog_queue',
        queueOptions: {
          durable: false,
        },
      },
    }]),
    OrderModule
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
  • order-service - main.ts
import { NestFactory } from '@nestjs/core';
import { Transport } from '@nestjs/microservices';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const microservice = app.connectMicroservice({
    transport: Transport.RMQ,
    options: {
      urls: ['CATALOG_QUEUE_URL'],
      queue: 'catalog_queue',
      queueOptions: {
        durable: false,
      },
    },
  });

  await app.startAllMicroservices();
  await app.listen(7200);
}
bootstrap();

여기까지 연동을 완료했고, 이제 에러 케이스인 1-4-1) ~ 1-4-2)를 구현하도록 하겠습니다.

1-4-1) 만약 qty > stock 즉, 주문량이 재고량보다 많다면 에러메시지를 반환합니다.

  • catalog.service.ts
import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { InjectRepository } from '@nestjs/typeorm';
import { CatalogDto } from 'src/dto/catalog.dto';
import { CatalogEntity } from 'src/entity/catalog.entity';
import { ResponseCatalog } from 'src/vo/response.catalog';
import { Repository } from 'typeorm';

@Injectable()
export class CatalogService {
    constructor(
        @InjectRepository(CatalogEntity) private catalogRepository: Repository<CatalogEntity>,
        @Inject('catalog-service') private readonly client: ClientProxy    
    ) {}

    ...

    public async createOrderAndDecreaseStock(data: any): Promise<any> {
        const catalogEntity = await this.catalogRepository.findOne({ where: { productId: data.productId }});

        catalogEntity.stock -= data.qty;

        if(catalogEntity.stock < 0) {
            data.status = 'ERROR_ORDER';

            this.client.emit('ERROR_ORDER', data);
            
            catalogEntity.stock += data.qty;

            await this.catalogRepository.save(catalogEntity);
            
            return data.status + " : Product stock is less than 0";
        } else {
            await this.catalogRepository.save(catalogEntity);

            return catalogEntity;
        }
    }  
}

1-4-2) 에러메시지(ERROR_ORDER)를 order-service에서 받는다면 status의 값을 ERROR_ORDER로 변경합니다.

  • order.controller.ts
import { Body, Controller, Get, Param, Patch, Post } from '@nestjs/common';
import { EventPattern } from '@nestjs/microservices';
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,) {}

   ...

    @EventPattern('ERROR_ORDER')
    public async errorOrder(data: any) {
        return await this.orderService.errorOrder(data);
    }
}
  • order.service.ts
import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
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 {
    ...

    public async errorOrder(data: any): Promise<any> {
        const orderEntity = await this.orderRepository.findOne({ where: { orderId: data.orderId }});

        orderEntity.status = "ERROR_ORDER";

        await this.orderRepository.save(orderEntity);

        return "Product stock is less than 0 so please modifying qty, reorder this product";
    }
}

이런 흐름대로 orderEntity의 값을 ERROR_ORDER로 바꾸어 주고, 다시 수정하게끔 구현을 마쳤습니다. 결과를 한번 보도록 하겠습니다.




product-001의 재고량은 100입니다. 에러 케이스를 테스트해보기 위해 qty를 110으로 맞추고 주문을 진행했습니다. 예상되는 결과값은 재고량의 변동이 없고, 주문상태가 ERROR_ORDER이겠죠. 데이터베이스를 살펴 본 결과 재고량 변동 X, 주문 상태는ERROR_ORDER임을 알 수 있습니다.

  • 에러케이스인 경우 에러메시지를 postman에서 반환받아야 하는데 이 부분은 다음 포스트에서 고치도록 하겠습니다.

#3 주문 취소 구현

주문 취소를 구현하도록 하겠습니다. 다시 시나리오를 살펴보겠습니다.

2-1) order.status의 값이 CANCEL로 변경됩니다.
2-2) 메시지 큐로 order.status의 값이 CANCEL이 됐음을 알립니다.
2-3) catalog-service에서는 해당 메시지를 받고 stock의 값을 Rollback시킵니다.

일전에 OrderController에서 REST로 cancel메서드를 구현했고, 비즈니스 로직을 구현하는 OrderService부터 이 방식대로 구현을 진행하겠습니다.

2-1) order.status의 값이 CANCEL로 변경됩니다.

  • order.service.ts
import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
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>,
        @Inject('order-service') private readonly client: ClientProxy    
    ) {}

    ...

    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);
            
            this.client.emit('CANCEL_ORDER', orderEntity);
            
            responseOrder.orderId = orderEntity.orderId;
            responseOrder.status = orderEntity.status;

            return responseOrder;
        } catch(err) {
            throw new HttpException(err, HttpStatus.BAD_REQUEST);
        }
    }

    ...

2-2) 메시지 큐로 CANCEL_ORDER 메시지를 알립니다.

  • catalog.controller.ts
import { Body, Controller, Get, Inject, Logger, Param, Patch, Post } from '@nestjs/common';
import { ClientProxy, EventPattern } from '@nestjs/microservices';
import { CatalogDto } from 'src/dto/catalog.dto';
import { RequestCreate } from 'src/vo/request.create';
import { RequestUpdate } from 'src/vo/request.update';
import { ResponseCatalog } from 'src/vo/response.catalog';
import { CatalogService } from './catalog.service';

@Controller('catalogs')
export class CatalogController {
    ...

    @EventPattern('CANCEL_ORDER')
    public async cancelOrderAndRollbackStock(data: any): Promise<any> {
        return await this.catalogService.cancelOrderAndRollbackStock(data);
    }
}

2-3) catalog-service에서는 해당 메시지를 받고 stock의 값을 Rollback시킵니다.

  • catalog.service.ts
import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { InjectRepository } from '@nestjs/typeorm';
import { CatalogDto } from 'src/dto/catalog.dto';
import { CatalogEntity } from 'src/entity/catalog.entity';
import { ResponseCatalog } from 'src/vo/response.catalog';
import { Repository } from 'typeorm';

@Injectable()
export class CatalogService {
    constructor(
        @InjectRepository(CatalogEntity) private catalogRepository: Repository<CatalogEntity>,
        @Inject('catalog-service') private readonly client: ClientProxy    
    ) {}

    ...

    public async cancelOrderAndRollbackStock(data: any): Promise<any> {
        const catalogEntity = await this.catalogRepository.findOne({ where: { productId: data.productId }});

        catalogEntity.stock += data.qty;

        await this.catalogRepository.save(catalogEntity);

        return "Successfully Rollback stock";
    }
}

흐름도대로 잘 구현이 되었는지 결과를 한번 살펴보겠습니다.




product-002를 주문하였고, 재고량 파악 결과 stock의 갯수가 10개가 차감되었음을 볼 수 있습니다. 그리고 해당 주문 번호로 주문을 취소하였고, 다시 stock의 갯수가 10개가 증감이 되었음을 볼 수 있습니다.

#3 RE_ORDER 구현

RE_ORDER는 CREATE_ORDER랑 같은 흐름이므로 이를 바탕으로 구현을 하도록 하겠습니다.

  • order.service.ts
import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
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 {
    ...

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

            this.client.emit("RE_ORDER", orderEntity);
            
            await this.orderRepository.save(orderEntity);

            responseOrder.orderId = orderEntity.orderId;
            responseOrder.status = orderEntity.status;

            return responseOrder;
        } catch(err) {
            throw new HttpException(err, HttpStatus.BAD_REQUEST);
        }
    }

    ...
  • catalog.controller.ts
import { Body, Controller, Get, Inject, Logger, Param, Patch, Post } from '@nestjs/common';
import { ClientProxy, EventPattern } from '@nestjs/microservices';
import { CatalogDto } from 'src/dto/catalog.dto';
import { RequestCreate } from 'src/vo/request.create';
import { RequestUpdate } from 'src/vo/request.update';
import { ResponseCatalog } from 'src/vo/response.catalog';
import { CatalogService } from './catalog.service';

@Controller('catalogs')
export class CatalogController {
    ...

    @EventPattern('RE_ORDER')
    public async reOrderAndDecreaseStock(data: any): Promise<any> {
        return await this.catalogService.reOrderAndDecreaseStock(data);
    }
}
  • catalog.service.ts
import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { InjectRepository } from '@nestjs/typeorm';
import { CatalogDto } from 'src/dto/catalog.dto';
import { CatalogEntity } from 'src/entity/catalog.entity';
import { ResponseCatalog } from 'src/vo/response.catalog';
import { Repository } from 'typeorm';

@Injectable()
export class CatalogService {
    ...

    public async reOrderAndDecreaseStock(data: any): Promise<any> {
        const catalogEntity = await this.catalogRepository.findOne({ where: { productId: data.productId }});

        catalogEntity.stock -= data.qty;

        await this.catalogRepository.save(catalogEntity);

        return "Complete Reorder";
    }
}




CANCEL_ORDER상태인 주문번호 69adadf6-c099-4d0b-a448-6c4a0aabbd97를 reorder를 진행하였고, postman에서는 결과값이 잘 반환되었습니다. 그리고 데이터베이스도 확인한 결과 RE_ORDER로 변경되었고, stock도 10이 차감이 된 상태입니다.

이렇게 총 3가지 케이스에 대한 메시지 기반 데이터 동기화를 구현해보았습니다. 우선 오늘 포스트를 작성하면서 수정하거나 구현해야할 상황은 ERROR_ORDER 대한 재주문 케이스입니다.

다음 포스트에서는 재주문 케이스에 관해 다뤄보도록 하겠습니다.

좋은 웹페이지 즐겨찾기