[๐Ÿ”ฅTroubleShooting - TicToc๐Ÿ”ฅ] ๋ˆ์ด ๊ฑธ๋ฆฐ ๋ฌธ์ œโ€ผ๏ธ 1๋ช…๋งŒ ์ž…์ฐฐ ์„ฑ๊ณต โœ…, 4999๋ช… ์ž…์ฐฐ ์‹คํŒจ โŒ

._mungยท2025๋…„ 3์›” 8์ผ
0

TicToc

๋ชฉ๋ก ๋ณด๊ธฐ
5/6

๐Ÿ“Œ ํ”„๋กœ์ ํŠธ ์†Œ๊ฐœ

ํŒ€์› : ๊ฐœ์ธ ํ”„๋กœ์ ํŠธ
๊ธฐ๊ฐ„ : 2025.01 ~ ์ง„ํ–‰ ์ค‘
๋งํฌ : https://github.com/M-ung/TicToc_Server
์„œ๋น„์Šค ๋‚ด์šฉ : ๋‹น์‹ ์˜ ์‹œ๊ฐ„์— ๊ฐ€์น˜๋ฅผ ๋งค๊ธฐ๋‹ค, ์‹œ๊ฐ„ ๊ฑฐ๋ž˜ ๊ฒฝ๋งค ํ”Œ๋žซํผ


๐Ÿ”ฅTroubleShooting๐Ÿ”ฅ

Problems

์šฐ๋ฆฌ ์„œ๋น„์Šค "TicToc"์˜ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ํ•ต์‹ฌ ๊ธฐ๋Šฅ์— ๋Œ€ํ•ด์„œ ๊ณ ๋ฏผํ•  ์‹œ๊ฐ„์ด๋‹ค. ๋ฐ”๋กœ "์ž…์ฐฐ ๊ธฐ๋Šฅ" ์ด๋‹ค.
๋‹จ์ˆœํžˆ Bid ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ €์žฅํ•ด์„œ ์ž…์ฐฐ์„ ํ•˜๋Š” ๋ฐฉ์‹์€ ๋™์‹œ์„ฑ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.

์—ฌ๊ธฐ์„œ ๋™์‹œ์„ฑ ๋ฌธ์ œ๋ž€?
๋™์‹œ์„ฑ ๋ฌธ์ œ๋Š” ์—ฌ๋Ÿฌ ๊ฐœ์˜ ํŠธ๋žœ์žญ์…˜์ด ๋™์ผํ•œ ๋ฐ์ดํ„ฐ์— ๋™์‹œ ์ ‘๊ทผํ•˜์—ฌ ์˜ˆ๊ธฐ์น˜ ์•Š์€ ๊ฒฐ๊ณผ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ๋ฅผ ์˜๋ฏธํ•œ๋‹ค.
์˜ˆ๋ฅผ ๋“ค์–ด ์šฐ๋ฆฌ TicToc ์„œ๋น„์Šค์˜ ์ž…์ฐฐ ๊ธฐ๋Šฅ์€ ์—ฌ๋Ÿฌ ์‚ฌ์šฉ์ž๊ฐ€ ๊ฐ™์€ ๊ฒฝ๋งค์— ๋™์‹œ์— ์ž…์ฐฐํ•  ๋•Œ, ์ž…์ฐฐ ๊ฒฐ๊ณผ๊ฐ€ ์˜ˆ์ƒ๊ณผ ๋‹ค๋ฅด๊ฒŒ ๋ฐ˜์˜๋˜๊ฑฐ๋‚˜ ๋ฐ์ดํ„ฐ ์ถฉ๋Œ์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.

์šฐ๋ฆฌ ์„œ๋น„์Šค๋Š” ๋ˆ์œผ๋กœ ์ž…์ฐฐ์„ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ •ํ™•ํžˆ ์ž…์ฐฐ ๊ฒฐ๊ณผ๋ฅผ ์•Œ๋ ค์•ผ ํ•œ๋‹ค. ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— "์ž…์ฐฐ ๊ธฐ๋Šฅ"์„ ๊ตฌํ˜„ํ•˜๋ฉด์„œ ์˜ค๋žœ ๊ณ ๋ฏผ์„ ํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.

