2주 챌린지 - 4. 잘못된 접근 방식

yy·2023년 11월 23일

개발일지

목록 보기
44/122

어제부터 부하테스트를 하고난 후 테이블의 값들이 내가 원하는대로 나와서 트랜잭션의 데드락이 해결이 된 줄 알았다. 터미널에 계속해서 올라오는 오류 메시지들이 사실은 로직이 수행되면서 에러가 발생했고. 에러 미들웨어로 빠져서 서버가 안꺼진건줄 알았다. 그래서 결과만 맞으면 되니까 됐다 됐어. 라고 그냥 눈가리고 아웅인 상태였다. 그런데 생각해보니까 그러면 안되는거였다.



1. 예매api 테스트.
duration: 10
arrivalRate: 5
count: 1

config:
  environments:
    local:
      target: 'http://localhost:3334'
      phases:
        - duration: 10
          arrivalRate: 5
          name: Warm up
      defaults:
        User-Agent: 'Artillery'
      payload:
        - path: 'usersToken.csv'
          fields:
            - 'userToken'
scenarios:
  - name: '예매'
    flow:
      - loop:
          - post:
              url: '/api/reservation/3'
              headers:
                authorization: '{{userToken}}'
        count: 1```


터미널에서 transaction 에러도 발생 X. 테이블 일관성 유지 확인.


2. 예매api 테스트.
duration: 10
arrivalRate: 5
count: 10

터미널에서 transaction 에러도 발생. 테이블 일관성이 유지 확인.
테이블의 일관성이 유지되는건 catch에서 if (transaction) {await prisma.$executeRaw'ROLLBACK';} 이 코드때문에 일관성이 유지된다.

=> 그럼 트랜잭션 에러가 나지않도록 격리수준을 낮춰보자. Serializable -> RepeatableRead
그렇게 되면 deadlock 에러는 나오지 않지만 일관성이 유지가 되지 않는다.


100장을 예매하랬더니 231장을 예매해버렸다. 이건 로직에서 수정을 해야하는 부분이다. 어제같은경우 이런 문제를 아래와 같은 코드( 1)예매 router안에 있는 모든 코드를 transaction안으로 넣는것과 2) if (Math.floor(user.credit / show.price) > 0)이라는 조건문을 넣는것 )와 Serializable를 적용해서 해결한줄 알았다. 하지만 내 오산이었다.

/** 공연 예매 **/
router.post('/reservation/:showId', authMiddleware, async (req, res, next) => {
  let transaction;
  try {
    const { showId } = req.params;
    const { userId } = req.user;

    transaction = await prisma.$transaction(
      async (tx) => {
        const show = await tx.shows.findFirst({
          where: { showId: +showId },
        });

        if (!show) {
          console.log(`${userId} : 공연없음`);
          return res.status(400).json({ message: '찾는 공연이 없습니다.' });
        }

        const user = await tx.users.findFirst({
          where: { userId: +userId },
        });

        if (Math.floor(user.credit / show.price) > 0) {
          if (show.quantity > 0) {
            await tx.shows.update({
              where: { showId: +showId },
              data: { quantity: --show.quantity },
            });
          } else {
            console.log(`${userId} : 예매수량부족`);
            throw new Error('예매 수량이 부족합니다.');
          }

          if (user.credit >= show.price) {
            await tx.users.update({
              where: { userId: +userId },
              data: { credit: user.credit - show.price },
            });
            await tx.reservation.create({
              data: { UserId: user.userId, ShowId: show.showId },
            });
          } else {
            console.log(`${userId} : credit부족`);
            throw new Error('보유한 credit이 부족합니다.');
          }
        }
      },
      {
        isolationLevel: Prisma.TransactionIsolationLevel.RepeatableRead,
      },
    );
    return res.status(200).json({ message: '좌석 예매가 완료되었습니다.' });
  } catch (error) {
    console.log(`catch로 빠진 ${error}`);
    next(error);
    if (transaction) {
      await prisma.$executeRaw`ROLLBACK`;
    }
  }
});

계속해서 위의 코드를 수정해보겠다.

1) 조건문들의 스코프 조절 => 원자성 실패

(reservation의 user별 갯수, shows의 quantity, user의 credit이 일관성이 안맞음)
: 사용자가 가지고 있는 돈보다 많이 예매하는 것이 문제로, Math.floor(user.credit / show.price) > 0는 사용자가 살 수 있는 표의 갯수로 0개를 초과하는 경우에만 아래 로직을 수행하도록 했다. 하지만,,,동시성 제어의 문제여서 이게 실제 문제는 아니었던것 같다.

if (Math.floor(user.credit / show.price) > 0) {
          if (show.quantity > 0) {
            await tx.shows.update({
              where: { showId: +showId },
              data: { quantity: --show.quantity },
            });
            if (user.credit >= show.price) {
              await tx.users.update({
                where: { userId: +userId },
                data: { credit: user.credit - show.price },
              });
              await tx.reservation.create({
                data: { UserId: user.userId, ShowId: show.showId },
              });
            } else {
              console.log(`${userId} : credit부족`);
              throw new Error('보유한 credit이 부족합니다.');
            }
          } else {
            console.log(`${userId} : 예매수량부족`);
            throw new Error('예매 수량이 부족합니다.');
          }
        }

1-2) 스코프를 다시 조절하고 조건문 수정 => 원자성 실패

: 혹시 조건을 뒤에서 나온 조건문 하나하나 더 추가하면 안될까 하는 마음으로 다시 넣어봤다. 역시나 실패

//문제: user가 가지고 있는 돈보다 더 많이 예매한다.
        if (
          show.quantity > 0 &&
          Math.floor(user.credit / show.price) <= show.quantity
        ) {
          await tx.shows.update({
            where: { showId: +showId },
            data: { quantity: --show.quantity },
          });
        } else {
          console.log(`${userId} : 예매수량부족`);
          throw new Error('예매 수량이 부족합니다.');
        }

        if (
          user.credit >= show.price &&
          Math.floor(user.credit / show.price) <= show.quantity
        ) {
          await tx.users.update({
            where: { userId: +userId },
            data: { credit: user.credit - show.price },
          });

          await tx.reservation.create({
            data: { UserId: user.userId, ShowId: show.showId },
          });
        } else {
          console.log(`${userId} : credit부족`);
          throw new Error('보유한 credit이 부족합니다.');
        }
      }

아니그럼, 대체 동시접속자 몇명부터 이런 현상이 발생하는걸까? 1~2명까지의 동시접속은 괜찮았다. 다만 3명부터 일관성에 문제가 생겼다. user가 총 99개의 예약을 했는데 표가 95표밖에 안나갔다.


1-3) 조건문의 data 내용 수정 => 원자성 성공!

update문의 data를 원래는 data: { quantity: --show.quantity }, 이렇게 써줬었는데 굳이 위에서 찾은 show의 quantity를 바꿔줄 이유가 있을까? 해서 decrement로 변경해줬더니 우선 동시 접속 3명에서는 트랜잭션오류도 안났고, 일관성이 유지가 되었다.

if (show.quantity > 0) {
          await tx.shows.update({
            where: { showId: +showId },
            data: { quantity: { decrement: 1 } },
          });
        } else {
          console.log(`${userId} : 예매수량부족`);
          throw new Error('예매 수량이 부족합니다.');
        }

위에서 트랜잭션 오류가 떴던 조건으로 다시 실행했다. 성공!
duration: 10
arrivalRate: 5
count: 10

profile
시간이 걸릴 뿐 내가 못할 건 없다.

0개의 댓글