์์ ์์ฝ์ ์ค๋ณต ์์ฝ ๋ฌธ์
์จ๋ผ์ธ ์์ฝ ์์คํ
์์๋ “๋๋ธ ๋ถํน(Double Booking)”์ด ์ธ์ ๋ ํฐ ๊ณ ๋ฏผ๊ฑฐ๋ฆฌ์
๋๋ค.
๊ฐ์ ์์๋ฅผ ๋์์ ์ฌ๋ฌ ์ฌ์ฉ์๊ฐ ์์ฝํ๋ ค๊ณ ์๋ํ ๋, ์ ๋๋ก ์ ์ดํ์ง ์์ผ๋ฉด ํ ์๋ฆฌ์๋ ๋ ๋ช
์ด์์ ์์ฝ์ด ์์ฑ๋ ์ ์์ต๋๋ค. ๋์์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ฉด ๋๋ธ ๋ถํน(Double Booking)๊ณผ ๊ฐ์ ์ฌ๊ฐํ ์ค๋ฅ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค. ์ด๋ ๋จ์ํ ์ฌ์ฉ์ ๋ถํธ์ ๋์ด ์๋น์ค ์ ๋ขฐ๋๋ฅผ ๋ฌด๋๋จ๋ฆฌ๋ ์น๋ช
์ ์ธ ๋ฌธ์ ๋ก ์ด์ด์ง๋๋ค.

