코드

user.service.ts

// imports 생략
// User 엔티티 와 dto 코드 생략

@Injectable()
export class UserService {
  constructor(
    private dataSource: DataSource,
    @InjectRepository(User) private userRepository: Repository<User>,
    private readonly configService: ConfigService,
    private readonly authService: AuthService,
  ) {}
  
  // 동시성 문제를 테스기하기 위해 추가
  delay(ms: number): Promise<void> {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }

  async signUp(signUpRequestDto: SignUpRequestDto): Promise<any> {
    const { password, email, name } = signUpRequestDto;
    const queryRunner = this.dataSource.createQueryRunner();

    await queryRunner.connect();
    await queryRunner.startTransaction();
    try {
      // lock 사용 -> 다른 트랜잭션이 같은 이메일의 사용자를 생성하는 것을 방지
      // pessimistic_write : 데이터를 처음 읽을 때 락을 걸고 트랜잭션이 끝날 때까지 락을 유지하는 방법 사용
      let user = await queryRunner.manager.findOne(User, 
        { where: { email }, lock: { mode: 'pessimistic_write' } });
      if (user) {
        throw new Error('Email already exists');
      }
      const saltRounds = this.configService.get<string>('SALT_ROUNDS');
      if (saltRounds === undefined) {
        throw new Error('SALT_ROUNDS is not defined in the configuration.');
      }
      const hashedPassword = await bcrypt.hash(password, parseInt(saltRounds));
      user = new User({ name, email, password: hashedPassword });

      await this.delay(5000); // 동시성 문제를 테스기하기 위해 추가
      
      const newUser = await queryRunner.manager.save(user);
      await queryRunner.commitTransaction();
      return new SignUpResponseDto({ ...newUser });
    } catch (error) {
      await queryRunner.rollbackTransaction();
      throw error;
    } finally {
      // 리소스 해제 필수
      await queryRunner.release();
    }
  }
}

 

 

Postman 을 이용하여 같은 유저 데이터를 전송한 결과

첫번째 요청

{
    "id": 1,
    "email": "test_lock@email.com",
    "name": "Tester"
}

 

두번째 요청

{
    "statusCode": 500,
    "message": "Internal server error"
}

- console 화면

+ Recent posts