diff --git a/src/main/java/LuckyVicky/backend/global/api_payload/ErrorCode.java b/src/main/java/LuckyVicky/backend/global/api_payload/ErrorCode.java index 13114c5..67cf9a4 100644 --- a/src/main/java/LuckyVicky/backend/global/api_payload/ErrorCode.java +++ b/src/main/java/LuckyVicky/backend/global/api_payload/ErrorCode.java @@ -70,7 +70,8 @@ public enum ErrorCode implements BaseCode { PACHINKO_NO_MORE_CHANCE(HttpStatus.BAD_REQUEST, "PACHINKO_4002", "이미 세칸을 고르셨습니다."), PACHINKO_NO_REWARD(HttpStatus.NOT_FOUND, "PACHINKO_4042", "해당 보석 종류에 대한 보상 레코드가 없습니다."), PACHINKO_NO_PREVIOUS_ROUND(HttpStatus.NOT_FOUND, "PACHINKO_4043", "사용자가 빠칭코 게임을 한 전적이 없어 보상 반환이 불가합니다."), - PACHINKO_ALREADY_SELECT_SQUARE(HttpStatus.OK, "PACHINKO_4003", "해당 칸은 이미 다른 사용자에 의해 선택되었습니다."), + PACHINKO_ALREADY_SELECT_SQUARE(HttpStatus.BAD_REQUEST, "PACHINKO_4003", "해당 칸은 이미 다른 사용자에 의해 선택되었습니다."), + PACHINKO_NO_MORE_JEWEL(HttpStatus.BAD_REQUEST, "PACHINKO_4004", "이미 3칸을 선택하셔서 더 이상 칸을 선택할 수 없습니다."), // Ranking RANKING_WEEK_ITEM_LIST_EMPTY(HttpStatus.BAD_REQUEST, "RANKING_4001", "해당 주차에 강화한 상품이 없습니다."), diff --git a/src/main/java/LuckyVicky/backend/pachinko/handler/PachinkoWebSocketHandler.java b/src/main/java/LuckyVicky/backend/pachinko/handler/PachinkoWebSocketHandler.java index 81cdef8..aeca503 100644 --- a/src/main/java/LuckyVicky/backend/pachinko/handler/PachinkoWebSocketHandler.java +++ b/src/main/java/LuckyVicky/backend/pachinko/handler/PachinkoWebSocketHandler.java @@ -13,7 +13,6 @@ import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.Semaphore; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -33,7 +32,6 @@ public class PachinkoWebSocketHandler extends TextWebSocketHandler { private final ObjectMapper objectMapper = new ObjectMapper(); private final List sessions = new ArrayList<>(); private final ExecutorService virtualThreadExecutor = Executors.newVirtualThreadPerTaskExecutor(); - private final Semaphore messageLimiter = new Semaphore(200); // 동시에 200개만 처리 @Override public void afterConnectionEstablished(WebSocketSession session) { @@ -100,10 +98,6 @@ private boolean validateJwt(WebSocketSession session, JsonNode node) throws IOEx } private boolean validateUserState(WebSocketSession session, User user, long currentRound) { - if (pachinkoService.noMoreJewel(user)) { - sendMessage(session, "칸을 선택할때 필요한 보석이 부족합니다."); - return false; - } if (!pachinkoService.canSelectMore(user, currentRound)) { sendMessage(session, "이미 " + PACHINKO_USER_MAX_SQUARES + "칸을 선택하셔서 더 이상 칸을 선택할 수 없습니다."); return false; @@ -112,15 +106,14 @@ private boolean validateUserState(WebSocketSession session, User user, long curr } private void processSquareSelection(WebSocketSession session, User user, long currentRound, int selectedSquare) { - String result = pachinkoService.selectSquare(user, currentRound, selectedSquare); + String result = pachinkoService.selectSquare(user, selectedSquare); switch (result) { case "정상적으로 선택 완료되었습니다." -> { broadcastMessage(user.getNickname() + "가 " + selectedSquare + "을 선택했습니다."); checkGameStatusAndCloseSessionsIfNeeded(); } - case "다른 사용자가 이전에 선택한 칸입니다." -> sendMessage(session, selectedSquare + "번째 칸은 이미 다른 사용자에 의해 선택되었습니다."); - case "본인이 이전에 선택한 칸입니다." -> sendMessage(session, selectedSquare + "번째 칸은 본인이 이전에 선택한 칸입니다."); - case null, default -> sendMessage(session, "이미 3칸을 선택하셔서 더 이상 칸을 선택할 수 없습니다."); + case "이미 선택된 칸입니다." -> sendMessage(session, selectedSquare + "번째 칸은 이미 다른 사용자에 의해 선택되었습니다."); + case "다른 사용자가 해당 칸을 선택 중입니다." -> sendMessage(session, selectedSquare + "번째 칸은 다른 사용자가 선택중인 칸입니다."); } } diff --git a/src/main/java/LuckyVicky/backend/pachinko/repository/UserPachinkoRepository.java b/src/main/java/LuckyVicky/backend/pachinko/repository/UserPachinkoRepository.java index b6dd497..682cbe0 100644 --- a/src/main/java/LuckyVicky/backend/pachinko/repository/UserPachinkoRepository.java +++ b/src/main/java/LuckyVicky/backend/pachinko/repository/UserPachinkoRepository.java @@ -15,7 +15,7 @@ public interface UserPachinkoRepository extends JpaRepository new ReentrantLock()); + if (!lock.tryLock()) { + return "다른 사용자가 해당 칸을 선택 중입니다."; } + try { + // DB, 캐시 갱신 이전 재확인 + if (selectedSquares.contains(squareNumber)) { + return "이미 선택된 칸입니다."; + } - // 사용자 Pachinko 상태 저장 - userPachinkoRepository.save(PachinkoConverter.saveUserPachinko(user, currentRound, squareNumber)); - log.info("user pachinko에 선택한 칸인 {}을 저장했습니다.", squareNumber); + // 보석 개수 확인 후 차감 + userJewelService.deductUserJewel(user); + log.info("빠칭코 칸 선택을 위해 B급 보석 하나를 지불하여 DB에서 보석을 차감했습니다."); - // 선택한 칸을 set에 추가 - addSelectedSquare(squareNumber); - log.info("선택한 칸을 set에 삽입했습니다. 변경된 set: {}", selectedSquares); + // 캐시 갱신 + addSelectedSquare(squareNumber); + log.info("선택한 칸을 set에 삽입했습니다. 변경된 set: {}", selectedSquares); - // 보석 차감 - deductUserJewel(user); - log.info("빠칭코 칸 선택을 위해 B급 보석 하나를 지불하여 DB에서 보석을 차감했습니다."); + // DB 갱신 + userPachinkoRepository.save(PachinkoConverter.saveUserPachinko(user, currentRound, squareNumber)); + log.info("user pachinko에 선택한 칸인 {}을 저장했습니다.", squareNumber); - return "정상적으로 선택 완료되었습니다."; + return "정상적으로 선택 완료되었습니다."; + } finally { + lock.unlock(); + } } public void addSelectedSquare(int square) { selectedSquares.add(square); } - private void deductUserJewel(User user) { - UserJewel userJewel = userJewelRepository.findByUserAndJewelType(user, PACHINKO_NEED_JEWEL_TYPE) - .orElseThrow(() -> new GeneralException(ErrorCode.USER_JEWEL_NOT_FOUND)); - userJewel.decreaseCount(PACHINKO_NEED_JEWEL_COUNT); - userJewelRepository.save(userJewel); - } - private void validateSquareNumber(int squareNumber) { if (squareNumber < PACHINKO_MIN_SQUARE_NUMBER || squareNumber > PACHINKO_TOTAL_SQUARE_COUNT) { throw new GeneralException(ErrorCode.PACHINKO_OUT_OF_BOUND); diff --git a/src/main/java/LuckyVicky/backend/user/repository/UserJewelRepository.java b/src/main/java/LuckyVicky/backend/user/repository/UserJewelRepository.java index 3e2a2da..d3d0a48 100644 --- a/src/main/java/LuckyVicky/backend/user/repository/UserJewelRepository.java +++ b/src/main/java/LuckyVicky/backend/user/repository/UserJewelRepository.java @@ -3,13 +3,16 @@ import LuckyVicky.backend.enhance.domain.JewelType; import LuckyVicky.backend.user.domain.User; import LuckyVicky.backend.user.domain.UserJewel; +import jakarta.persistence.LockModeType; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; import org.springframework.stereotype.Repository; @Repository public interface UserJewelRepository extends JpaRepository { UserJewel findFirstByUserAndJewelType(User user, JewelType jewelType); + @Lock(LockModeType.PESSIMISTIC_WRITE) Optional findByUserAndJewelType(User user, JewelType jewelType); } diff --git a/src/main/java/LuckyVicky/backend/user/service/UserJewelService.java b/src/main/java/LuckyVicky/backend/user/service/UserJewelService.java new file mode 100644 index 0000000..4052d01 --- /dev/null +++ b/src/main/java/LuckyVicky/backend/user/service/UserJewelService.java @@ -0,0 +1,36 @@ +package LuckyVicky.backend.user.service; + +import LuckyVicky.backend.enhance.domain.JewelType; +import LuckyVicky.backend.global.api_payload.ErrorCode; +import LuckyVicky.backend.global.exception.GeneralException; +import LuckyVicky.backend.user.domain.User; +import LuckyVicky.backend.user.domain.UserJewel; +import LuckyVicky.backend.user.repository.UserJewelRepository; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +@Slf4j +public class UserJewelService { + + public final UserJewelRepository userJewelRepository; + private static final JewelType PACHINKO_NEED_JEWEL_TYPE = JewelType.B; + private static final int PACHINKO_NEED_JEWEL_COUNT = 1; + + @Transactional + public void deductUserJewel(User user) { + // DB에서 락 걸고 사용자 보석 정보 조회 + UserJewel userJewel = userJewelRepository.findByUserAndJewelType(user, PACHINKO_NEED_JEWEL_TYPE) + .orElseThrow(() -> new GeneralException(ErrorCode.USER_JEWEL_NOT_FOUND)); + // 선택을 위한 코색 개수가 있는지 + if (userJewel.getCount() < PACHINKO_NEED_JEWEL_COUNT) { + throw new GeneralException(ErrorCode.PACHINKO_NO_MORE_JEWEL); + } + // 보석 수량 차감 + userJewel.decreaseCount(PACHINKO_NEED_JEWEL_COUNT); + userJewelRepository.save(userJewel); + } +}