ํ์ง๋ง ์ค๋ณต ์์ฝ ๋ฌธ์ ๋ ๋จ์ํ SQL ์ฟผ๋ฆฌ๋ง ์ ์ง ๋ค๊ณ ํด๊ฒฐ๋์ง ์์ต๋๋ค.
์๋ํ๋ฉด ์ฌ๋ฌ ์ฌ์ฉ์์ ์์ฒญ์ด ๊ฑฐ์ ๋์์ ๋ค์ด์ค๋ฉด, ๊ฐ ์์ฒญ์ด ๋
๋ฆฝ์ ์ผ๋ก DB๋ฅผ ์ฝ๊ณ ์ฐ๋ฉด์ ์ถฉ๋์ด ๋ฐ์ํ ์ ์๊ธฐ ๋๋ฌธ์
๋๋ค. ๋ฐ๋ผ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ํธ๋์ญ์
, ๋ฝ(Lock), ๊ฒฉ๋ฆฌ ์์ค(Isolation Level) ๋ฑ ๋ค์ํ ๋์์ฑ ์ ์ด ์ ๋ต์ ํจ๊ป ๊ณ ๋ฏผํด์ผ ํฉ๋๋ค.
์ด๋ฒ ๊ธ์์๋ ์์ฝ ์์คํ ์์ ์ค๋ณต ์์ฝ์ ๋ฐฉ์งํ๊ธฐ ์ํ ๋์์ฑ ์ ์ด ์ ๋ต์ ๋จ๊ณ๋ณ๋ก ์ดํด๋ณด๊ณ , ์ค์ ๊ตฌํ ์ฌ๋ก์ PostgreSQL์ ํ์ฉํ ์์ ํ ์์ฝ ์ฒ๋ฆฌ ๋ฐฉ๋ฒ๊น์ง ๋ค๋ฃจ๊ฒ ์ต๋๋ค.
๋ชฉ์ฐจ
- ์ค๋ณต ์์ฝ ๋ฌธ์ ์ ์ดํด
- ๋๋ธ ๋ถํน์ด ๋ฐ์ํ๋ ์๋๋ฆฌ์ค ์๊ฐ
- ๋จ์ ์ฟผ๋ฆฌ๋ก ์ฒ๋ฆฌํ ๋ ๋ฐ์ํ ์ ์๋ ๋์์ฑ ๋ฌธ์ ์์
- ๋์์ฑ ์ ์ด ๊ธฐ๋ฒ ์๊ฐ
- ํธ๋์ญ์ (Transaction)๊ณผ ๊ฒฉ๋ฆฌ ์์ค(Isolation Level)
- ๋น๊ด์ ๋ฝ(Pessimistic Lock, SELECT … FOR UPDATE)
- ๋๊ด์ ๋ฝ(Optimistic Lock, Version/UpdatedAt ํ๋ ํ์ฉ)
- DB ๋ ๋ฒจ ์ ์ฝ ์กฐ๊ฑด (Unique, EXCLUDE Constraint ๋ฑ)
- PostgreSQL ๊ธฐ๋ฐ ๊ตฌํ ์ฌ๋ก
- ์์ฝ ํ ์ด๋ธ ์ค๊ณ์ ์ ์ฝ ์กฐ๊ฑด
- ํธ๋์ญ์ + FOR UPDATE๋ฅผ ํ์ฉํ ์์ ํ ์์ฝ ์ฒ๋ฆฌ
- ๋์์ฑ ํ ์คํธ ์์ ๋ฐ ์๋๋ฆฌ์ค
- ์ฅ๋จ์ ๋ฐ ์ค์ ์ ์ฉ ์ ๋ต
- ๋น๊ด์ ๋ฝ vs ๋๊ด์ ๋ฝ
- ํธ๋์ญ์ ๊ฒฉ๋ฆฌ ์์ค๊ณผ ์ฑ๋ฅ ๊ณ ๋ ค
- ์๋น์ค ๊ท๋ชจ, ํธ๋ํฝ ํจํด์ ๋ฐ๋ฅธ ์ ํ ๊ฐ์ด๋
- ์ ๋ฆฌ ๋ฐ ๊ฒฐ๋ก
- ์ค๋ณต ์์ฝ ๋ฐฉ์ง๋ฅผ ์ํ ํต์ฌ ํฌ์ธํธ ์์ฝ
- ๋์์ฑ ์ ์ด๋ฅผ ์ ์ค๊ณํด์ผ ํ๋ ์ด์ ๊ฐ์กฐ
์ค๋ณต ์์ฝ ๋์์ฑ ๋ฌธ์ (Race Condition)
๋๋ธ ๋ถํน ์๋๋ฆฌ์ค
์๋ฅผ ๋ค์ด, ๊ฐ์ ์์ A์ 12์ 1์ผ๋ถํฐ 12์ 5์ผ๊น์ง ์์ฝ์ ํ๊ณ ์ถ์ด ํ๋ ๋ ์ฌ์ฉ์๊ฐ ์๋ค๊ณ ๊ฐ์ ํด๋ด ์๋ค.
๋ง์ฝ ์ด ๊ณผ์ ์์ DB๊ฐ ์์ฒญ ๊ฐ์ ๋์์ฑ์ ์ ์ดํ์ง ์์ผ๋ฉด,
์๊ฐ →
[์ฌ์ฉ์ 1] ์์ฝ ๊ฐ๋ฅ ์ฌ๋ถ ์กฐํ (OK)
[์ฌ์ฉ์ 2] ์์ฝ ๊ฐ๋ฅ ์ฌ๋ถ ์กฐํ (OK) ← ์์ง A์ ์์ฝ์ด DB์ ๋ฐ์๋์ง ์์
[์ฌ์ฉ์ 1] ์์ฝ ์์ฑ (SUCCESS)
[์ฌ์ฉ์ 2] ์์ฝ ์์ฑ (SUCCESS) ← ๋๋ธ ๋ถํน ๋ฐ์!
๋ ๋ช ์ ์ฌ์ฉ์(1, 2)๊ฐ ๋์ผํ ์์, ๋์ผํ ๊ธฐ๊ฐ์ ๋์์ ์์ฝํ๋ ค๋ ๊ฒฝ์ฐ ์ฌ์ฉ์ 1์ด ์์ฝ ๊ฐ๋ฅ ์ฌ๋ถ๋ฅผ ์กฐํํ๊ณ ์์ฑํ๊ธฐ ์ง์ ์ ์ฌ์ฉ์ 2๊ฐ ๊ทธ ์ฌ์ด์ ๊ปด๋ค์ด ์์ฝ ๊ฐ๋ฅ ์ฌ๋ถ ์กฐํ๋ฅผ ํ ์ ์๊ณ ์ฌ์ฉ์ 2๋ ์ฌ์ฉ์ 1์ด ์ด๋ฏธ ํด๋น ๋ ์ง์ ์์ฝ์ ์์ฑํ ์์ ์ด๋ผ๋ ๊ฒ๋ ๋ชจ๋ฅธ์ฑ ์ค๋ณต ์์ฝ์ ์ด๋ํ๊ฒ ๋ฉ๋๋ค.
์๋ /reservation ์ ๋น์ฆ๋์ค ๋ก์ง์ ์ฒ๋ฆฌํ๋ ์๋น์ค ๋จ์ ์ฝ๋๋ฅผ ์ดํด๋ณด๋ฉด
์์ ์์ฝ์ 1) ์ค๋ณต ์์ฝ ์กฐํ 2) ์์ฝ ์์ฑ ๋ ๋จ๊ณ๋ก ์ด๋ฃจ์ด์ ธ ์๋ค๋ ์ฌ์ค์ ์ ์ ์์ต๋๋ค.
/**
* โ ํธ๋์ญ์
์๋ ์์ฝ ์์ฑ (๋๋ธ ๋ถํน ๋ฐ์ ๊ฐ๋ฅ)
*
* ๋ฌธ์ ์ :
* 1. ์กฐํ์ ์์ฑ ์ฌ์ด์ Race Condition ๋ฐ์
* 2. ๋์ ์์ฒญ ์ ๋ ์์ฒญ ๋ชจ๋ "์์ฝ ์์"์ผ๋ก ํ๋จ
* 3. ๊ฒฐ๊ณผ: ๋๋ธ ๋ถํน ๋ฐ์
*
*/
async createWithoutTransaction(
createReservationDto: CreateReservationDto,
): Promise<Reservation> {
// ๋ ์ง ์ ํจ์ฑ ๊ฒ์ฆ
this.validateDates(
createReservationDto.check_in,
createReservationDto.check_out,
);
this.logger.log(
`Checking availability for accommodation ${createReservationDto.accommodation_id}`,
);
// Step 1: ์ค๋ณต ์์ฝ ์กฐํ (๋ฝ ์์)
const overlap = await this.reservationRepository
.createQueryBuilder('r')
.where('r.accommodation_id = :accommodationId', {
accommodationId: createReservationDto.accommodation_id,
})
.andWhere('r.status != :cancelled', {
cancelled: ReservationStatus.CANCELLED,
})
.andWhere('(r.check_in < :checkOut AND r.check_out > :checkIn)', {
checkIn: createReservationDto.check_in,
checkOut: createReservationDto.check_out,
})
// โ ๋ฝ์ด ์์! ์ฌ๊ธฐ์ Race Condition ๋ฐ์
.getOne();
if (overlap) {
throw new ConflictException('ํด๋น ๊ธฐ๊ฐ์ ์ด๋ฏธ ์์ฝ์ด ์กด์ฌํฉ๋๋ค.');
}
// โ ๏ธ ๋ฌธ์ : ์ ์กฐํ์ ์๋ ์์ฑ ์ฌ์ด์ ๋ค๋ฅธ ์์ฒญ์ด ๋ผ์ด๋ค ์ ์์!
// ์๋๋ฆฌ์ค:
// T1: ์ฌ์ฉ์ A ์กฐํ → ์์ฝ ์์
// T2: ์ฌ์ฉ์ B ์กฐํ → ์์ฝ ์์ (A๊ฐ ์์ง ์์ฑ ์ )
// T3: ์ฌ์ฉ์ A ์์ฑ → ์ฑ๊ณต
// T4: ์ฌ์ฉ์ B ์์ฑ → ์ฑ๊ณต (๋๋ธ ๋ถํน!)
// Step 2: ์์ฝ ์์ฑ
const reservation = this.reservationRepository.create({
...createReservationDto,
status: ReservationStatus.PENDING,
created_at: new Date(),
});
const saved = await this.reservationRepository.save(reservation);
this.logger.log(`Reservation created (ID: ${saved.id}) - NO TRANSACTION`);
return saved;
}
Race Condition ์ ์์ธ
์ด๋ฌํ ๋๋ธ ๋ถํน(Race Condition)์ด ๋ฐ์ํ๋ ๋ณธ์ง์ ์ธ ์์ธ์ ๋ฌด์์ผ๊น์?
์ฐ์ , ์ฒซ๋ฒ์งธ๋ ์์ ์์ฝ์ด '์์์ ' ์ธ ์์
๋จ์๊ฐ ์๋๊ธฐ ๋๋ฌธ์
๋๋ค.
๋ค์ ๋งํด, ์์ ์์ฝ = ์์ ์กฐํ + ์์ฑ ์ด๋ผ๋ ์ฌ๋ฌ๊ฐ์ ์์
์ผ๋ก ์ด๋ฃจ์ด์ ธ ์๊ธฐ ๋๋ฌธ์,
์์
์ฌ์ด์ ๋ค๋ฅธ ํธ๋์ญ์
์ด ๊ปด๋ค์ด๊ฐ ์ฌ์ง๋ฅผ ์ฃผ๋ ๊ฒ์ด ์ฒซ๋ฒ์งธ ๋ฌธ์ ๋ผ๊ณ ๋ณผ ์ ์์ต๋๋ค.
๋ง์ฝ ์์ ์์ฝ์ด atomic ํ ํ๋์ ์ฐ์ฐ์ผ๋ก ์ด๋ฃจ์ด์ ธ ์์๋ค๋ฉด, ๋์์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ง ์์์ ๊ฒ์ ๋๋ค.
๐ก *์์ ์์ฝ = ์์ ์กฐํ + ์์ฑ ์ ํ๋๋ก ๋ฌถ์ด์ ์์์ ์ธ ์ฐ์ฐ์ผ๋ก ์ฒ๋ฆฌ ํ ์ ์๋ค๋ฉด, Race Condition ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ์ง ์์๊น? *
์์์ ์ด๋ผ๋ ๊ฒ์ ํ ์์ ์ด ์์๋๋ฉด ์ค๊ฐ์ ๋ค๋ฅธ ์์ ์ด ๋ผ์ด๋ค ์ ์๊ณ , ๋๋ ๋๊น์ง ์์ ํ ์๋ฃ๋์ด์ผ ํ๋ค๋ ๋ป์ ๋๋ค. ๊ทธ๋ ๊ฒ ๋๋ฉด ๊ทธ ์ฌ์ด์ ๋ค๋ฅธ ์์ ์ด ๋ผ์ด๋ค์ด๊ฐ ์ ์๋ ํ์ ์ฃผ์ง ์๊ฒ ๋๋ฏ๋ก Race Condition ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ ์์ฒ์ ์ฐจ๋จํฉ๋๋ค. ํ์ง๋ง ํ์ค์ ์ผ๋ก DB์ ์ฌ๋ฌ ์ฟผ๋ฆฌ๋ฅผ ๋ ๋ฆฌ๋ ๊ฒฝ์ฐ, ๋จ์ผ ์ฟผ๋ฆฌ์ฒ๋ผ ์์ ํ ์์์ ์ผ๋ก ์คํ๋๋๋ก ๋ณด์ฅํ๋ ๋ฐฉ๋ฒ์ ์ ํ์ ์ ๋๋ค. ๋ค์ ๋์ฌ ํธ๋์ญ์ ์ ์ฌ์ฉํ๋ฉด ์ด๋ ์ ๋ ์์์ฑ์ ํ๋ณดํ ์ ์์ง๋ง, ์ฌ์ ํ ๊ฒฉ๋ฆฌ ์์ค, ๋ฝ ์ ๋ต, DB ์ ์ฝ ์กฐ๊ฑด ๋ฑ์ ๋ฐ๋ผ ๋์์ฑ ๋ฌธ์ ๋ฅผ ์์ ํ ์ฐจ๋จํ ์ ์๋์ง๋ ๋ฌ๋ผ์ง๋๋ค. ํธ๋์ญ์ ์ ์์์ ์ฐ์ฐ์ด ์๋ ์ฌ๋ฌ๊ฐ์ ์์ ๋จ์๋ฅผ ํ๋ฐ ๋ฌถ์ด all or nothing ์ ์คํํ๋๋ฐ ์ด์ ์ด ๋ง์ถ์ด์ ธ ์์ผ๋ฉฐ, ์ค์ ๋ฌผ๋ฆฌ์ ์ธ ์๋ฏธ์ ์์์ ์ฐ์ฐ์ด ์๋๋ผ '์์์ฑ' ์ด๋ผ๋ ํน์ง์ ๋ง์กฑํ ์ ์๋๋ก ์ฒ๋ฆฌํ๋ ์ผ์ข ์ trick ์ ๋๋ค.
์ฆ, “์์์ ์ฐ์ฐ”์ด๋ผ๋ ๊ฐ๋ ์ ์์ด๋์ด์ ์๋ฒฝํ ํด๊ฒฐ์ฑ ์ด์ง๋ง, ์ค์ ์์คํ ์์๋ ํธ๋์ญ์ , ๋น๊ด์ /๋๊ด์ ๋ฝ, DB ์ ์ฝ ์กฐ๊ฑด ๋ฑ๊ณผ ํจ๊ป ์ ์ฉํด์ผ Race Condition์ ํจ๊ณผ์ ์ผ๋ก ๋ฐฉ์งํ ์ ์์ต๋๋ค.
๋๋ฒ์งธ๋, ๋์ผํ ๋ฆฌ์์ค์ ๋ํด ๋์ ์ ๊ทผ์ ์ ์ดํ์ง ์์๊ธฐ ๋๋ฌธ์ ๋๋ค.
๋์์ฑ ์ ์ด ๊ธฐ๋ฒ ์๊ฐ
๋ฐ๋ผ์ ์ด๋ฌํ ์ค๋ณต ์์ฝ์ ๋ง๊ธฐ ์ํด ์ฌ์ฉํ ์ ์๋ ๋์์ฑ ์ ์ด ๋ฐฉ๋ฒ์ ํฌ๊ฒ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
- ํธ๋์ญ์ (Transaction) ๊ณผ ๊ฒฉ๋ฆฌ ์์ค(Isolation Level)
- ๋น๊ด์ ๋ฝ(Pessimistic Lock, SELECT … FOR UPDATE)
- ๋๊ด์ ๋ฝ(Optimistic Lock, Version/UpdatedAt ํ๋ ํ์ฉ)
- DB ๋ ๋ฒจ ์ ์ฝ ์กฐ๊ฑด (Unique, EXCLUDE Constraint ๋ฑ)
๊ฐ ๋ฐฉ์์ ์กฐ๊ธ ๋ค๋ฅธ ์๋ฆฌ๋ก Race Condition ๋ฌธ์ ๋ฅผ ํด๊ฒฐํฉ๋๋ค. ์์ผ๋ก ๋ค๊ฐ์ง ๋ฐฉ์์ ๋ํด ํ๋์ฉ ์๊ฐํ๊ฒ ์ต๋๋ค.
ํธ๋์ญ์ (Transaction)๊ณผ ๊ฒฉ๋ฆฌ ์์ค(Isolation Level)
์ฒซ๋ฒ์งธ ๋ฐฉ์์ ํธ๋์ญ์
(Transaction)๊ณผ ๊ฒฉ๋ฆฌ ์์ค(Isolation Level)์ ํ์ฉํด์ ๋์์ฑ ์ ์ด๋ฅผ ํ๋ ๋ฐฉ๋ฒ์
๋๋ค. ํธ๋์ญ์
์ ์ฌ์ฉํ๋ฉด ์ฌ๋ฌ ์ฟผ๋ฆฌ๋ฅผ ํ๋์ ๋จ์๋ก ๋ฌถ์ด ์์์ฑ(Atomicity) ์ ๋ณด์ฅํ ์ ์์ต๋๋ค. ๋ํ, ๊ฒฉ๋ฆฌ ์์ค์ ์กฐ์ ํ๋ฉด ๋ค๋ฅธ ํธ๋์ญ์
๊ณผ์ ๊ฐ์ญ์ ์ด๋ ์ ๋ ์ ์ดํ ์ ์์ต๋๋ค.
์: READ COMMITTED, SERIALIZABLE
ํธ๋์ญ์ ์ด๋?
ํธ๋์ญ์ ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์ํ๋๋ ๋ ผ๋ฆฌ์ ์์ ๋จ์ ๋ก, ์ฌ๋ฌ ๊ฐ์ SQL ๋ฌธ์ ํ๋์ ์์์ (atomic) ๋จ์๋ก ๋ฌถ์ด ์คํํฉ๋๋ค.
ACID ์์ฑ
- Atomicity (์์์ฑ): ํธ๋์ญ์ ๋ด ๋ชจ๋ ์์ ์ด ์์ ํ ์ฑ๊ณตํ๊ฑฐ๋, ์์ ํ ์คํจํด์ผ ํจ
- Consistency (์ผ๊ด์ฑ): ํธ๋์ญ์ ์คํ ์ ํ๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค๊ฐ ์ผ๊ด๋ ์ํ๋ฅผ ์ ์ง
- Isolation (๊ฒฉ๋ฆฌ์ฑ): ๋์์ ์คํ๋๋ ํธ๋์ญ์ ๋ค์ด ์๋ก ์ํฅ์ ์ฃผ์ง ์์
- Durability (์ง์์ฑ): ์ปค๋ฐ๋ ํธ๋์ญ์ ์ ๊ฒฐ๊ณผ๋ ์๊ตฌ์ ์ผ๋ก ์ ์ฅ๋จ
์์ฝ ์์คํ ์์์ ์ค์์ฑ
์์ฝ ์์คํ ์์๋ ๋ค์๊ณผ ๊ฐ์ ์์ ์ด ์์์ ์ผ๋ก ์ํ๋์ด์ผ ํฉ๋๋ค.
1. ํด๋น ๊ธฐ๊ฐ์ ์์ฝ์ด ์๋์ง ํ์ธ
2. ์์ผ๋ฉด ์์ฝ ์์ฑ
/**
* ํธ๋์ญ์
๊ธฐ๋ฐ ์์ฝ ์์ฑ
*/
async create(createReservationDto: CreateReservationDto): Promise<Reservation> {
// ๋ ์ง ์ ํจ์ฑ ๊ฒ์ฆ
this.validateDates(createReservationDto.check_in, createReservationDto.check_out);
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction('READ COMMITTED');
const startTime = Date.now();
try {
// Step 1: ์ค๋ณต ์์ฝ ์กฐํ + ๋น๊ด์ ๋ฝ (FOR UPDATE)
this.logger.log(
`Checking availability for accommodation ${createReservationDto.accommodation_id} ` +
`from ${createReservationDto.check_in} to ${createReservationDto.check_out}`,
);
const overlap = await this.checkOverlapWithLock(queryRunner, createReservationDto);
if (overlap) {
throw new ConflictException(
`ํด๋น ๊ธฐ๊ฐ(${createReservationDto.check_in} ~ ${createReservationDto.check_out})์ ์ด๋ฏธ ์์ฝ์ด ์กด์ฌํฉ๋๋ค. ` +
`์์ฝ ID: ${overlap.id}`,
);
}
// Step 2: ์์ฝ ์์ฑ
const reservation = queryRunner.manager.create(Reservation, {
...createReservationDto,
status: ReservationStatus.PENDING,
created_at: new Date(),
});
const saved = await queryRunner.manager.save(Reservation, reservation);
// Step 3: ์ปค๋ฐ
await queryRunner.commitTransaction();
const duration = Date.now() - startTime;
this.logger.log(`Reservation created successfully (ID: ${saved.id}) in ${duration}ms`);
if (duration > 1000) {
this.logger.warn(`Slow transaction detected: ${duration}ms`);
}
return saved;
} catch (error) {
// ๋กค๋ฐฑ
await queryRunner.rollbackTransaction();
this.logger.error(
`Reservation failed for accommodation ${createReservationDto.accommodation_id}: ${error.message}`,
error.stack,
);
throw error;
} finally {
// ์ฐ๊ฒฐ ํด์
await queryRunner.release();
}
}
/**
* ๋ ์ง ์ ํจ์ฑ ๊ฒ์ฆ
*/
private validateDates(checkIn: string, checkOut: string): void {
const checkInDate = new Date(checkIn);
const checkOutDate = new Date(checkOut);
const today = new Date();
today.setHours(0, 0, 0, 0);
if (checkInDate < today) {
throw new BadRequestException('์ฒดํฌ์ธ ๋ ์ง๋ ์ค๋ ์ดํ์ฌ์ผ ํฉ๋๋ค');
}
if (checkOutDate <= checkInDate) {
throw new BadRequestException('์ฒดํฌ์์ ๋ ์ง๋ ์ฒดํฌ์ธ ๋ ์ง ์ดํ์ฌ์ผ ํฉ๋๋ค');
}
// ์ต๋ ์์ฝ ๊ธฐ๊ฐ ์ ํ (์: 30์ผ)
const diffDays = Math.ceil(
(checkOutDate.getTime() - checkInDate.getTime()) / (1000 * 60 * 60 * 24),
);
if (diffDays > 30) {
throw new BadRequestException('์ต๋ 30์ผ๊น์ง ์์ฝ ๊ฐ๋ฅํฉ๋๋ค');
}
}
๊ทธ๋ฌ๋ ํธ๋์ญ์ ์ ํตํด 1~2๋ฅผ ์์์ ์ผ๋ก ์ฒ๋ฆฌํ๋ค๊ณ ํ๋๋ผ๋ ์ฌ์ ํ Race Condition ๋ฌธ์ ๊ฐ ๋ฐ์ํฉ๋๋ค. ๊ทธ ์ด์ ๋ ํธ๋์ญ์ ์ฌ์ด์ ๋ค๋ฅธ ํธ๋์ญ์ ์ด ๋์์ ์คํ๋ ์ ์๊ธฐ ๋๋ฌธ์ ๋๋ค.
- Non-atomic Read-Modify-Write: ์ฝ๊ธฐ์ ์ฐ๊ธฐ ์ฌ์ด์ ๋ค๋ฅธ ํธ๋์ญ์ ์ด ๋ผ์ด๋ค ์ ์์
- ๊ฒฉ๋ฆฌ ์์ค ๋ถ์กฑ: ํธ๋์ญ์ ์ด ๋ค๋ฅธ ํธ๋์ญ์ ์ ๋ฏธ์ปค๋ฐ ๋ฐ์ดํฐ๋ฅผ ๋ณด์ง ๋ชปํจ
- ๋ฝ(Lock) ๋ถ์ฌ: ๋์ผํ ๋ฆฌ์์ค์ ๋ํด ๋์ ์ ๊ทผ์ ์ ์ดํ์ง ์์
๋ง์ ์ฌ๋๋ค์ด ๊ฐ์ง๊ณ ์๋ ํํ ์คํด ์ค ํ๋๋ 'ํธ๋์ญ์ ' ์ด ๋ฐฐํ์ ์ผ๋ก ์คํ๋๋ค๊ณ ์๊ฐํ๋ ์ ์ ๋๋ค. ํ์ง๋ง ์ด๋ ํธ๋์ญ์ ๊ฒฉ๋ฆฌ ๋ ๋ฒจ ์์ค์ ๋ฐ๋ผ ๋ค๋ฆ ๋๋ค. ๋ฐ๋ผ์ ํธ๋์ญ์ ์ ์ฌ์ฉํ๋๋ผ๋ ๋ฐฐํ์ ์ฒ๋ฆฌ๋ฅผ ๋ณด์ฅํ์ง ์์ผ๋ฉฐ ๋์์ ์คํ๋ ์ ์๋ค๋ ์ฌ์ค์ ์ ์ํด์ผํฉ๋๋ค.
ํธ๋์ญ์ ๊ฒฉ๋ฆฌ ์์ค(Isolation Level)
PostgreSQL์ 4๊ฐ์ง ๊ฒฉ๋ฆฌ ์์ค์ ์ ๊ณตํฉ๋๋ค.
| ๊ฒฉ๋ฆฌ ์์ค | Dirty Read | Non-Repeatable Read | Phantom Read | ์ค๋ช |
|---|---|---|---|---|
| READ UNCOMMITTED | O | O | O | ์ปค๋ฐ๋์ง ์์ ๋ฐ์ดํฐ๋ ์ฝ์ ์ ์์ (PostgreSQL ๋ฏธ์ง์) |
| READ COMMITTED | X | O | O | ์ปค๋ฐ๋ ๋ฐ์ดํฐ๋ง ์ฝ์ (PostgreSQL ๊ธฐ๋ณธ๊ฐ) |
| REPEATABLE READ | X | X | O | ๋์ผ ํธ๋์ญ์ ๋ด์์ ๋ฐ๋ณต ์ฝ๊ธฐ ์ ๋์ผํ ๊ฒฐ๊ณผ ๋ณด์ฅ |
| SERIALIZABLE | X | X | X | ์์ ํ ์ง๋ ฌํ, ๊ฐ์ฅ ์์ ํ์ง๋ง ์ฑ๋ฅ ์ ํ ๊ฐ๋ฅ |
๋ง์ฝ ๊ฒฉ๋ฆฌ ์์ค์ SERIALIZABLE๋ก ์ค์ ํ๋ค๋ฉด ๋ชจ๋ ํธ๋์ญ์ ์ ํญ์ ๋ฐฐํ์ ์ผ๋ก ์คํ๋๋ค๋ ๊ฒ์ ๋ณด์ฅํ ์ ์๊ฒ ์ง๋ง, ๊ทธ์ธ์ READ UNCOMMITTED๋ READ COMMITTED ๋ก ์ค์ ํ๋ฉด, ํ๋์ ํธ๋์ญ์ ๋ด๋ถ์์๋ ๋ค๋ฅธ ํธ๋์ญ์ ์์ ์งํ ์ค์ธ ์์ ๋ ์ฝ์ ์ ์๊ฒ ๋ฉ๋๋ค. ์ฆ, ํ๋์ ์์ ๋ด์์๋ ๋ค๋ฅธ ํธ๋์ญ์ ์ด ๋ผ์ด๋ค์ด ํ์ฌ ํธ๋์ญ์ ์ ์ํฅ์ ์ค ์ ์๊ฒ ๋๋ค๋ ์๋ฏธ์ ๋๋ค.
๋ฐ๋ผ์ ํธ๋์ญ์ ์ ๊ฑธ๊ณ ๊ฒฉ๋ฆฌ ์์ค์ SERIALIZABLE๋ก ์ค์ ํ๋ฉด ์์ ํ ์ง๋ ฌํ๋ฅผ ๋ณด์ฅํ๋ฏ๋ก ๋์์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์์ต๋๋ค. ํ์ง๋ง ์ด ๋ฐฉ๋ฒ์ ๋ชจ๋ ์์ ์ ์์ฐจ์ ์ผ๋ก ์ฒ๋ฆฌํ๋ฏ๋ก ๋์ ์ฒ๋ฆฌ ์ฑ๋ฅ์ด ํฌ๊ฒ ๋จ์ด์ง๋ค๋ ํฐ ๋จ์ ์ด ์์ต๋๋ค. ์ฆ, ๊ฒฉ๋ฆฌ ์์ค์ ๋์ด๋ฉด ๋ฐ์ดํฐ์ ์ผ๊ด์ฑ์ ๋์์ง์ง๋ง, ๊ทธ์ ๋์์ ๋ณ๋ ฌ ์ฒ๋ฆฌ ์ฑ๋ฅ์ ๋จ์ด์ง๋ ํธ๋ ์ด๋ ์คํ ๊ด๊ณ์ ์์ต๋๋ค.
ํ์ง๋ง ์ค์ ๋์์ฑ ์ ์ด๊ฐ ํ์ํ ๋ถ๋ถ์ '์์ ์์ฝ' ๋ฟ์ธ๋ฐ,
SERIALIZABLE๋ก ์ค์ ํด์ ๋ชจ๋ DB ์ฐ์ฐ์ ์ง๋ ฌํํด์ ์ฒ๋ฆฌํ๋ ๊ฑด ๋ฌด์์ธ๊ฐ ์ํด๋ฅผ ๋ณด๋ ๋๋์ด ๋ญ๋๋ค.
๋ฐ๋ผ์ ์ผ๋ฐ์ ์ผ๋ก๋ ํธ๋์ญ์ ๊ฒฉ๋ฆฌ ์์ค์ ๊ฐ๋ฅํ ํ ๋ฎ๊ฒ ์ ์งํ๋ฉด์ ํน์ ํ ์ํฉ๊ณผ ์กฐ๊ฑด์ ๋ํด์๋ง ๋์์ฑ ์ฒ๋ฆฌ๋ฅผ ํด์ฃผ๋ ๊ฒ์ด ์ฒ๋ฆฌ ์ฑ๋ฅ ์ธก๋ฉด์์ ์ข์ ์ ํ์ผ ์ ์์ต๋๋ค.
์ฐธ๊ณ
MySQL์ ๋ฝ ์ข ๋ฅ๋ ํฌ๊ฒ ๊ธ๋ก๋ฒ ๋ฝ, ํ ์ด๋ธ ๋ฝ, ๋ค์๋ ๋ฝ, ๋ฉํ๋ฐ์ดํฐ ๋ฝ ๋ฑ๊ณผ ๊ฐ์ ์์ง ๋ ๋ฒจ์ ๋ฝ๊ณผ ๊ณต์ ๋ฝ(S-LOCK), ๋ฐฐํ ๋ฝ(X-LOCK) ๋ฑ๊ณผ ๊ฐ์ ํธ๋์ญ์ ๋ ๋ฒจ์ ๋ฝ์ผ๋ก ๋๋ ์ ์์ต๋๋ค.
์ฐธ๊ณ
ํธ๋์ญ์ ๋ ๋ฒจ์ ๋ฝ์ ํ์ฉํ๋ฉด DB ์ ์ฒด์ ๊ฒฉ๋ฆฌ ์์ค์ ๊ฑด๋ค์ง ์๊ณ ํน์ ํธ๋์ญ์ ์ ๋ํด์๋ง lock ์ ๊ฑธ ์ ์์ต๋๋ค.
ํ ์์ค ์ ๊ธ
๋น๊ด์ ๋ฝ(Pessimistic Lock)
๋น๊ด์ ๋ฝ(Pessimistic Lock)์๋ ๋ํ์ ์ผ๋ก ๋ ๊ฐ์ง๊ฐ ์์ต๋๋ค.
- ๊ณต์ ์ ๊ธ(Shared Lock, S-LOCK): ๋ฐ์ดํฐ๋ฅผ ์ฝ์ ๋ ์ฌ์ฉ๋๋ฉฐ, ์ฌ๋ฌ ํธ๋์ญ์
์ด ๋์์ ์ ๊ทผํ์ฌ ์ฝ์ ์ ์์ง๋ง, ๋์์ ์์ ํ ์๋ ์์ต๋๋ค.
ex) ์ํ ์์คํ ์์ ๊ณ ๊ฐ์ ์์ก์ ๋จ์ํ ์กฐํ๋ง ํ ๋, ๋์์ ์ฌ๋ฌ ์ง์์ด ์กฐํํด๋ ๋์ง๋ง, ์์ (์ ์ถ๊ธ)์ ํ๋๋ง ํด์ผ ํจ - ๋ฐฐํ ์ ๊ธ(Exclusive Lock, X-LOCK): ๋ฐ์ดํฐ๋ฅผ ์์ ํ ๋ ์ฌ์ฉ๋๋ฉฐ, ์ค์ง ํ๋์ ํธ๋์ญ์
๋ง ์ ๊ทผ์ ํ์ฉํฉ๋๋ค. ๋ค๋ฅธ ์ ๊ธ(๊ณต์ ํฌํจ)์ ๋ชจ๋ ์ฐจ๋จํฉ๋๋ค.
ex) ATM์์ ํ ๊ณ ๊ฐ์ ์์ก์ ์์ (์ถ๊ธ)ํ ๋, ๋์์ ๋ค๋ฅธ ๊ณณ์์ ๊ทธ ๊ณ ๊ฐ์ ์์ก์ ์ฝ๊ฑฐ๋ ์์ ํ์ง ๋ชปํ๊ฒ ํด์ผ ํจ.
| ๊ตฌ๋ถ | ๋ฝ ์ข ๋ฅ | ๋ค๋ฅธ ํธ๋์ญ์ ์ ์ฝ๊ธฐ ํ์ฉ | ๋ค๋ฅธ ํธ๋์ญ์ ์ ์ฐ๊ธฐ ํ์ฉ | ๋ํ ๊ตฌ๋ฌธ |
|---|---|---|---|---|
| ๊ณต์ ์ ๊ธ | S-Lock | โ ๊ฐ๋ฅ | โ ๋ถ๊ฐ | SELECT ... LOCK IN SHARE MODE |
| ๋ฐฐํ ์ ๊ธ | X-Lock | โ ๋ถ๊ฐ | โ ๋ถ๊ฐ | SELECT ... FOR UPDATE |
๋ค๋ฅธ ์ค๋ณต ์์ฝ ๋ฐฉ์ง ๊ธ์ ๋ณด๋ฉด ์ฃผ๋ก SELECT … FOR UPDATE ๋ฅผ ๋ง์ด ์ฌ์ฉํด์ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋๋ฐ
๋ณธ๋ SELECT … FOR UPDATE ๋ ๋จ์ ์กฐํ๊ฐ ์๋๋ผ ์ ์ฌ์ ์ผ๋ก ์์ (write)ํ ํ์ ํ๋ณดํ๊ธฐ ์ํด ์ฌ์ฉ๋๋ ๋ฌธ๋ฒ์
๋๋ค.SELECT … FOR UPDATE ๋ ํญ์ ํธ๋์ญ์
์์์ ์ฌ์ฉํด์ผํฉ๋๋ค. SELECT … FOR UPDATE ๋ฅผ ์ฐ๋ฉด, ์กฐํ ์์ ๋ถํฐ ํธ๋์ญ์
์ปค๋ฐ๊น์ง ๋ค๋ฅธ ํธ๋์ญ์
์ด ๊ฐ์ ํ์ ์ ๊ทผํ๊ฑฐ๋ ์์ ํ์ง ๋ชปํ๊ฒ ๋ฉ๋๋ค.
๊ทผ๋ฐ ์ฌ๊ธฐ์ ์๋ฌธ์ด ๋ ๊ฑด, SELECT … FOR UPDATE๋ ๊ฒฐ๊ตญ ํ์ฌ ํธ๋์ญ์ ๋ด์์ ๋ณ๊ฒฝ(FOR UPDATE)์ ์ํด ๋ค๋ฅธ ํธ๋์ญ์ ์ ์์ ํ์ง ๋ชปํ๊ฒ write ๋ฅผ ์ํด lock ์ ๊ฑฐ๋ ๋ฌธ๋ฒ ์ธ ๊ฒ ๊ฐ์์ต๋๋ค.
๊ทธ๋ฐ๋ฐ ๊ฒฐ๊ตญ ์ค๋ณต ์์ฝ์ ๋ฐฉ์งํ๊ธฐ ์ํด์๋ read์ ๋ฐฐํ์ Lock ์ ๊ฑฐ๋ ๊ฒ์ด ํต์ฌ์ ๋๋ค. ๋ฐ๋ผ์
SELECT ... FOR UPDATE ๊ฐ write ๋ฟ๋ง ์๋๋ผ read ์๋ ๋ฐฐํ์ lock ์ ๊ฑธ์ด์ฃผ๋์ง ํ์ธํ ํ์๊ฐ ์์์ต๋๋ค.
์ฌ๋ฌ๊ธ์ ์ฐพ์๋ณด์๋๋ฐ SELECT ... FOR UPDATE ๋ ์ผ๋ฐ์ ์ผ๋ก๋ Exclusive Lock ์ฒ๋ผ ๋์ํ๋ค๊ณ ์ค๋ช
ํ๋ ๊ธ๋ค์ด ๋ง์์ผ๋, ์ผ๋ถ ๊ธ์์๋ ์ฝ๊ธฐ ์์ฒด์ ๋ํด์๋ ๋ฐฐํ์ ์ผ๋ก lock ์ ๊ฑธ์ด์ฃผ์ง๋ ์๋๋ค๊ณ ์ค๋ช
ํ์ต๋๋ค.
๊ทธ๋์ ์คํ์ ์ง์ ์ค๊ณํด๋ณด์์ต๋๋ค. ๊ทธ ๊ฒฐ๊ณผ PostgreSQL ์์ SELECT FOR UPDATE ๊ตฌ๋ฌธ์ Shared Lock ์ฒ๋ผ ๋์ํ๋ ๊ฒ์ ํ์ธํ ์ ์์์ต๋๋ค.