๊ถ๊ธˆ์ ์œผ๋กœ ๋ฌธ์ œ๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

1. ๋™์ผํ•œ ๋ฐ์ดํ„ฐ์— ๋™์‹œ ์ ‘๊ทผํ•˜๋Š” ๋™์‹œ์„ฑ ๋ฌธ์ œ ํ•ด๊ฒฐ
2. ์—ฌ๋Ÿฌ ์ ‘๊ทผ ์ค‘์— ๋”ฑ ํ•˜๋‚˜์˜ ์ ‘๊ทผ๋งŒ ๊ฐ€์ ธ์˜ค๊ณ  ๋‚˜๋จธ์ง€๋Š” ๋กค๋ฐฑ


How

์œ„ ๋ฌธ์ œ๋“ค์„ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ์›์ž์  ์—ฐ์‚ฐ๊ณผ ๋‚™๊ด€์  ๋ฝ์„ ํ˜ผํ•ฉํ•ด์„œ ํ™œ์šฉํ•ด ๋ณด์•˜๋‹ค.

์—ฌ๊ธฐ์„œ ์›์ž์  ์—ฐ์‚ฐ์ด๋ž€?
์›์ž์  ์—ฐ์‚ฐ์€ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์—ฐ์‚ฐ์ด ํ•˜๋‚˜์˜ ๋‹จ์œ„๋กœ ์‹คํ–‰๋˜์–ด์„œ ์ค‘๊ฐ„์— ๋‹ค๋ฅธ ์—ฐ์‚ฐ์ด ๋ผ์–ด๋“ค ์ˆ˜ ์—†๋„๋ก ๋ณด์žฅํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค.
์ฆ‰, ์ž…์ฐฐ ์‹œ ๊ฒฝ๋งค ๊ฐ€๊ฒฉ๋ณด๋‹ค ๋†’์€ ๊ฒฝ์šฐ์—๋งŒ ์ž…์ฐฐ์„ ์ง„ํ–‰ํ•˜๋„๋ก ์›์ž์ ์ธ UPDATE SQL์„ ํ™œ์šฉํ•˜์—ฌ ๋™์‹œ์„ฑ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋ ค๊ณ  ํ•œ๋‹ค.

์—ฌ๊ธฐ์„œ ๋‚™๊ด€์ ์ด๋ž€?
๋‚™๊ด€์  ๋ฝ์€ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ •ํ•  ๋•Œ, ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋ณ€๊ฒฝ์ด ๋ฐœ์ƒํ–ˆ๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค.
๋ณดํ†ต ์—”ํ‹ฐํ‹ฐ์— ๋ฒ„์ „ ํ•„๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๋ณ€๊ฒฝ์ด ๋ฐœ์ƒํ–ˆ์„ ๊ฒฝ์šฐ ์˜ˆ์™ธ๋ฅผ ๋˜์ง€๊ณ , ๋‹ค์‹œ ์‹œ๋„ํ•˜๋„๋ก ํ•œ๋‹ค.
์šฐ๋ฆฌ ์„œ๋น„์Šค์˜ ๊ฒฝ์šฐ ๋‚™๊ด€์  ๋ฝ์„ ์ง์ ‘ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ , ์›์ž์  ์—ฐ์‚ฐ ํ›„ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋ฐฉ์‹์œผ๋กœ ์ ์šฉํ•˜๋ ค๊ณ  ํ•œ๋‹ค.


Process

์•„๋ž˜๋Š” ์›์ž์  ์—ฐ์‚ฐ์„ ์ ์šฉํ•œ updateBidIfHigher ์ฝ”๋“œ์ด๋‹ค. ์ด๋ฅผ ์ž…์ฐฐํ•  ๋•Œ ๋ฐ”๋กœ ์‹คํ–‰ํ•˜์—ฌ ํ˜„์žฌ currentPrice๊ฐ€ ์ž…์ฐฐ ๊ธˆ์•ก๋ณด๋‹ค ๋†’์€์ง€ ํ™•์ธํ•˜๊ณ  ๋งŒ์•ฝ ์ด๋ณด๋‹ค ๋‚ฎ์„ ๋•Œ 0์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋กœ์ง์ด๋‹ค.

