๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

ํ”„๋กœ์ ํŠธ/Airbnb Clone

[๋™์‹œ์„ฑ ์ œ์–ด] ์œ ๋‹ˆํฌ ์ œ์•ฝ์กฐ๊ฑด์„ ํ™œ์šฉํ•ด 1 ๊ฑด์˜ ์˜ˆ์•ฝ๋งŒ ์ €์žฅ

โญ์š”๊ตฌ์‚ฌํ•ญ

์ฝ”๋“œ์Šค์ฟผ๋“œ์˜ Airbnb ํ”„๋กœ์ ํŠธ ์ค‘

๊ฐ™์€ ์ˆ™์†Œ์— ์‚ฌ๋žŒ A ์™€ ์‚ฌ๋žŒ B ๊ฐ€ ๋™์‹œ์— ์˜ˆ์•ฝ์„ ์š”์ฒญํ•  ๊ฒฝ์šฐ, ํ•œ ๊ฑด์˜ ์˜ˆ์•ฝ๋งŒ ์„ฑ๊ณตํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

(๊ฐ™์€ ์ˆ™์†Œ๋ฅผ ์‚ฌ๋žŒ A์™€ ์‚ฌ๋žŒ B๊ฐ€ ๋™์‹œ์— ์‚ฌ์šฉํ•˜๋ฉด ์•ˆ๋˜๋‹ˆ๊นŒ!)

โญ๊ณ ๋ฏผ

๊ทธ๋ž˜์„œ, ๋™์‹œ์— ์˜ˆ์•ฝ ์š”์ฒญ์„ ๋ง‰๊ธฐ ์œ„ํ•ด

  • ๋น„๊ด€์  ๋ฝ
  • ๋‚™๊ด€์  ๋ฝ
  • DB์˜ ์œ ๋‹ˆํฌ ์ œ์•ฝ์กฐ๊ฑด

์„ ๊ณ ๋ฏผํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

โญํ•ด๊ฒฐ ๊ณผ์ •

์˜ˆ์•ฝ์ด ์ €์žฅ๋˜๋Š” ์ฝ”๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

@Transactional
public BookingResponse create(BookingSaveRequest request) {

    // ์š”์ฒญํ•œ ์˜ˆ์•ฝ์ด ๊ธฐ์กด ์˜ˆ์•ฝ์˜ ์ผ์ •๊ณผ ์ค‘๋ณต๋˜๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๋กœ์ง
    Long bookedStayCount = bookingRepository.countBookedStay(request.getStayId(), request.getCheckIn(), request.getCheckOut());
    if (bookedStayCount > 0) {
        throw new IllegalArgumentException("์˜ˆ์•ฝ ๋ถˆ๊ฐ€์ž…๋‹ˆ๋‹ค.");
    }

    // ์˜ˆ์•ฝ์„ ์š”์ฒญํ•œ ์‚ฌ๋žŒ์ด ํšŒ์›์ธ์ง€ ํ™•์ธ
    Member member = memberRepository.findById(request.getMemberId())
                .orElseThrow();
    // ์ˆ™์†Œ๊ฐ€ ์˜ˆ์•ฝ ์ธ์›์„ ์ˆ˜์šฉํ•  ์ˆ˜ ์žˆ๊ณ , ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ์ง€ ํ™•์ธ
    Stay findStay = stayRepository.findById(request.getStayId())
                .orElseThrow(() -> new IllegalArgumentException("์ˆ™์†Œ ๋ชป์ฐพ์•„์š”"));
    findStay.validateExceedGuest(request.getGuestCount());
    findStay.validateOpenStatus();

    Booking entity = Booking.builder()
            ...(์ค‘๋žต)
            .build();

    // ์˜ˆ์•ฝ์„ DB์— ์ €์žฅ
    try {
        Booking saved = bookingRepository.save(entity);
        return BookingResponse.of(saved);
    } catch (PersistenceException error) {
        ์˜ˆ์™ธ์ฒ˜๋ฆฌ
    }
}

 

์œ„ ์ฝ”๋“œ์—์„œ DB์— ์ ‘๊ทผํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋กœ์ง์€ 