๋ํ, https://www.postgresql.org/docs/current/transaction-iso.html ๊ณต์ ๋ฌธ์์์ SELECT FOR UPDATE ์ ๊ตฌ์ฒด์ ์ธ ๋์์ ํ์ธํ ์ ์์์ต๋๋ค.
์ฝ๊ธฐ ์ ์ฉ ํธ๋์ญ์ ์๋ ์ง๋ ฌํ ์ถฉ๋์ด ๋ฐ์ํ์ง ์๋๋ค.
ํ ์์ค ์ ๊ธ์ ๋ฐ์ดํฐ ์ฟผ๋ฆฌ์๋ ์ํฅ์ ๋ฏธ์น์ง ์์ผ๋ฉฐ, ๋์ผํ ํ์ ๋ํ ์ฐ๊ธฐ(writer)์ ์ ๊ธ(locker) ๋ง ์ฐจ๋จํฉ๋๋ค.

๋ํ, PostgreSQL ์์ SELECT FOR UPDATE ๊ตฌ๋ฌธ์ ๋์๋ฐฉ์ ์์ select for update ๊ตฌ๋ฌธ์ ํธ๋์ญ์ ๊ฒฉ๋ฆฌ๋ ๋ฒจ์ ๋ฐ๋ผ์ ๋ค๋ฅด๊ฒ ๋์ํ๋ค๋ ์ฌ์ค์ ์์์ต๋๋ค. ์ฌ๊ธฐ์๋
Read committed ์ Repeatable Read๋
A ํธ๋์ญ์
์ด select for update ๋ก ํ์ ์ ๊ทธ๋๋ผ๋ B ํธ๋์ญ์
์์ ๋ฐ์ดํฐ๋ฅผ ์ฝ์ ์ ์๋ค๊ณ ๋งํฉ๋๋ค.
์ฆ, PostgreSQL์์ SELECT … FOR UPDATE ๋ Exclusive Lock(X-Lock)์ ๊ฑธ์ง๋ง, MVCC ๋๋ถ์ ๋ค๋ฅธ ํธ๋์ญ์ ์ ๋จ์ SELECT๋ ๋ธ๋กํนํ์ง ์์ต๋๋ค.
์ฒ์์๋ SELECT … FOR UPDATE ๋ฅผ ์ฌ์ฉํ๋ฉด write lock์ ๊ฑฐ๋ ๊ฒ๊ณผ ๋์์ read์๋ lock ์ด ๊ฑธ๋ฆฌ๋ฏ๋ก, ์กฐํ ์์ ๋ถํฐ ํธ๋์ญ์ ์ปค๋ฐ๊น์ง ๋ค๋ฅธ ํธ๋์ญ์ ์ด ๊ฐ์ ํ์ ์ ๊ทผํ๊ฑฐ๋ ์์ ํ์ง ๋ชปํ ๊ฒ ์ด๋ผ๊ณ ์๊ฐํ๋๋ฐ, ์๋ชป ์๊ณ ์์๋ค๋ ์ฌ์ค์ ๊นจ๋ฌ์์ต๋๋ค. ์ด๋ ๊ฑธ๋ฆฌ๋ ๋ฝ์ ํ์ฌ ํธ๋์ญ์ ์ด ํด๋น ํ์ ์์ ํ ๊ฒ์ด๋ผ๋ ์ ์ ํ์ ๋ค๋ฅธ ํธ๋์ญ์ ์ ์ฐ๊ธฐ(write) ์ ๊ทผ์ ๋ง๋ ๊ฒ์ ๋๋ค. ๋ค๋ฅธ ํธ๋์ญ์ ์์ ๋จ์ ์ฝ๊ธฐ(SELECT๋ง ํ๋ ๊ฒฝ์ฐ)๋ ๋งํ์ง ์์ต๋๋ค. ์ฆ, read๋ ํ์ฉ๋์ง๋ง, ๋ค๋ฅธ ํธ๋์ญ์ ์์ FOR UPDATE ๋๋ UPDATE/DELETE ๊ฐ์ write ์๋๋ง ๋๊ธฐํฉ๋๋ค. ๋ฐ๋ผ์ ์์ฝ๊ฒ๋ read ์ ๋ฐฐํ์ lock ์ ๊ฑธ์ด์ผ ํ๋ ํ์ฌ ๋ฌธ์ ์ํฉ์์๋ SELECT … FOR UPDATE ๊ฐ ๋์์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํด์ฃผ์ง ๋ชปํฉ๋๋ค.
๋ํ, ์ด ์ฟผ๋ฆฌ๊ฐ ์ค์ ๋ก ๋ฝ์ ๊ฑฐ๋ ๋์์ "์กฐํ๋ ํ๋ค" ๋ฟ ์ด๋ผ๋ ๋ฌธ์ ์ ๋ ์์์ต๋๋ค.
๋ง์ฝ “์กฐํ ๊ฒฐ๊ณผ๊ฐ ๋น์ด ์๋ค๋ฉด”, ์ฆ ์์ง ์์ฝ๋ ํ์ด ์์ผ๋ฉด ๋ฝ์ด ๊ฑธ๋ฆฌ์ง ์์ต๋๋ค. ์ด ๋๋ฌธ์ SELECT … FOR UPDATE๋ “๊ฒฝ์ ๋ฐ์ดํฐ๊ฐ ์ด๋ฏธ ์กด์ฌํ๋ ์ํฉ”์์๋ ์ ํจํ์ง๋ง,
“์์ง ์ฒซ ์์ฝ์ด ๋ง๋ค์ด์ง๊ธฐ ์ ” ์ํฉ์์๋ ๋ฌด๋ ฅํฉ๋๋ค.
๊ทธ ๊ฒฐ๊ณผ, ํธ๋์ญ์ ์์ค์ ๋ฝ์ธ SELECT FOR UPDATE ๋ก๋ ํ์ฌ ์ํฉ์์ ์๋ฒฝํ๊ฒ ๋์์ฑ(Race Condition) ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์๋ค๊ณ ํ๋จํ๊ฒ ๋์์ต๋๋ค.
Explicit Locking
๊ทธ๋ ๋ค๋ฉด ์ฝ๊ธฐ ์์ฒด์ ๋ฐฐํ์ ์ธ lock ์ ๊ฑธ๋ ค๋ฉด SELECT FOR UPDATE ๋ง๊ณ ๋ญ ํ์ฉํด์ผํ์ง?
๊ณ ๋ฏผ์ ํ๋ค๊ฐ, PostgreSQL ์์ Explicit Locking ์ ์ ๊ณตํด์ฃผ๋ ๊ฒ์ ํ์ธํ ์ ์์์ต๋๋ค.
PostgreSQL์ ํ ์ด๋ธ ๋ฐ์ดํฐ์ ๋ํ ๋์ ์ ๊ทผ์ ์ ์ดํ๊ธฐ ์ํด ๋ค์ํ ์ ๊ธ ๋ชจ๋๋ฅผ ์ ๊ณตํฉ๋๋ค. ์ด๋ฌํ ๋ชจ๋๋ MVCC๊ฐ ์ํ๋ ๋์์ ์ ๊ณตํ์ง ์๋ ์ํฉ์์ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ด ์ ๊ธ์ ์ฌ์ฉ๋ ์ ์์ต๋๋ค.
13.3.1. ํ
์ด๋ธ ์์ค ์ ๊ธ
13.3.2. ํ ์์ค ์ ๊ธ
13.3.3. ํ์ด์ง ์์ค ์ ๊ธ
13.3.4. ๊ต์ฐฉ ์ํ
13.3.5. ๊ถ๊ณ ์ ๊ธ

๊ทธ๋ฆฌ๊ณ ๊ณต์ ๋ฌธ์ ์ค์์ ๋ค์๊ณผ ๊ฐ์ด Tip ์ ๋ช ์ํ ๊ฒ์ ํ์ธํ ์ ์์์ต๋๋ค.

“Only an ACCESS EXCLUSIVE lock blocks a SELECT (without FOR UPDATE/SHARE) statement.”
์ฆ ์ด๋ฅผ ์์ฝํ๋ฉด, ์ผ๋ฐ์ ์ธ SELECT ์ฟผ๋ฆฌ(๋จ์ ์ฝ๊ธฐ)๋ ์ฌ๋งํ ๋ฝ์๋ ๋งํ์ง ์์ผ๋ฉฐ, ์ค์ง ๊ฐ์ฅ ๊ฐ๋ ฅํ ๋ฝ(ACCESS EXCLUSIVE)์ด ๊ฑธ๋ ค ์์ ๋๋ง SELECT๋ ๋งํ๋ค๋ ์๋ฏธ์ ๋๋ค.
LOCK TABLE ~ IN ACCESS EXCLUSIVE MODE
๋ฐ๋ผ์ LOCK TABLE reservations IN ACCESS EXCLUSIVE MODE ๋ฅผ ํ์ฉํ์ฌ ํ
์ด๋ธ ๋จ์๋ก
ํด๋น ํ
์ด๋ธ์ ์ ๊ทผํ๋ ค๋ ๋ชจ๋ ๋ค๋ฅธ ํธ๋์ญ์
์ ์ฐจ๋จํ ์ ์์ต๋๋ค.
- ๋ค๋ฅธ ํธ๋์ญ์
์
SELECT,INSERT,UPDATE,DELETE๋ฅผ ์ ํ ์ํํ ์ ์์ต๋๋ค. - ๋์ค์ ๋ค์ด์ค๋ SELECT์กฐ์ฐจ๋ ๋ฝ์ด ํด์ ๋ ๋๊น์ง ๋๊ธฐ ์ํ์ ๋ค์ด๊ฐ๋๋ค.
reservations ํ
์ด๋ธ์ ์ฌ์ฉํ ๋ ๋ฐฐํ์ lock ์ ๊ฑฐ๋ ๋ฐฉ์์ผ๋ก ๋ณธ ๋ฌธ์ ์ํฉ์ ํด๊ฒฐํ ์๋ ์๊ฒ ์ง๋ง,
์ฌ์ค์ ๋ณ๋ ฌ ์ฒ๋ฆฌ๊ฐ ๊ฐ๋ฅํ ์๋ก ๋ค๋ฅธ ์์์ ์๋ก ๋ค๋ฅธ ๋ ์ง์ ์์ฝ์ ๋ํด์๋ ๋ฐฐํ์ lock ์ ๊ฑฐ๋๊ฑฐ๋ผ
์ฑ๋ฅ์ด ๋จ์ด์ง ๊ฒ์ ์ฐ๋ คํ์ฌ ๋ ๋์ ๋ฐฉ์์ด ์์์ง ๊ณ ๋ฏผ์ ํ๊ฒ ๋์์ต๋๋ค. ( ๊ทธ๋ฐ๋ฐ ์ฐ์ ์ ์๊ฒฌ์ผ๋ก๋ ์์ ์์ฝ์ด ๋ฐ์ํ๋ ๋น๋๊ฐ ์ค์ํ ๊ฒ ๊ฐ์๋ฐ 1์ด ์์ ์๋ฒ ์์ฝ์ด ๋ฐ์ํ ์ ์๋ ์ํฉ์ธ์ง์ ๋ํด์ ๋จผ์ ํ๋จํ๊ณ ๊ทธ๋ ๋ค๋ฉด ๋ ๋ฎ์ ๊ฒฉ๋ฆฌ ์์ค์ผ๋ก ๋์ ์ฒ๋ฆฌ ์ฑ๋ฅ์ ๋์ด๋ ๋ฐฉ์์ ๊ณ ๋ฏผํด๋ณผ ๊ฒ ๊ฐ์ต๋๋ค. )
BEGIN;
LOCK TABLE reservations IN ACCESS EXCLUSIVE MODE;
-- ์ด์ ์ด ํ
์ด๋ธ์ ๋ํด SELECT, INSERT, UPDATE, DELETE ๋ฑ ๋ชจ๋ ์ ๊ทผ์ด ์ฐจ๋จ๋จ
SELECT * FROM reservations; -- ๊ฐ๋ฅ (ํ์ฌ ํธ๋์ญ์
๋ง)
UPDATE reservations SET status = 'CANCELLED'; -- ๊ฐ๋ฅ (ํ์ฌ ํธ๋์ญ์
๋ง)
COMMIT;
์ ํ๋ฆฌ์ผ์ด์ ๋ ๋ฒจ ๋ฝ(Application-Level Lock) : Advisory Locks
PostgreSQL์์๋ ์ผ๋ฐ์ ์ธ ํ ์ด๋ธ/ํ ๊ธฐ๋ฐ ๋ฝ ์ธ์๋ ์ ํ๋ฆฌ์ผ์ด์ ์ด ์ ์ํ ์๋ฏธ๋ฅผ ๊ฐ์ง ๋ฝ์ ๊ฑธ ์ ์์ต๋๋ค.
Advisory Lock์ PostgreSQL์์ “๋ด๊ฐ ์ ์ํ ์๋ฏธ๋ก ์์ ๋ฐฐํ์ ๋ฝ์ ๊ฑฐ๋ ๋ฐฉ๋ฒ”์ ๋๋ค.
์ฆ, ๋ค๋ฅธ ํธ๋์ญ์
์ด ๊ฐ์ lock ID๋ฅผ ์์ฒญํ๋ฉด block๋์ง๋ง, ์ ํ๋ฆฌ์ผ์ด์
์ด ๊ทธ lock์ ์ค์ํ๋ ๋ฐฉ๋ฒ์
๋๋ค.
Advisory Lock์ PostgreSQL์์ ๊ฐ์ ๋๋ ๋ฝ์ด ์๋ → DB ์์ง์ด ํ/ํ
์ด๋ธ์ ๋ง๋ ๊ฒ์ด ์๋๋ผ, ๊ฐ์ ID๋ก ์ ํ๋ฆฌ์ผ์ด์
์ด ์์ฒญํ ๋๋ง ๋ธ๋กํฉ๋๋ค.
์ฆ, ๋ค๋ฅธ ์ธ์ /ํธ๋์ญ์ ์ด ๊ฐ์ Advisory Lock์ ์์ฒญํ๋ฉด ๊ธฐ๋ค๋ฆฌ๊ฒ ๋ฉ๋๋ค.
ํ์ง๋ง DB์ ์ผ๋ฐ์ ์ธ SELECT (์ฝ๊ธฐ) ์ฟผ๋ฆฌ ์์ฒด๋ฅผ ๋ง์ง๋ ์์ต๋๋ค.
→ SELECT ์์ฒด๊ฐ Advisory Lock ID๋ฅผ ์์ฒญํ์ง ์๋ ํ ์์ ๋กญ๊ฒ ์คํ๋ฉ๋๋ค.
**
Advisory Lock ์ ํ์ฉํ๋ฉด ์์+๋ ์ง ๋จ์๋ก ๋ฝ ๋ฒ์๋ฅผ ์ ํํ ์ ์์ด์
์ค๋ณต ์์ฝ ๋ฐฉ์ง๋ฅผ ์ํ ์ต์ํ์ ์ง๋ ฌํ๋ง ๋ณด์ฅํ๋ฉด์ ๋๋จธ์ง ๋ณ๋ ฌ๋ก ์ฒ๋ฆฌ๋ ์ ์๋ ๊ฒ๋ค์ ๋์์ ์ฒ๋ฆฌํ ์ ์๋๋ก ํ๊ธฐ์ ์ ์ ๋ฌธ์ ์ํฉ์ ๊ฐ์ฅ ์ ํฉํ ํด๊ฒฐ์ฑ
์ด๋ผ๊ณ ๋๊ผ์ต๋๋ค.**
DB ์ ์ฝ์กฐ๊ฑด(UNIQUE + EXCLUDE) ํ์ฉ (PostgreSQL ์ ์ฉ )
PostgreSQL์ ๋์์ฑ ์ ์ด๋ฅผ ์ํด PostgreSQL์ EXCLUDE ์ ์ฝ ์กฐ๊ฑด์ ์ถ๊ฐํ์ฌ
์ด๋ฅผ ํตํด ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ ๋ฒจ์์ ๋ ์ง๊ฐ ๊ฒน์น๋ ์์ฝ์ ์์ฒ์ ์ผ๋ก ์ฐจ๋จํ ์ ์์ต๋๋ค.
- ๊ฐ์ accommodation_id + ๊ฒน์น๋ daterange๋ฅผ ๊ฐ์ง ์์ฝ ์ฝ์
์๋ ์
- PostgreSQL์ด ์๋์ผ๋ก ์๋ฌ ๋ฐ์ (exclusion constraint violation)
- ๋ฐ๋ผ์ SELECT FOR UPDATE ๋ฝ์ด ๋ถํ์
์ด๋ฌ๋ฉด ๋์์ ๋ ํธ๋์ญ์ ์ด ๊ฒน์น๋ ์์ฝ์ ์ฝ์ ํ๋ ค ํ๋ฉด DB๊ฐ ์ง์ Conflict ์๋ฌ(์ ์ฝ์กฐ๊ฑด ์๋ฐ) ์ ๋์ง๋๋ค. ์ค๋ฌด์์๋ ์์ฃผ ์ฌ์ฉ๋๋ ๊ฐ์ฅ “์ ํํ๊ณ ์ฐ์ํ” ๋ฐฉ๋ฒ์ด๋ผ๊ณ ๋ณผ ์ ์์ต๋๋ค.
๋ง์ด๊ทธ๋ ์ด์
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddExcludeConstraintToReservation1761639317235 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
// EXCLUDE ์ ์ฝ ์กฐ๊ฑด ์ถ๊ฐ
// - ๊ฐ์ accommodation_id์ ๋ํด
// - check_in๊ณผ check_out ๊ธฐ๊ฐ์ด ๊ฒน์น๋ ๊ฒฝ์ฐ
// - status๊ฐ 'CANCELLED'๊ฐ ์๋ ์์ฝ์ ๋์ ์กด์ฌ ๋ถ๊ฐ
await queryRunner.query(`
ALTER TABLE reservations
ADD CONSTRAINT reservation_no_overlap_exclude
EXCLUDE USING gist (
accommodation_id WITH =,
daterange(check_in, check_out, '[]') WITH &&
)
WHERE (status != 'CANCELLED')
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
// EXCLUDE ์ ์ฝ ์กฐ๊ฑด ์ ๊ฑฐ
await queryRunner.query(`
ALTER TABLE reservations
DROP CONSTRAINT IF EXISTS reservation_no_overlap_exclude
`);
}
}
์ ์ฝ ์กฐ๊ฑด ์ ์
Exclusion,
} from 'typeorm';
import { User } from './user.entity';
import { Accommodation } from './accommodation.entity';
import { Review } from './review.entity';
export enum ReservationStatus {
PENDING = 'PENDING',
CONFIRMED = 'CONFIRMED',
CANCELLED = 'CANCELLED',
}
@Entity('reservations')
@Exclusion(
`USING gist (accommodation_id WITH =, daterange(check_in, check_out, '[]') WITH &&) WHERE (status != 'CANCELLED')`,
)
export class Reservation {
@PrimaryGeneratedColumn()
id: number;
@Column({ type: 'int' })
user_id: number;
@Column({ type: 'int' })
accommodation_id: number;
@Column({ type: 'date' })