๐Ÿ“ Bid.java

@Getter
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "bid")
public class Bid {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private Long auctionId;
    private Long bidderId;
    private Integer bidPrice;
    @Enumerated(EnumType.STRING)
    private BidStatus status;
}

๐Ÿ“ AuctionRepository.java

public interface AuctionRepository extends JpaRepository<Auction, Long>, AuctionRepositoryCustom {
    @Modifying(clearAutomatically = true, flushAutomatically = true)
    @Query("UPDATE Auction a SET a.currentPrice = :price WHERE a.id = :auctionId AND a.currentPrice < :price")
    int updateBidIfHigher(@Param("auctionId") Long auctionId, @Param("price") Integer price);
}

0์„ ๋ฐ˜ํ™˜ํ•˜๊ฒŒ ๋œ๋‹ค๋ฉด BidException๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„ํ–ˆ๋‹ค. ๋˜ 0์ด ์•„๋‹ˆ๋ผ๋ฉด ์ •์ƒ์ ์œผ๋กœ ์ž…์ฐฐ ๊ธฐ๋Šฅ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„ํ–ˆ๋‹ค.

@Service
@Transactional
@RequiredArgsConstructor
public class BidCommandService implements BidCommandUseCase {
    private final AuctionRepositoryPort auctionRepositoryPort;
    private final BidRepositoryPort bidRepositoryPort;

    @Override
    public void bid(final Long userId, BidUseCaseReqDTO.Bid requestDTO) {
        if(auctionRepositoryPort.updateBidIfHigher(requestDTO) == 0) {
            throw new BidException(BID_FAIL);
        }
        var findAuction = auctionRepositoryPort.findAuctionById(requestDTO.auctionId());
        bidRepositoryPort.checkBeforeBid(findAuction);
        findAuction.start(userId);
        bidRepositoryPort.saveBid(Bid.of(userId, requestDTO));
    }
}

์ด์ œ ์ •์ƒ์ ์œผ๋กœ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ๋Œ๋ ค๋ณด๊ฒ ๋‹ค.
ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์˜ ์‹œ๋‚˜๋ฆฌ์˜ค๋Š” 5000๋ช…์ด ๋™์‹œ์— ์ž…์ฐฐ์„ ์‹œ๋„ํ–ˆ์„ ๋•Œ "1๋ช…๋งŒ ์ž…์ฐฐ ์„ฑ๊ณต โœ…, 4999๋ช… ์ž…์ฐฐ ์‹คํŒจ โŒ" ํ•ด์•ผ ํ•œ๋‹ค.

๐Ÿ“ ์ฒซ ๋ฒˆ์งธ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ

    @Test
    @DisplayName("์ž…์ฐฐ ์‹œ ๋™์‹œ์„ฑ ์ด์Šˆ ๋ฐœ์ƒ์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ")
    public void ์ž…์ฐฐ_์‹œ_๋™์‹œ์„ฑ_์ด์Šˆ_๋ฐœ์ƒ์—_๋Œ€ํ•œ_ํ…Œ์ŠคํŠธ() throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(NUM_USERS);
        ExecutorService executorService = Executors.newFixedThreadPool(NUM_USERS);

        AtomicInteger successUsers = new AtomicInteger(0);
        AtomicInteger failedUsers = new AtomicInteger(0);

        for (int i = 1; i <= NUM_USERS; i++) {
            final Long userId = (long) i;
            executorService.submit(() -> {
                try {
                    bidCommandService.bid(userId, new BidUseCaseReqDTO.Bid(auction.getId(), BID_PRICE));
                    successUsers.incrementAndGet();
                } catch (BidException e) {
                    failedUsers.incrementAndGet();
                    assertThat(e.getMessage()).isEqualTo(ErrorCode.BID_FAIL.getMessage());
                } finally {
                    latch.countDown();
                }
            });
        }

        latch.await();

        Auction updatedAuction = auctionRepositoryPort.findAuctionById(auction.getId());
        assertThat(updatedAuction.getCurrentPrice()).isEqualTo(BID_PRICE);
        assertThat(successUsers.get()).isEqualTo(1);
        assertThat(failedUsers.get()).isEqualTo(NUM_USERS-1);
        executorService.shutdown();
    }

