Skip to content

Conversation

wlgns2223
Copy link
Collaborator

@wlgns2223 wlgns2223 commented Aug 21, 2025

  • 데이터베이스 lock 학습 후 lock을 적용했습니다. ( src/product/product.repository.ts )
    • 트랜잭션이 끝날때까지 다른 트랜잭션이 레코드를 조회 할 수 없는 pessimistic lock을 사용했습니다.
  • 주문 정보를 생성 ( src/order/order.repository.ts )
  • 트랜잭션 데코레이터 모듈을 직접 구현한 인터셉터에서 nestjs-cls 구현체로 바꾸었습니다.
    • nestjs-cls는 트랜잭션 전파레벨을 설정 할 수 있어서 모듈을 바꾸었습니다.
  • 테스트 코드 작성은 아직 주문 생성이 미완성이라 마지막 단계인 주문 상세로직 작성 후 주문 생성 테스트 코드를 작성하겠습니다.

@wlgns2223 wlgns2223 self-assigned this Aug 21, 2025
@wlgns2223 wlgns2223 requested a review from f-lab-namu August 21, 2025 16:50
where: {
id: In(productIds),
},
lock: {
mode: 'pessimistic_write',

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분에 lock 은 왜 적용한거에요? 어떤 부분에 문제가 있었고, 어떻게 해결되는지 궁금합니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

적용한 이유

주문생성 중 상품의 재고감소 로직입니다. 동시성이 높은 환경에서 한 트랜잭션이 실행중일때 다른 트랜잭션이 동시에 재고를 감소하여 음수 재고가 되는 것을 막고자 적용을 했었습니다.

예상했던 문제와 해결하고자 했던 문제

위와 같은 의도로 동시성이 높은 환경에서 lock이 없다면, 재고가 음수가 되는 문제가 있을거라고 생각했고 pessimistic_lockselect... for update 쿼리를 실행함으로서 트랜잭션 시작 후 읽어드린 레코드를 커밋하기 전까지 점유하고 있기 때문에 다른 트랜잭션이 이를 동시에 읽는 것을 막아 재고가 음수가되는 것을 해결 할 수 있다고 생각했습니다.

현실

하지만,실제로 lock이 없으면 재고가 음수가 되는지 테스트를 했었고 사실 단 한번도 재고가 음수가 된적이 없었습니다. 왜 그런지 분석을 해보았습니다.

테스트 환경

테스트 환경은 postman에서 performance 테스트 기능으로 100명의 유저로 병렬로 요청을 보냈고, nodejs는 단일 쓰레드 비동기 처리를 해서 아무리 많은 요청이 와도 한번에 하나씩 처리하는 특징이 있기때문에 동시성이 높은 환경을 위해 pm2로 인스턴스를 8대로 실험을 했습니다.

테스트 조건

  • 데이터베이스에 재고는 2개 있습니다.
  • 모든 동시 요청이 같은 요청 파라미터를 가지고 보냅니다. 예를 들면 모두 상품을 1개씩 주문하여 재고를 1씩 감소시키려고 시도합니다.

첫번째 코드

코드에서 재고 감소 로직을 작성했습니다.

private async decreaseStocks(
    orderItems: OrderItemsInput[],
    products: PersistedProductEntity[],
  ): Promise<PersistedProductEntity[]> {
    const productsMap = this.generateProductMap(products);
    const productsWithDecreasedStocks = orderItems.map((oi) => {
      const product = productsMap[oi.productId];
      product.stocks -= oi.quantity;
      return product;
    });
    return await this.productRepository.save(productsWithDecreasedStocks);
  }

위는 초기 버전의 재고 감소 로직입니다. 최신 버전의 재고 감소 로직을 작성해놓고 적용을 안시킨 채로 초기버전 코드를 그대로 썼는데 위 코드역시 재고가 0으로 줄지 않았습니다. 대신에 여러 트랜잭션이 같은 재고를 읽은 후 똑같은 재고 감소 로직을 적용합니다. 그래서
트랜잭션 T1이 초기에 재고를 2로 읽고, 다른 트랜잭션 T2이 똑같이 재고를 2로 읽은 후 T1이 재고를 1로 감소, T2가 재고를 1로 감소하여 T1,T2를 적용합니다. 둘다 재고 감소 로직을 덮어쓰는 문제가 있습니다.

두번째 수정된 코드

async decreaseStocks(orderItems: OrderItemsInput[]) {
    const updateQuries = orderItems.map((oi) =>
      this.txHost.tx
        .getRepository(ProductEntity)
        .createQueryBuilder()
        .update()
        .set({
          stocks: () => `stocks - ${oi.quantity}`,
        })
        .where(`id = :productId`, { productId: oi.productId })
        .andWhere(`stocks >= :quantity`, { quantity: oi.quantity })
        .execute(),
    );

재고 감소와 업데이트를 데이터베이스에서 원자적으로 처리합니다. .andWhere(stocks >= :quantity, { quantity: oi.quantity }) 조건과 업데이트 연산때문에 재고가 음수가 되는 일은 없었습니다. 왜냐하면 Mysql에서 업데이트시에는 Exclusive lock이 자동으로 걸려 동시 업데이트를 막습니다. 재고감소,재고 조건 검사등이 업데이트가 원자적으로 일어나기 때문에 일관성이 깨지지는 않습니다.

배운점

그럼에도 불구하고 몇가지 문제가 있어서 pessimistic_lock을 적용해야 된다고 생각합니다. pessimistic_lock없이도 업데이트 연산 자체에 Exclusive lockwhere 절의 조건 검사 및 재고 감소가 원자적으로 일어나기때문에 재고가 음수가 되는 일관성 문제는 생기지 않으나

동시성이 높은 환경에서 재고가 충분한 경우 재고 읽기 -> 재고 감소가 원자적으로 일어나지 않아, 같은 재고 감소로직이 여러번 일어나는 문제가 있을 수 있습니다. (첫번째 코드에서는 확인 가능했고, 두번째 수정한 코드에서는 확인은 아직 못해봤습니다. )

따라서 pessimistic_lock을 적용함으로써 다른 트랜잭션에서 동시에 읽어 동시에 같은 작업을 레코드에 적용하는 문제를 해결 할 수 있습니다.

@wlgns2223 wlgns2223 requested a review from f-lab-namu August 24, 2025 10:58
Copy link

@f-lab-namu f-lab-namu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants