포시코딩

[Nest.js][동시성 문제] Transaction 사용과 통 Lock 걸어버리기 본문

Node.js

[Nest.js][동시성 문제] Transaction 사용과 통 Lock 걸어버리기

포시 2023. 2. 17. 21:51
728x90

코드

Back-End: Nest.js - boards.service.ts

async joinGroup(boardId: number, userId: number) {
  const board = await this.getBoard(boardId);
  const boardJoinInfo = await this.joinRepository.find({
    where: { boardId },
  });

  if (board.joinLimit <= boardJoinInfo.length) {
    throw new ForbiddenException('자리 부족');
  }
  boardJoinInfo.forEach((join) => {
    if (join.userId === userId) {
      throw new ConflictException('이미 join 했습니다.');
    }
  });

  const queryRunner = this.dataSource.createQueryRunner();
  await queryRunner.connect();
  await queryRunner.startTransaction();
  // await queryRunner.startTransaction('SERIALIZABLE');

  try {
    // this.joinRepository.insert({ boardId, userId });
    await queryRunner.manager.getRepository(Join).insert({
      boardId, userId
    });
    const afterJoin = await this.joinRepository.count({
      where: { boardId },
    });
    if (board.joinLimit <= afterJoin) {
      throw new Error('동시성 문제 발생');
    }
    await queryRunner.commitTransaction();
  } catch (e) {
    await queryRunner.rollbackTransaction();
    throw new BadGatewayException(e.message);
  } finally {
    await queryRunner.release();
  }
}

 

Front-End: React - List.js

function test() {
  test_form(9, 0);
  test_form(9, 1);
  test_form(9, 2);
  test_form(9, 3);
  test_form(9, 4);
}

function test_form(boardId, userId) {
  axios
    .post(
      `http://localhost:8080/boards/${boardId}/join2`,
      {userId},
      { withCredentials: true },
    )
    .then((response) => {
      const statusCode = response.status;
      // console.log('status code: ' + statusCode);
      if (statusCode === 201) {
        console.log(`id: ${userId} 성공 - status code: ` + statusCode);
      } else {
        console.log('status code: ' + statusCode);
      }
    })
    .catch((e) => {
      // console.log('axios 통신실패');
      console.log(e.response.data.message);
    });
}

 

동시성 문제 구현

joinLimit이 2인 board에 대해 동시에 5개의 요청을 진행

 

원래라면 두 개만 들어간 다음 나머지는 transaction에 의해 502 에러를 내며 

롤백 됐어야 했는데 4개나 들어간 모습. 

 

2개나 초과되어 들어갔다.

 

다시 시도해보면 시도할 때마다 잘못 들어가는 개수도 달라지는데

어차피 의도했던바라 성공이라고 볼 수 있다.

 

이제 이걸 어떻게 해결하면 좋을지 찾아내는건데

高.. 쉽지 않다.

 

1차 피드백

위 내용 관련해서 튜터님과 상담을 진행했고

그 결과 아래와 같은 내용이 종합되었다.

 

1. 

현재 동시에 테스트를 진행한건 완전한 '동시'가 아니다.

물론 원하는 결과를 얻고 있긴 하지만

진짜 '동시'가 되려면 더 빡세게 해야 되는데

 

JavaScript는 싱글 쓰레드기 때문에

Python의 Gevent나 

Java에서 쓰레드를 나눠 진행하는 방법을 생각해보면 좋겠다.

 

2. 아니면 아예 중간에서 해당 요청들을 받는 

Python 또는 Java로 이루어진 서버를 구성한다던가 해서

다시 본래 백엔드 Nest.js 서버에 보내는 방법도 괜찮을듯

 

이거와 관련해서 SaaS 인프라 구성에 대해 찾아보기

 

3. 그게 아니면 스트레스 테스트 툴을 이용하는 것도 방법이 될 것이다.

 

4. 통으로 Lock을 거는 방법을 사용할 수도 있는데

let isLock = false;

function test() {
  if (isLock) {
    return;
  }
  isLock = true;
  // .. do something
  isLock = false;
}

이런 느낌으로 로직을 만드는 것도 어느정도 해결방법이 될 수 있을 것이다.