ํ…Œ์ŠคํŠธ๋ฅผ ๋Œ๋ ค๋ณธ ๊ฒฐ๊ณผ ํ…Œ์ŠคํŠธ๋ฅผ ์ •์ƒ์ ์œผ๋กœ ํ†ต๊ณผํ•˜์˜€๊ธฐ์— ๋™์‹œ์„ฑ ๋ฌธ์ œ๋ฅผ ์ž˜ ํ•ด๊ฒฐํ–ˆ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๋‹ค.

ํ•˜.์ง€.๋งŒ

์ด๋Š” ์ฐฉ๊ฐ์ด์—ˆ๋‹ค... ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ €๋ ‡๊ฒŒ ์ž‘์„ฑํ•ด์„œ ์ •์ƒ์ ์œผ๋กœ ์ž…์ฐฐ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ–ˆ๋‹ค๊ณ  ์ฐฉ๊ฐํ–ˆ๋‹ค.

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์•„๋ž˜์ฒ˜๋Ÿผ ๋ฐ”๊ฟ”๋ณด๊ฒ ๋‹ค.

๐Ÿ“ ๋‘ ๋ฒˆ์งธ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ

    @Test
    @DisplayName("์ž…์ฐฐ ์‹œ ๋™์‹œ์„ฑ ์ด์Šˆ ๋ฐœ์ƒ์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ")
    public void ์ž…์ฐฐ_์‹œ_๋™์‹œ์„ฑ_์ด์Šˆ_๋ฐœ์ƒ์—_๋Œ€ํ•œ_ํ…Œ์ŠคํŠธ() throws InterruptedException {
        CountDownLatch readyLatch = new CountDownLatch(NUM_USERS);
        CountDownLatch startLatch = new CountDownLatch(1);
        CountDownLatch doneLatch = new CountDownLatch(NUM_USERS);
        ExecutorService executorService = Executors.newFixedThreadPool(NUM_USERS);

        AtomicInteger successUsers = new AtomicInteger(0);
        AtomicInteger failedUsers = new AtomicInteger(0);
        AtomicReference<Long> successUserId = new AtomicReference<>();
        AtomicReference<Integer> successBidPrice = new AtomicReference<>();

        for (int i = 1; i <= NUM_USERS; i++) {
            final Long userId = (long) i;
            executorService.submit(() -> {
                try {
                    readyLatch.countDown();
                    startLatch.await();
                    int bidPrice = (int) (BID_PRICE * userId);
                    bidCommandUseCase.bid(userId, new BidUseCaseReqDTO.Bid(auction.getId(), bidPrice));
                    successUserId.set(userId);
                    successBidPrice.set(bidPrice);
                    successUsers.incrementAndGet();
                } catch (Exception e) {
                    failedUsers.incrementAndGet();
                    assertThat(e.getMessage()).isEqualTo(ErrorCode.BID_FAIL.getMessage());
                } finally {
                    doneLatch.countDown();
                }
            });
        }

        readyLatch.await();
        startLatch.countDown();
        doneLatch.await();
        executorService.shutdown();

        Auction findAuction = auctionRepositoryPort.findAuctionById(auction.getId());
        Bid findBid = bidRepositoryPort.findBidByAuctionId(findAuction.getId());
        assertThat(findAuction.getCurrentPrice()).isEqualTo(successBidPrice.get());
        assertThat(findBid.getBidderId()).isEqualTo(successUserId.get());
        assertThat(successUsers.get()).isEqualTo(1);
        assertThat(failedUsers.get()).isEqualTo(NUM_USERS-1);
        executorService.shutdown();
    }

์ด๋Š” ์ž…์ฐฐ ๊ธˆ์•ก์ด ์ ์  ์ฆ๊ฐ€ํ•˜๋„๋ก ๋ฐ”๊พธ์—ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ์ž‘์„ฑํ–ˆ์„ ๋•Œ ํ…Œ์ŠคํŠธ๋Š” ์–ด๋–ค ๊ฒฐ๊ณผ๊ฐ€ ๋‚˜์˜ฌ๊นŒ?


์ด๊ฒŒ ๋ฌด์Šจ ์ผ์ผ๊นŒ..?

์ด๋Š” ๋‚ด ์™„์ „ํ•œ ์ฐฉ๊ฐ์ด์—ˆ๋‹ค. ์›์ž์  ์—ฐ์‚ฐ์„ ์ž˜ ์ ์šฉํ–ˆ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๋Š”๋ฐ, ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž˜๋ชป ์ž‘์„ฑํ•ด์„œ ์„ฑ๊ณตํ–ˆ๋‹ค๊ณ  ์˜คํ•ดํ–ˆ๋‹ค. ๋‹จ์ˆœํžˆ currentPrice๋ณด๋‹ค ๋†’์ง€ ์•Š๋‹ค๋ฉด 0์„ ๋ฐ˜ํ™˜ํ•˜๊ฒŒ ์ฟผ๋ฆฌ๋ฌธ์„ ์ž‘์„ฑํ–ˆ๊ณ , ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ๋˜ํ•œ ์ฒ˜์Œ์— BID_PRICE๋ฅผ ํ•˜๋‚˜๋กœ ๊ณ ์ •ํ•ด ๋†จ๊ธฐ์— ํ†ต๊ณผํ•  ์ˆ˜ ์žˆ์—ˆ๋˜ ๊ฑฐ๋‹ค.

๐Ÿ“ ์ฒซ ๋ฒˆ์งธ ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค

์ฒซ ๋ฒˆ์งธ ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค

currentPrice = 1000
BID_PRICE = 1500

1์ฐจ Bid : 1000(currentPrice) < 1500(BID_PRICE) -> Bid ์ •์ƒ์ ์œผ๋กœ ์ˆ˜ํ–‰

currentPrice = 1500
BID_PRICE = 1500

2์ฐจ Bid : 1500(currentPrice) < 1500(BID_PRICE) -> Bid ์‹คํŒจ

currentPrice = 1500
BID_PRICE = 1500

3์ฐจ Bid : 1500(currentPrice) < 1500(BID_PRICE) -> Bid ์‹คํŒจ

.
.
.
.

์œ„ ๊ฐ™์€ ์‹œ๋‚˜๋ฆฌ์˜ค์ด๊ธฐ ๋•Œ๋ฌธ์— ํ†ต๊ณผํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

์•„๋ž˜๋Š” ๋‘ ๋ฒˆ์งธ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์‹œ๋‚˜๋ฆฌ์˜ค์ด๋‹ค.

๐Ÿ“ ๋‘ ๋ฒˆ์งธ ์‹œ๋‚˜๋ฆฌ์˜ค

๋‘ ๋ฒˆ์งธ ์‹œ๋‚˜๋ฆฌ์˜ค

currentPrice = 1000
BID_PRICE = 1500

1์ฐจ Bid : 1000(currentPrice) < 1500(BID_PRICE) -> Bid ์ •์ƒ์ ์œผ๋กœ ์ˆ˜ํ–‰

currentPrice = 1500
BID_PRICE = 3000

2์ฐจ Bid : 1500(currentPrice) < 3000(BID_PRICE) -> Bid ์ •์ƒ์ ์œผ๋กœ ์ˆ˜ํ–‰

currentPrice = 3000
BID_PRICE = 4500