1. Long bookedStayCount = bookingRepository.countBookedStay
2. Member member = memberRepository.findById
3. Stay findStay = stayRepository.findById

 

์ด๋ ‡๊ฒŒ ์ด 3๊ฐ€์ง€๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜, ์ด 3๊ฐ€์ง€ ๊ฒฝ์šฐ ๋‹ค DB์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒ๋งŒ ํ•  ๋ฟ, update ๋ฅผ ํ•˜๊ฑฐ๋‚˜ delete ๋ฅผ ํ•˜์ง„ ์•Š์Šต๋‹ˆ๋‹ค.
๋‚™๊ด€์  ๋ฝ๊ณผ ๋น„๊ด€์  ๋ฝ์„ ์–ธ์ œ ์‚ฌ์šฉํ• ์ง€๋ฅผ ๊ณต๋ถ€ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์—,
ํ•œ ํŠธ๋žœ์žญ์…˜์—์„œ DB์— ์ ‘๊ทผํ•ด์„œ ์กฐํšŒ๋งŒ ํ•˜๋Š” ์ œ ๊ฒฝ์šฐ์—๋Š” ๋ฝ์ด ์ ํ•ฉํ•˜์ง€ ์•Š๋‹ค๊ณ  ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.

๋˜ํ•œ, bookingRepository.save(entity) ๋„ DB์— INSERT INTO ์ฟผ๋ฆฌ๊ฐ€ ๋‚ ๋ผ๊ฐˆ ๋ฟ,

SELECT ์ฟผ๋ฆฌ๋Š” ์‹คํ–‰๋˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ๋น„๊ด€์  ๋ฝ๊ณผ ๋‚™๊ด€์  ๋ฝ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

 

๋ฌผ๋ก , ๊ฒฉ๋ฆฌ์ˆ˜์ค€ SERIALIZABLE ๋กœ ์„ค์ •ํ•ด์„œ ํŠธ๋žœ์žญ์…˜์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์‹คํ–‰์‹œํ‚ฌ ์ˆœ ์žˆ์ง€๋งŒ

์—์–ด๋น„์•ค๋น„ ์„œ๋น„์Šค ํŠน์„ฑ ์ƒ ์ฃผ๋กœ ์ˆ™์†Œ ์กฐํšŒ์— ๋งŽ์€ DB ์ปค๋„ฅ์…˜์ด ์‚ฌ์šฉ๋  ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์—

ํŠธ๋žœ์žญ์…˜์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์‹คํ–‰ํ•˜์—ฌ ๋™์‹œ ์ฒ˜๋ฆฌ๋Ÿ‰์„ ๋‚ฎ์ถ”๋Š” ๊ฒƒ์€ ์ ํ•ฉํ•˜์ง€ ์•Š๋‹ค๊ณ  ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค. 


๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— DB์˜ ์œ ๋‹ˆํฌ ์ œ์•ฝ ์กฐ๊ฑด์„ ์‚ฌ์šฉํ•˜์—ฌ, ๋™์‹œ ์˜ˆ์•ฝ ์š”์ฒญ์„ ๋ง‰๊ณ ์ž ํ•˜์˜€์Šต๋‹ˆ๋‹ค.


์œ ๋‹ˆํฌ ์ œ์•ฝ ์กฐ๊ฑด์„ ์˜ˆ์•ฝ ์—”ํ‹ฐํ‹ฐ์˜ ์ˆ™์†ŒID, ์ฒดํฌ์ธ, ์ฒดํฌ์•„์›ƒ๋กœ ์„ค์ •ํ•˜์—ฌ

๊ฐ™์€ ๋‚ ์งœ์— ๊ฐ™์€ ์ˆ™์†Œ์— 2๊ฐœ ์ด์ƒ์˜ ์˜ˆ์•ฝ์ด ์ƒ๊ธฐ์ง€ ์•Š๋„๋ก ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