물론 완벽히 막지는 못한다는걸 알고 있어라!

 

1차 정리

boards.service.ts

@Injectable()
export class BoardsService {
  private isLock: boolean;

  constructor(
    // ...생략
  ) {
    this.isLock = false;
  }
async joinGroup(boardId: number, userId: number) {
  if (this.isLock) {
    throw new BadGatewayException('진행중인 상태가 있으므로 다시 시도해주시기 바랍니다.');
  }
  this.isLock = true;

  // ...생략
  try {
    // ...생략
    await queryRunner.commitTransaction();
  } catch (e) {
    await queryRunner.rollbackTransaction();
    throw new BadGatewayException(e.message);
  } finally {
    await queryRunner.release();
    this.isLock = false;
  }
}

 

마지막 피드백 내용의 통 Lock을 거는 방법으로 로직을 수정해봤다.

아까 넣었던 DB 데이터를 지우고 다시 똑같이 시도해봤는데

효과가 있다!

 

처음 들어간 userId = 0의 저장이 진행중일 때

isLock = true 상태가 되어 다른 userId들이 접근하지 못하고 502 에러를 리턴하는

BadGatewayException으로 인해 팅겨 나왔다.

 

당장은 이렇게 사용할 수도 있지만 사용자로 하여금 본인이 시도했는데

자꾸 이미 진행중인 상태가 있어서 다시 시도하라는 말이 나오면 짜증이 날 것이다.

더군다나 아직 빈 자리가 있으면 더더욱

 

그래도 이와 관련해서 스터디를 진행한 결과 TypeORM을 쓰면서

bull, queue와 같은 키워드 힌트를 얻었는데 한 번 찾아보며 관련해서도 구현해봐야겠다.

 

추가 작성

https://velog.io/@soyeon207/DB-Lock-총-정리-2-낙관적-락과-비관적-락-분산락-데드락

 

[DB] Lock 총 정리 - 2 (낙관적 락과 비관적 락, 분산락, 데드락)

낙관적락, 비관적락, 분산락, 데드락에 대해서 알아보자

velog.io

 

위에서 했던 통 Lock을 거는 행위를 존재하는 용어로 표현하자면 

비관적 락(Perssimistic Lock)이라고 하는듯 하다.

 

위 포스팅을 읽어보면 내가 원하는 결과를 얻기 위해서는 

낙관적 락(Optimistic Lock)을 해야하는것으로 보이는데

 

TypeORM 에서 사용하는 예제를 좀처럼 찾아보기 힘들었다.

좀 찾아본 결과 이정도..?

https://itchallenger.tistory.com/m/entry/TypeORM-%EC%8A%A4%ED%84%B0%EB%94%94-QueryBuilder-2%ED%8E%B8-%EA%B8%B0%ED%83%80%EB%AC%B8%EB%B2%95

 

TypeORM 스터디 : QueryBuilder 2편 - CRUD 심화

1편 보기 TypeORM 스터디 : QueryBuilder 1편 - CRUD 기본 TypeORM - Amazing ORM for TypeScript and JavaScript (ES7, ES6, ES5). Supports MySQL, PostgreSQL, MariaDB, SQLite, MS SQL Server, Oracle, WebSQL databases. Works in NodeJS, Browser, Ionic, Cord

itchallenger.tistory.com

 

일단 이 다음 포스팅에서 얘기하는 Bull Queue를 사용하는 방식에서 진전이 있어

해당 방법으로 어느정도 결과를 보고나서 낙관적 락, 비관적 락에 대해 알아보고 테스트해봐야 할 것 같다.

 

 

해당 포스팅은 아래 포스팅으로 이어진다.

https://4sii.tistory.com/423

 

[동시성 문제] (2) Nest.js의 Bull Queue - 작성중

개요 이전에 고민했던 동시성 문제에 대해 Queue 디자인 패턴을 활용해 해결할 수 있을 것 같다는 팀 의견이 모아졌다. 마침 Nest에서는 Node.js 기반 대기열 시스템 구현에 필요한 @nestjs/bull 패키지

4sii.tistory.com

728x90