3์ฐจ Bid : 3000(currentPrice) < 4500(BID_PRICE) -> Bid ์ •์ƒ์ ์œผ๋กœ ์ˆ˜ํ–‰

.
.
.
.

์œ„ ๊ฐ™์€ ์‹œ๋‚˜๋ฆฌ์˜ค์ด๊ธฐ ๋•Œ๋ฌธ์— ๋ชจ๋“  ์ž…์ฐฐ์ด ์„ฑ๊ณตํ•˜๊ณ  ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋Š” ์‹คํŒจํ•˜๊ฒŒ ๋œ ๊ฒƒ์ด๋‹ค.

๊ทธ๋Ÿผ.. ๋‹ค์‹œ ์ƒˆ๋กญ๊ฒŒ ์„ค๊ณ„๋ฅผ ํ•ด์•ผ ํ•  ๊ฒƒ ๊ฐ™๋‹ค.

์œ„ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ "๋ถ„์‚ฐ ๋ฝ" ์„ ํ™œ์šฉํ•ด์„œ ๋‹จ ํ•˜๋‚˜์˜ ์ž…์ฐฐ๋งŒ ์„ฑ๊ณตํ•  ์ˆ˜ ์žˆ๊ฒŒ ๊ตฌํ˜„ํ•ด ๋ณด์•˜๋‹ค.

๋˜ ๋‹จ์ˆœํžˆ ๋ถ„์‚ฐ ๋ฝ๋งŒ ์ ์šฉํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ๋ฝ์„ ํš๋“ํ–ˆ์„ ๋•Œ, ์›์ž์  ์—ฐ์‚ฐ์„ ํ†ตํ•ด ๋ฐ”๋กœ currentPrice๋ฅผ DB์— ์—…๋ฐ์ดํŠธํ•ด ์ฃผ์—ˆ๋‹ค. ๊ทธ ํ›„ ์ž…์ฐฐ ๋กœ์ง์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ–ˆ๊ณ , ๋ฐ”๋กœ ๋ฝ์„ ํ•ด์ œํ–ˆ์„ ๋•Œ ๋Œ€๊ธฐ ์ค‘์ด์—ˆ๋˜ ์ž…์ฐฐ์ด ์„ฑ๊ณต๋  ๊ฒฝ์šฐ๋ฅผ ๋Œ€๋น„ํ•ด Thread.sleep(RedisConstants.LOCK_LEASE_TIME * 1000); ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๋”œ๋ ˆ์ด ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ–ˆ๋‹ค.

์ฝ”๋“œ๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

๐Ÿ“ BidCommandService.java

@Service
@Transactional
@RequiredArgsConstructor
public class BidCommandService implements BidCommandUseCase {
    private final AuctionRepositoryPort auctionRepositoryPort;
    private final BidRepositoryPort bidRepositoryPort;
    private final RedissonClient redissonClient;

