Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public ResponseEntity<BiddingResponse> registerBidding(
@Valid @RequestBody RegisterBiddingRequest request,
@Parameter(hidden = true) @JwtAuthorization User bidder
) {
BiddingResponse response = biddingService.registerBiddingWithPessimisticLock(request, auctionId, bidder);
BiddingResponse response = biddingService.registerBidding(request, auctionId, bidder);
return ResponseEntity.ok(response);
}

Expand Down
108 changes: 108 additions & 0 deletions core/src/main/java/dev/handsup/bidding/service/BiddingLockService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package dev.handsup.bidding.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import dev.handsup.auction.domain.Auction;
import dev.handsup.auction.exception.AuctionErrorCode;
import dev.handsup.auction.repository.auction.AuctionRepository;
import dev.handsup.bidding.domain.Bidding;
import dev.handsup.bidding.dto.BiddingMapper;
import dev.handsup.bidding.dto.request.RegisterBiddingRequest;
import dev.handsup.bidding.dto.response.BiddingResponse;
import dev.handsup.bidding.exception.BiddingErrorCode;
import dev.handsup.bidding.repository.BiddingRepository;
import dev.handsup.common.exception.NotFoundException;
import dev.handsup.common.exception.ValidationException;
import dev.handsup.common.redisson.DistributeLock;
import dev.handsup.notification.domain.NotificationType;
import dev.handsup.notification.service.NotificationSender;
import dev.handsup.user.domain.User;
import lombok.RequiredArgsConstructor;

