nest.js 로 결제와 예매, 결제 취소와 예약 취소가 동시에 이뤄져야 하기 떼문에
트랜잭션 처리를 공부했다. 여러 방법 중 제일 간편해 보이는 queryRunner 를 사용해보기로 했다..
async makeReservation(
user: User,
concert_name: string,
schedule_id: number,
seat_id: number,
payment_method_id: number,
) {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const findConcert = await this.concertRepository.findOne({
where: { name: concert_name },
});
if (!findConcert) {
throw new BadRequestException(
'해당 이름의 콘서트가 존재 하지 않습니다.',
);
}
const findSchedule = await this.scheduleRepository.findOne({
where: { id: schedule_id },
});
if (!findSchedule) {
throw new BadRequestException('존재 하지 않는 스케쥴입니다.');
}
const findSeat = await this.seatRepository
.createQueryBuilder('seat')
.leftJoinAndSelect('seat.class', 'class')
.where('seat.id = :seat_id', { seat_id })
.select(['seat.state', 'class.id', 'class.grade', 'class.price'])
.getOne();
if (!findSeat) {
throw new BadRequestException({
message: '해당 좌석이 존재하지 않습니다.',
});
}
if (findSeat.state === '예매 불가') {
throw new ConflictException('이미 선택된 좌석입니다.');
}
//결제요청
const paymentRequest = {
concert_name: concert_name,
seat_id: seat_id,
amount: findSeat.class.price,
method_id: payment_method_id,
};
const paymentResult = await this.payment(paymentRequest);
if (!paymentResult.success) {
throw new ConflictException(
'결제에 실패하였습니다. 다시 시도해 주세요.',
);
}
findSeat.state = '예매 불가';
await this.seatRepository.update(seat_id, { state: findSeat.state });
//결제 내역 저장
const payment = this.paymentRepository.create({
cost: findSeat.class.price,
paymentMethod: { id: payment_method_id },
state: '결제 완료',
user: { id: user.id },
approve_number: paymentResult.approve_number,
approvedAt: paymentResult.approve_time,
});
const savedPayment = await this.paymentRepository.save(payment);
//예약 정보 저장
const reservation = this.reservationRepository.create({
seat: { id: seat_id },
concert: { id: findConcert.id },
payment: { id: savedPayment.id },
});
const savedReservation =
await this.reservationRepository.save(reservation);
await queryRunner.commitTransaction();
return {
status_code: 201,
message: '예약이 완료되었습니다.',
reservation_info: {
concert_name,
date: findSchedule.date,
seat_id: seat_id,
},
};
} catch (error) {
await queryRunner.rollbackTransaction();
console.error(error);
throw error;
} finally {
await queryRunner.release();
}
}
(길다..)
<과정>
1) Quaryrunner 인스턴스 생성
2) connect 를 통해 연결
3) startTransaction 으로 시작
4) 모든 쿼리 수행 후 commitTransaction
5) 에러 발생 시 catch 문에 걸리게 하여 rollbackTransaction 이 시행되도록함(사실 처음에 빼먹었었는데, 없어도 rollback 이 작동하긴 한다. 하지만 안전을 위해 있는게 좋을 듯)
6) 마지막에 release 를 통해 인스턴스 종료
사용 방법이 아주 간단했다. 내가 트랜잭션으로 묶고 싶은 범위 시작 시에 start해주고
끝날 시에 commit 해주면 되는 직관적인 문법이다.
단점으로는 트랜잭션이 필요한 메소드 마다 저렇게 인스턴스 생성 삭제, 과정을 거쳐야 한다는 것이다.
안그래도 코드가 길기 때문에 데코레이터와 인터셉트를 만들어야 겠다.