    @Override
    @Transactional
    public void bid(final Long userId, BidUseCaseReqDTO.Bid requestDTO) {
        String lockKey = RedisConstants.LOCK_PREFIX + requestDTO.auctionId();
        RLock lock = redissonClient.getLock(lockKey);
        try {
            if (!lock.tryLock(Math.round(RedisConstants.LOCK_WAIT_TIME), RedisConstants.LOCK_LEASE_TIME, TimeUnit.SECONDS)) {
                throw new BidException(BID_FAIL);
            }
            var findAuction = auctionRepositoryPort.findAuctionById(requestDTO.auctionId());

            bidRepositoryPort.checkBeforeBid(findAuction);
            findAuction.startBid(userId);

            Integer beforePrice = findAuction.getCurrentPrice();
            int updatedRows = auctionRepositoryPort.updateBidIfHigher(requestDTO);
            if (updatedRows == 0) {
                throw new BidException(BID_FAIL);
            }
            bidRepositoryPort.saveBid(Bid.of(userId, requestDTO, beforePrice));
            Thread.sleep(RedisConstants.LOCK_LEASE_TIME * 1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new BidException(BID_FAIL);
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

๋˜ "์ž…์ฐฐ ๊ธฐ๋Šฅ" ์€ ์ค‘์š”ํ•œ ๊ธฐ๋Šฅ์ด๊ธฐ์— Bid ํ…Œ์ด๋ธ”์— beforePrice ๋ผ๋Š” ์ž…์ฐฐ ์„ฑ๊ณต ์ „ ๊ฐ€๊ฒฉ์„ ์ €์žฅํ•˜๊ณ , @Table(name = "bid", uniqueConstraints = @UniqueConstraint(columnNames = {"auctionId", "beforePrice"})) ์œ ๋‹ˆํฌ ์ œ์•ฝ ์กฐ๊ฑด์„ ์ถ”๊ฐ€ํ–ˆ๋‹ค.

์œ ๋‹ˆํฌ ์ œ์•ฝ ์กฐ๊ฑด์œผ๋กœ ์ธํ•ด Bid ํ…Œ์ด๋ธ”์—๋Š” ํ•˜๋‚˜์˜ auctionId์—๋Š” ์ค‘๋ณต๋˜๋Š” beforePrice๊ฐ€ ์˜ฌ ์ˆ˜ ์—†๊ธฐ์— ๊ฐ™์€ ๊ฒฝ๋งค๊ฐ€์— ์ž…์ฐฐ์„ ์„ฑ๊ณตํ•˜๋Š” Bid๊ฐ€ 2๊ฐœ ์ด์ƒ ์กด์žฌํ•  ์ˆ˜ ์—†๊ฒŒ ๋œ๋‹ค.

์œ„ ์ฝ”๋“œ์˜ ์‹œ๋‚˜๋ฆฌ์˜ค๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค.


Result

๋ฌธ์ œ ํ•ด๊ฒฐ๋กœ ์•„๋ž˜์™€ ๊ฐ™์€ ๊ฒฐ๊ณผ๋ฅผ ์–ป์—ˆ๋‹ค.
1. โ€œ์›์ž์  ์—ฐ์‚ฐ + ๋ถ„์‚ฐ ๋ฝ + ๋”œ๋ ˆ์ด + ์œ ๋‹ˆํฌ ์ œ์•ฝ ์กฐ๊ฑดโ€ โ†’ ๋™์‹œ์„ฑ ๋ฌธ์ œ ํ•ด๊ฒฐ.
2. 5,000๊ฑด ๊ธฐ์ค€, ๋™์‹œ ์ž…์ฐฐ ์š”์ฒญ ์ค‘ 1๊ฑด ์„ฑ๊ณต, 4,999๊ฑด ์‹คํŒจ.
3. ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ์„ 100% ๋ณด์žฅ.
4. ์„œ๋น„์Šค์˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ โ€œ์ž…์ฐฐโ€ ์„ ๋งค์šฐ ์•ˆ์ „ํ•˜๊ฒŒ ์šด์˜ ๊ฐ€๋Šฅ.

์ตœ์ข…์ ์œผ๋กœ "์›์ž์  ์—ฐ์‚ฐ + ๋ถ„์‚ฐ ๋ฝ + ๋”œ๋ ˆ์ด + ์œ ๋‹ˆํฌ ์ œ์•ฝ ์กฐ๊ฑด" ์„ ํ™œ์šฉํ•˜์—ฌ 5000๋ช… ์ด ๋™์‹œ์— ์ž…์ฐฐ ์‹œ๋„๋ฅผ ํ–ˆ์„ ๋•Œ, "1๋ช…๋งŒ ์ž…์ฐฐ ์„ฑ๊ณต โœ…, 4999๋ช… ์ž…์ฐฐ ์‹คํŒจ โŒ" ํ…Œ์ŠคํŠธ๋ฅผ ํ†ต๊ณผํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.


Thoughts

"์ž…์ฐฐ ๊ธฐ๋Šฅ" ์€ ์šฐ๋ฆฌ ์„œ๋น„์Šค "TicToc" ์—์„œ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๊ธฐ๋Šฅ์ด๊ณ  ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— ์˜ค๋žœ ์‹œ๊ฐ„์ด ๊ฑธ๋ ธ๋˜ ๊ฒƒ ๊ฐ™๋‹ค. ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž˜๋ชป ์ž‘์„ฑํ•˜์—ฌ ์ดˆ๋ฐ˜์— ์„ฑ๊ณตํ–ˆ๋‹ค๊ณ  ์˜คํ•ด๋ฅผ ๊ฐ–๊ฒŒ ๋˜์—ˆ๋‹ค.

์ด ๊ณผ์ •์„ ํ†ตํ•ด ๋‹จ์ˆœํžˆ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์˜ ์™„์„ฑ๋„๊ฐ€ ์‹œ์Šคํ…œ์˜ ์‹ ๋ขฐ์„ฑ์„ ๊ฒฐ์ •ํ•œ๋‹ค๋Š” ๊ฒƒ์„ ๋ฐฐ์šธ ์ˆ˜ ์žˆ์—ˆ๋‹ค.

"์ž…์ฐฐ ๊ธฐ๋Šฅ" ํ•˜๋‚˜๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐ์— ๋งŽ์€ ์‹œ๊ฐ„ ๋™์•ˆ ๋งŽ์€ ๊ณต๋ถ€๋ฅผ ํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค. ๋‹จ์ˆœํžˆ ์–ด๋…ธํ…Œ์ด์…˜์„ ๋ถ™์ด ๋™์‹œ์„ฑ ์ œ์–ด์—์„œ ๋๋‚ด๋Š” ๊ฒƒ์ด ์•„๋‹Œ Redis๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋ถ„์‚ฐ ๋ฝ์„ ํ™œ์šฉํ–ˆ๊ณ , ๋‹จ์ˆœํžˆ ๋ถ„์‚ฐ ๋ฝ์—์„œ ๋๋‚ด๋Š” ๊ฒƒ์ด ์•„๋‹Œ ๋”œ๋ ˆ์ด ์ „๋žต์„ ํ™œ์šฉํ•ด ์•ˆ์ „์„ฑ์„ ๋ณด์žฅํ–ˆ๋‹ค. ๋˜ ์ด์—์„œ ๋๋‚ด๋Š” ๊ฒƒ์ด ์•„๋‹Œ, ์›์ž์  ์—ฐ์‚ฐ๊ณผ ์œ ๋‹ˆํฌ ์ œ์•ฝ ์กฐ๊ฑด์„ ํ†ตํ•ด ๋”์šฑ ์•ˆ์ „์„ฑ์„ ๋ณด๊ฐ•ํ–ˆ๋‹ค.

์ด๋ฒˆ ๊ฒฝํ—˜์„ ํ†ตํ•ด ๋™์‹œ์„ฑ ์ œ์–ด์˜ ์ค‘์š”์„ฑ์„ ๋‹ค์‹œ ๊นจ๋‹ฌ์•˜๊ณ , ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์˜ ์‹ ๋ขฐ์„ฑ์ด ์„œ๋น„์Šค ์•ˆ์ •์„ฑ์— ์ง์ ‘์ ์ธ ์˜ํ–ฅ์„ ๋ฏธ์นœ๋‹ค๋Š” ์ ์„ ๋ฐฐ์šธ ์ˆ˜ ์žˆ์—ˆ๋‹ค. ์•ž์œผ๋กœ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ๊พธ์ค€ํžˆ ์ž‘์„ฑํ•˜์—ฌ ๋”์šฑ ์•ˆ์ •์„ฑ๊ณผ ์‹ ๋ขฐ์„ฑ์„ ๋ณด์žฅํ•˜๋Š” ๊ฐœ๋ฐœ์ž๋กœ ์„ฑ์žฅํ•ด์•ผ๊ฒ ๋‹ค.


profile
๐Ÿ’ป ๐Ÿ’ป ๐Ÿ’ป

0๊ฐœ์˜ ๋Œ“๊ธ€

๊ด€๋ จ ์ฑ„์šฉ ์ •๋ณด