/**
* lock 간 입찰 소요 시간 성능 테스트
*/
@Service
@RequiredArgsConstructor
public class BiddingLockService {

private final BiddingRepository biddingRepository;
private final AuctionRepository auctionRepository;
private final NotificationSender notificationSender;

@Transactional
public BiddingResponse registerBiddingWithPessimisticLock(RegisterBiddingRequest request, Long auctionId, User bidder) {
Auction auction = auctionRepository
.findByIdWithPessimisticLock(auctionId)
.orElseThrow(() -> new NotFoundException(AuctionErrorCode.NOT_FOUND_AUCTION));

validateBiddingPrice(request.biddingPrice(), auction);
updateAuctionOnNewBidding(request, auction);
Bidding bidding = BiddingMapper.toBidding(request.biddingPrice(), auction, bidder);
notificationSender.sendNotification(
bidder.getId(),
auction.getSeller().getId(),
auction.getSeller().getNickname(),
auction.getId(),
NotificationType.BIDDING_CREATED
);

return BiddingMapper.toBiddingResponse(biddingRepository.save(bidding));
}


@Transactional
public BiddingResponse registerBiddingWithOptimisticLock(RegisterBiddingRequest request, Long auctionId, User bidder) {
Auction auction = auctionRepository
.findByIdWithOptimisticLock(auctionId)
.orElseThrow(() -> new NotFoundException(AuctionErrorCode.NOT_FOUND_AUCTION));

validateBiddingPrice(request.biddingPrice(), auction);
updateAuctionOnNewBidding(request, auction);
Bidding bidding = BiddingMapper.toBidding(request.biddingPrice(), auction, bidder);

return BiddingMapper.toBiddingResponse(biddingRepository.save(bidding));
}


@Transactional
@DistributeLock(key = "'auction_' + #auctionId") // auctionId 값을 추출하여 락 키로 사용
public BiddingResponse registerBiddingWithDistributedLock(RegisterBiddingRequest request, Long auctionId, User bidder) {
Auction auction = getAuctionById(auctionId);

validateBiddingPrice(request.biddingPrice(), auction);
updateAuctionOnNewBidding(request, auction);
Bidding bidding = BiddingMapper.toBidding(request.biddingPrice(), auction, bidder);

return BiddingMapper.toBiddingResponse(biddingRepository.save(bidding));
}


public void validateBiddingPrice(int biddingPrice, Auction auction) {
Integer maxBiddingPrice = biddingRepository.findMaxBiddingPriceByAuctionId(auction.getId());

if (maxBiddingPrice == null) {
// 입찰 내역이 없는 경우, 최소 입찰가부터 입찰 가능
if (biddingPrice < auction.getInitPrice()) {
throw new ValidationException(BiddingErrorCode.BIDDING_PRICE_LESS_THAN_INIT_PRICE);
}
} else {
// 최고 입찰가보다 1000원 이상일 때만 입찰 가능
if (biddingPrice < (maxBiddingPrice + 1000)) {
throw new ValidationException(BiddingErrorCode.BIDDING_PRICE_NOT_HIGH_ENOUGH);
}
}
}

private void updateAuctionOnNewBidding(RegisterBiddingRequest request, Auction auction) {
auction.updateCurrentBiddingPrice(request.biddingPrice()); // 경매 입찰 최고가 갱신
auction.increaseBiddingCount(); // 경매 입찰 수 + 1
}


private Auction getAuctionById(Long auctionId) {
return auctionRepository.findById(auctionId)
.orElseThrow(() -> new NotFoundException(AuctionErrorCode.NOT_FOUND_AUCTION));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package dev.handsup.bidding.service;

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import dev.handsup.auction.domain.Auction;
import dev.handsup.auction.exception.AuctionErrorCode;
import dev.handsup.auction.repository.auction.AuctionRepository;
import dev.handsup.bidding.domain.Bidding;
import dev.handsup.bidding.dto.BiddingMapper;
import dev.handsup.bidding.dto.request.RegisterBiddingRequest;
import dev.handsup.bidding.dto.response.BiddingResponse;
import dev.handsup.bidding.repository.BiddingRepository;
import dev.handsup.common.exception.NotFoundException;
import dev.handsup.notification.domain.NotificationType;
import dev.handsup.notification.service.NotificationSender;
import dev.handsup.user.domain.User;
import lombok.RequiredArgsConstructor;

/**
* 입찰 생성 알림 비동기/동기 성능 테스트
*/
@Service
@RequiredArgsConstructor
public class BiddingNotificationService {
private final BiddingRepository biddingRepository;
private final AuctionRepository auctionRepository;
private final ApplicationEventPublisher eventPublisher;
private final NotificationSender notificationSender;

@Transactional
public BiddingResponse registerBiddingSync(RegisterBiddingRequest request, Long auctionId, User bidder) {
Auction auction = auctionRepository
.findByIdWithPessimisticLock(auctionId)
.orElseThrow(() -> new NotFoundException(AuctionErrorCode.NOT_FOUND_AUCTION));

updateAuctionOnNewBidding(request, auction);
Bidding bidding = BiddingMapper.toBidding(request.biddingPrice(), auction, bidder);
notificationSender.sendNotification(
bidder.getId(),
auction.getSeller().getId(),
auction.getSeller().getNickname(),
auction.getId(),
NotificationType.BIDDING_CREATED
);

return BiddingMapper.toBiddingResponse(biddingRepository.save(bidding));
}

@Transactional
public BiddingResponse registerBiddingWithAsync(RegisterBiddingRequest request, Long auctionId, User bidder) {
Auction auction = auctionRepository
.findByIdWithPessimisticLock(auctionId)
.orElseThrow(() -> new NotFoundException(AuctionErrorCode.NOT_FOUND_AUCTION));
updateAuctionOnNewBidding(request, auction);
Bidding bidding = BiddingMapper.toBidding(request.biddingPrice(), auction, bidder);

eventPublisher.publishEvent(new NotificationEvent(
bidder.getId(),
auction.getSeller().getId(),
auction.getSeller().getNickname(),
auction.getId(),
NotificationType.BIDDING_CREATED
));

return BiddingMapper.toBiddingResponse(biddingRepository.save(bidding));
}

private void updateAuctionOnNewBidding(RegisterBiddingRequest request, Auction auction) {
auction.updateCurrentBiddingPrice(request.biddingPrice()); // 경매 입찰 최고가 갱신
auction.increaseBiddingCount(); // 경매 입찰 수 + 1
}
}
61 changes: 11 additions & 50 deletions core/src/main/java/dev/handsup/bidding/service/BiddingService.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.handsup.bidding.service;

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.stereotype.Service;
Expand All @@ -19,9 +20,7 @@
import dev.handsup.common.dto.PageResponse;
import dev.handsup.common.exception.NotFoundException;
import dev.handsup.common.exception.ValidationException;
import dev.handsup.common.redisson.DistributeLock;
import dev.handsup.notification.domain.NotificationType;
import dev.handsup.notification.service.NotificationSender;
import dev.handsup.user.domain.User;
import lombok.RequiredArgsConstructor;

Expand All @@ -32,53 +31,29 @@ public class BiddingService {
private final BiddingRepository biddingRepository;
private final BiddingQueryRepository biddingQueryRepository;
private final AuctionRepository auctionRepository;
private final NotificationSender notificationSender;
private final ApplicationEventPublisher eventPublisher;

@Transactional
@DistributeLock(key = "'auction_' + #auctionId") // auctionId 값을 추출하여 락 키로 사용
public BiddingResponse registerBiddingWithDistributedLock(RegisterBiddingRequest request, Long auctionId, User bidder) {
Auction auction = getAuctionById(auctionId);

validateBiddingPrice(request.biddingPrice(), auction);
updateAuctionOnNewBidding(request, auction);
Bidding bidding = BiddingMapper.toBidding(request.biddingPrice(), auction, bidder);

sendBiddingNotification(bidder, auction);

return BiddingMapper.toBiddingResponse(biddingRepository.save(bidding));
}

@Transactional
public BiddingResponse registerBiddingWithPessimisticLock(RegisterBiddingRequest request, Long auctionId, User bidder) {
public BiddingResponse registerBidding(RegisterBiddingRequest request, Long auctionId, User bidder) {
Auction auction = auctionRepository
.findByIdWithPessimisticLock(auctionId)
.orElseThrow(() -> new NotFoundException(AuctionErrorCode.NOT_FOUND_AUCTION));

validateBiddingPrice(request.biddingPrice(), auction);
updateAuctionOnNewBidding(request, auction);
Bidding bidding = BiddingMapper.toBidding(request.biddingPrice(), auction, bidder);

sendBiddingNotification(bidder, auction);

return BiddingMapper.toBiddingResponse(biddingRepository.save(bidding));
}

@Transactional
public BiddingResponse registerBiddingWithOptimisticLock(RegisterBiddingRequest request, Long auctionId, User bidder) {
Auction auction = auctionRepository
.findByIdWithOptimisticLock(auctionId)
.findByIdWithPessimisticLock(auctionId)
.orElseThrow(() -> new NotFoundException(AuctionErrorCode.NOT_FOUND_AUCTION));

validateBiddingPrice(request.biddingPrice(), auction);
updateAuctionOnNewBidding(request, auction);
Bidding bidding = BiddingMapper.toBidding(request.biddingPrice(), auction, bidder);

sendBiddingNotification(bidder, auction);
eventPublisher.publishEvent(new NotificationEvent(
bidder.getId(),
auction.getSeller().getId(),
auction.getSeller().getNickname(),
auction.getId(),
NotificationType.BIDDING_CREATED
));

return BiddingMapper.toBiddingResponse(biddingRepository.save(bidding));
}


@Transactional(readOnly = true)
public PageResponse<BiddingResponse> getBidsOfAuction(Long auctionId, Pageable pageable) {
Slice<BiddingResponse> biddingResponsePage = biddingRepository
Expand Down Expand Up @@ -134,20 +109,6 @@ private Bidding findBiddingById(Long biddingId) {
.orElseThrow(() -> new NotFoundException(BiddingErrorCode.NOT_FOUND_BIDDING));
}

private Auction getAuctionById(Long auctionId) {
return auctionRepository.findById(auctionId)
.orElseThrow(() -> new NotFoundException(AuctionErrorCode.NOT_FOUND_AUCTION));
}

private void sendBiddingNotification(User bidder, Auction auction) {
notificationSender.sendNotification(
bidder,
auction.getSeller(),
auction.getId(),
NotificationType.BIDDING_CREATED
);
}

private void updateAuctionOnNewBidding(RegisterBiddingRequest request, Auction auction) {
auction.updateCurrentBiddingPrice(request.biddingPrice()); // 경매 입찰 최고가 갱신
auction.increaseBiddingCount(); // 경매 입찰 수 + 1
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package dev.handsup.bidding.service;

import dev.handsup.notification.domain.NotificationType;

public record NotificationEvent(
Long senderId,
Long receiverId,
String receiverNickname,
Long auctionId,
NotificationType type
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package dev.handsup.bidding.service;

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

import dev.handsup.notification.service.NotificationSender;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Component
@RequiredArgsConstructor
@Slf4j
public class NotificationEventListener {
private final NotificationSender notificationSender;

@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleBiddingEventCompleted(NotificationEvent event) {
notificationSender.sendNotification(
event.senderId(),
event.receiverId(),
event.receiverNickname(),
event.auctionId(),
event.type()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
@RequiredArgsConstructor
@Slf4j
public class OptimisticLockAuctionFacade {
private final BiddingService biddingService;
private final BiddingLockService biddingService;
private static final long RETRY_DELAY_MS = 50;

public BiddingResponse registerBidding(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import com.google.firebase.messaging.Message;
import com.google.firebase.messaging.Notification;

import dev.handsup.common.exception.ValidationException;
import dev.handsup.notification.domain.NotificationType;
import dev.handsup.notification.repository.FcmTokenRepository;
import lombok.RequiredArgsConstructor;
Expand All @@ -27,6 +26,7 @@ public void sendNotification(
) {
String fcmToken = fcmTokenRepository.getFcmToken(receiverId);
if (fcmToken == null) {
log.info("알림/fcm토큰 없음");
return;
}

Expand All @@ -44,9 +44,10 @@ public void sendNotification(
private void send(Message message, Long receiverId) {
try {
firebaseMessaging.send(message);
log.info("Sent message: {}, to: {}", message, receiverId);
log.info("알림 전송 성공: {} to: {}", message, receiverId);
} catch (FirebaseMessagingException e) {
throw new ValidationException(e.getMessage());
log.error("알림 발송 실패 receiverId={} message={}",
receiverId,e.getMessage());
}
}

Expand Down
Loading