/* ์˜ˆ์•ฝ ์—”ํ‹ฐํ‹ฐ */
@Table(name = "BOOKING",
uniqueConstraints = {
        @UniqueConstraint(columnNames = {"STAY_ID", "CHECK_IN", "CHECK_OUT"})}
)
@Entity
public class Booking extends BaseTimeEntity {


์ด์ œ, ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋™์‹œ ์˜ˆ์•ฝ ์š”์ฒญ์ด ๋ง‰์•„์ง€๋Š”์ง€ ํ™•์ธํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.


ํ…Œ์ŠคํŠธ๋Š” ์˜ˆ์•ฝ Request ๋ฅผ ๋งŒ๋“ค์–ด์„œ ๋ฉ€ํ‹ฐ ์“ฐ๋ ˆ๋“œ ํ™˜๊ฒฝ์—์„œ ๋™์‹œ์— request ๋ฅผ ๋ณด๋ƒˆ์„ ๋•Œ,

DB์— ๋ช‡ ๊ฑด์ด ์ €์žฅ๋˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.

@Test
void ๋™์‹œ_์˜ˆ์•ฝ_์š”์ฒญ์ด_์˜ค๋”๋ผ๋„_1๊ฑด๋งŒ_์˜ˆ์•ฝ๋œ๋‹ค() throws InterruptedException {
    // ์˜ˆ์•ฝ ์š”์ฒญ ์ƒ์„ฑ
    LocalDateTime checkIn = LocalDateTime.of(LocalDate.of(2024, 11, 1), LocalTime.of(15, 0, 0));
    LocalDateTime checkOut = LocalDateTime.of(LocalDate.of(2024, 11, 2), LocalTime.of(12, 0, 0));
    // ๋ฉค๋ฒ„ID ๊ฐ€ 1์ธ ์‚ฌ๋žŒ์ด ์ˆ™์†ŒID๊ฐ€ 1์ธ ๊ณณ์— ์˜ˆ์•ฝ ์š”์ฒญ
    BookingSaveRequest request = new BookingSaveRequest(1L, 1L, checkIn, checkOut, 2);

    AtomicInteger successCount = new AtomicInteger();
    AtomicInteger failCount = new AtomicInteger();

    long originCount = bookingRepository.count();
    int requestCount = 30;

    ExecutorService executorService = Executors.newFixedThreadPool(requestCount);
    CountDownLatch latch = new CountDownLatch(requestCount);

    for (int i = 0; i < requestCount; i++) {
        executorService.submit(() -> {
            try {
                bookingService.create(request);
                successCount.incrementAndGet();
            } catch (Exception e) {
                failCount.incrementAndGet();
            } finally {
                latch.countDown();
            }
        });
    }
    latch.await();

    System.out.println("successCount = " + successCount);
    System.out.println("failCount = " + failCount);

    long changeCount = bookingRepository.count();
    Assertions.assertThat(originCount + 1).isEqualTo(changeCount);
}

ํ…Œ์Šคํฌ ์ฝ”๋“œ์— ๋Œ€ํ•œ ์„ค๋ช… -> ๋งํฌ

ํ…Œ์ŠคํŠธ์˜ ๊ฒฐ๊ณผ๋กœ, ์„ฑ๊ณตํ•œ ๊ฐœ์ˆ˜๋Š” 1๊ฐœ, ์‹คํŒจํ•œ ๊ฐœ์ˆ˜๋Š” 29๊ฐœ๊ฐ€ ๋‚˜์˜ค๊ฒŒ ๋˜์—ˆ๊ณ ,


์œ ๋‹ˆํฌ ์ œ์•ฝ ์กฐ๊ฑด์— ์˜ํ•ด DB์— ์‚ฝ์ž…๋˜์ง€ ๋ชปํ•œ ํŠธ๋žœ์žญ์…˜์€ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

2024-07-01T12:34:40.659+09:00 ERROR 24864 --- [clone] [pool-2-thread-2] o.h.engine.jdbc.spi.SqlExceptionHelper   : Duplicate entry '1-2024-11-01 15:00:00.000000-2024-11-02 12:00:00.000000' for key 'BOOKING.UKl92471cusp79py2jcwnxaph0e'


DB์˜ ์˜ˆ์•ฝ ํ…Œ์ด๋ธ”์—๋„ ๋‹จ 1๊ฑด๋งŒ ์ €์žฅ๋จ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.


์ฐธ๊ณ  ๋งํฌ

ํ…Œ์ฝ”๋ธ” - ๋™์‹œ ์˜ˆ๋งค ์‹œ์Šคํ…œ