From 19ace35d9064287d55bef627d61439812483e8ef Mon Sep 17 00:00:00 2001 From: persi Date: Mon, 4 Aug 2025 17:18:05 +0900 Subject: [PATCH 1/9] =?UTF-8?q?refactor:=20=ED=95=84=EC=9A=94=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/pachinko/handler/PachinkoWebSocketHandler.java | 4 +--- .../backend/pachinko/repository/UserPachinkoRepository.java | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/LuckyVicky/backend/pachinko/handler/PachinkoWebSocketHandler.java b/src/main/java/LuckyVicky/backend/pachinko/handler/PachinkoWebSocketHandler.java index 81cdef8..b7f5efd 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) { @@ -112,7 +110,7 @@ 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 + "을 선택했습니다."); 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 Date: Mon, 4 Aug 2025 17:23:27 +0900 Subject: [PATCH 2/9] =?UTF-8?q?test:=20=EB=B6=80=ED=95=98=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B4=9D=20=EC=86=8C=EC=9A=94=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EC=B6=9C=EB=A0=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PachinkoLoadTestWithVerification.java | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoLoadTestWithVerification.java b/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoLoadTestWithVerification.java index b0b1afe..23a4598 100644 --- a/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoLoadTestWithVerification.java +++ b/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoLoadTestWithVerification.java @@ -16,11 +16,10 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import org.glassfish.tyrus.client.ClientManager; public class PachinkoLoadTestWithVerification { - private static final int USERS_PER_SQUARE = 1000; + private static final int USERS_PER_SQUARE = 200; private static final int TOTAL_SQUARES = 5; private static final String TOKEN_URL = "http://localhost:8080/token/generate"; private static final String WS_URL = "ws://localhost:8080/pachinko"; @@ -29,10 +28,7 @@ public class PachinkoLoadTestWithVerification { private static final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); public static void main(String[] args) throws Exception { - WebSocketContainer container = ClientManager.createClient(); - //WebSocketContainer container = ContainerProvider.getWebSocketContainer(); - System.out.println("사용 중인 WebSocketContainer 구현체: " + container.getClass().getName()); - + long start = System.nanoTime(); CountDownLatch latch = new CountDownLatch(USERS_PER_SQUARE * TOTAL_SQUARES); for (int square = 1; square <= TOTAL_SQUARES; square++) { @@ -56,13 +52,17 @@ public static void main(String[] args) throws Exception { executor.shutdown(); System.out.println("make user finish"); + long end = System.nanoTime(); + // 결과 검증 Thread.sleep(2000); // 데이터 반영 대기 verifySelectedSquares(); + + double elapsedMs = (end - start) / 1_000_000.0; + System.out.printf("부하 테스트 완료 – 총 소요 시간: %.2fms (%.2f초)\n", elapsedMs, elapsedMs / 1000.0); } private static String getTokenForUser(int userNum) throws Exception { - System.out.println(userNum); URL url = new URL(TOKEN_URL); HttpURLConnection con = (HttpURLConnection) url.openConnection(); con.setRequestMethod("POST"); @@ -87,9 +87,7 @@ private static String getTokenForUser(int userNum) throws Exception { String response = scanner.hasNext() ? scanner.next() : ""; ObjectMapper mapper = new ObjectMapper(); JsonNode root = mapper.readTree(response); - String token = root.get("result").get("accessToken").asText(); - System.out.println(token); - return token; + return root.get("result").get("accessToken").asText(); } } else { @@ -128,8 +126,6 @@ private static void verifySelectedSquares() throws Exception { try (Scanner scanner = new Scanner(con.getInputStream()).useDelimiter("\\A")) { String response = scanner.hasNext() ? scanner.next() : ""; System.out.println("선택된 칸 결과 확인 응답:"); - System.out.println(response); - int resultStart = response.indexOf("["); int resultEnd = response.indexOf("]", resultStart) + 1; String resultJsonArray = response.substring(resultStart, resultEnd); From 81e431f0295f3e1a31aa586a4bc765f90fb6cd4d Mon Sep 17 00:00:00 2001 From: persi Date: Mon, 4 Aug 2025 17:29:25 +0900 Subject: [PATCH 3/9] =?UTF-8?q?test:=20=EC=B6=9C=EB=A0=A5=20=ED=98=95?= =?UTF-8?q?=EC=8B=9D=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pachinko/service/PachinkoLoadTestWithVerification.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoLoadTestWithVerification.java b/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoLoadTestWithVerification.java index 23a4598..64b7c95 100644 --- a/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoLoadTestWithVerification.java +++ b/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoLoadTestWithVerification.java @@ -125,7 +125,6 @@ private static void verifySelectedSquares() throws Exception { if (con.getResponseCode() == 200) { try (Scanner scanner = new Scanner(con.getInputStream()).useDelimiter("\\A")) { String response = scanner.hasNext() ? scanner.next() : ""; - System.out.println("선택된 칸 결과 확인 응답:"); int resultStart = response.indexOf("["); int resultEnd = response.indexOf("]", resultStart) + 1; String resultJsonArray = response.substring(resultStart, resultEnd); @@ -133,7 +132,7 @@ private static void verifySelectedSquares() throws Exception { System.out.printf("최종 선택된 칸 개수: %d개%n", squares.length); if (squares.length == TOTAL_SQUARES) { - System.out.printf("테스트 성공: %d개 칸이 정확히 채워졌습니다.", squares.length); + System.out.printf("테스트 성공: %d개 칸이 정확히 채워졌습니다.\n", squares.length); } else { System.err.println("테스트 실패: 선택된 칸 수 = " + squares.length); } From a0aee6bd21e81bd093d948a0fe15621b08f2cf96 Mon Sep 17 00:00:00 2001 From: persi Date: Mon, 4 Aug 2025 18:11:39 +0900 Subject: [PATCH 4/9] =?UTF-8?q?refactor:=20synchronized=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=20-=2012=EC=B4=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pachinko/service/PachinkoService.java | 27 +++++-------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java b/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java index b1c204d..eb7db45 100644 --- a/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java +++ b/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java @@ -34,6 +34,7 @@ import java.util.stream.Collectors; import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.Synchronized; import lombok.extern.slf4j.Slf4j; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.retry.annotation.Backoff; @@ -118,34 +119,20 @@ public boolean canSelectMore(User user, Long round) { maxAttempts = 2, backoff = @Backoff(delay = 100, multiplier = 2) ) - public String selectSquare(User user, long currentRound, int squareNumber) { + @Synchronized + public String selectSquare(User user, int squareNumber) { // 칸 번호 유효성 검증 validateSquareNumber(squareNumber); - // 이미 선택된 칸인지 확인 - if (selectedSquares.contains(squareNumber)) { - log.info("{} 이미 {}가 선택되었습니다.", selectedSquares, squareNumber); - - boolean userAlreadySelected = userPachinkoRepository.existsByUserAndRoundAndSquare(user, currentRound, - squareNumber); - - if (userAlreadySelected) { - log.info("본인이 이전에 선택한 칸입니다."); - return "본인이 이전에 선택한 칸입니다."; - } else { - log.info("다른 사용자가 이전에 선택한 칸입니다."); - return "다른 사용자가 이전에 선택한 칸입니다."; - } + // DB 확인 + if (userPachinkoRepository.existsByRoundAndSquare(currentRound, squareNumber)) { + return "이미 선택된 칸 입니다."; } - // 사용자 Pachinko 상태 저장 + // DB 갱신 userPachinkoRepository.save(PachinkoConverter.saveUserPachinko(user, currentRound, squareNumber)); log.info("user pachinko에 선택한 칸인 {}을 저장했습니다.", squareNumber); - // 선택한 칸을 set에 추가 - addSelectedSquare(squareNumber); - log.info("선택한 칸을 set에 삽입했습니다. 변경된 set: {}", selectedSquares); - // 보석 차감 deductUserJewel(user); log.info("빠칭코 칸 선택을 위해 B급 보석 하나를 지불하여 DB에서 보석을 차감했습니다."); From b596f0a6ac126c410bfa5273d330be2b85e462ce Mon Sep 17 00:00:00 2001 From: persi Date: Mon, 4 Aug 2025 18:19:03 +0900 Subject: [PATCH 5/9] =?UTF-8?q?refactor:=20ReentrantLock=20=ED=86=B5?= =?UTF-8?q?=ED=95=9C=20=EB=9D=BD=EB=B6=84=ED=95=A0=EA=B3=BC=20=EC=BA=90?= =?UTF-8?q?=EC=8B=B1=20-=200.5=EC=B4=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pachinko/service/PachinkoService.java | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java b/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java index eb7db45..0ed7ffd 100644 --- a/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java +++ b/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java @@ -31,10 +31,10 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; import lombok.Getter; import lombok.RequiredArgsConstructor; -import lombok.Synchronized; import lombok.extern.slf4j.Slf4j; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.retry.annotation.Backoff; @@ -66,6 +66,7 @@ public class PachinkoService { @Getter private final Set selectedSquares = ConcurrentHashMap.newKeySet(); + private final ConcurrentHashMap squareLocks = new ConcurrentHashMap<>(); public Set viewSelectedSquares() { // 읽기 전용 뷰 반환 return Collections.unmodifiableSet(selectedSquares); @@ -119,25 +120,42 @@ public boolean canSelectMore(User user, Long round) { maxAttempts = 2, backoff = @Backoff(delay = 100, multiplier = 2) ) - @Synchronized public String selectSquare(User user, int squareNumber) { // 칸 번호 유효성 검증 validateSquareNumber(squareNumber); - // DB 확인 - if (userPachinkoRepository.existsByRoundAndSquare(currentRound, squareNumber)) { - return "이미 선택된 칸 입니다."; + // 캐시 검증 + if (selectedSquares.contains(squareNumber)) { + throw new IllegalStateException("이미 선택된 칸입니다."); } - // DB 갱신 - userPachinkoRepository.save(PachinkoConverter.saveUserPachinko(user, currentRound, squareNumber)); - log.info("user pachinko에 선택한 칸인 {}을 저장했습니다.", squareNumber); + // lock 획득 시도 + ReentrantLock lock = squareLocks.computeIfAbsent(squareNumber, key -> new ReentrantLock()); + if (!lock.tryLock()) { + return "다른 사용자가 해당 칸을 선택 중입니다."; + } + try { + // DB, 캐시 갱신 이전 재확인 + if (selectedSquares.contains(squareNumber)) { + return "이미 선택된 칸입니다."; + } + + // DB 갱신 + userPachinkoRepository.save(PachinkoConverter.saveUserPachinko(user, currentRound, squareNumber)); + log.info("user pachinko에 선택한 칸인 {}을 저장했습니다.", squareNumber); - // 보석 차감 - deductUserJewel(user); - log.info("빠칭코 칸 선택을 위해 B급 보석 하나를 지불하여 DB에서 보석을 차감했습니다."); + // 캐시 갱신 + addSelectedSquare(squareNumber); + log.info("선택한 칸을 set에 삽입했습니다. 변경된 set: {}", selectedSquares); - return "정상적으로 선택 완료되었습니다."; + // 보석 차감 + deductUserJewel(user); + log.info("빠칭코 칸 선택을 위해 B급 보석 하나를 지불하여 DB에서 보석을 차감했습니다."); + + return "정상적으로 선택 완료되었습니다."; + } finally { + lock.unlock(); + } } public void addSelectedSquare(int square) { From 13a548c232a01b228e315f829465ae73707964ea Mon Sep 17 00:00:00 2001 From: persi Date: Mon, 4 Aug 2025 18:33:21 +0900 Subject: [PATCH 6/9] =?UTF-8?q?refactor:=20=EC=9E=AC=EC=8B=9C=EB=8F=84=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/pachinko/service/PachinkoService.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java b/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java index 0ed7ffd..3d6facf 100644 --- a/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java +++ b/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java @@ -36,9 +36,6 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.retry.annotation.Backoff; -import org.springframework.retry.annotation.Retryable; import org.springframework.stereotype.Service; @Slf4j @@ -115,11 +112,6 @@ public boolean canSelectMore(User user, Long round) { } @Transactional - @Retryable( - value = DataIntegrityViolationException.class, - maxAttempts = 2, - backoff = @Backoff(delay = 100, multiplier = 2) - ) public String selectSquare(User user, int squareNumber) { // 칸 번호 유효성 검증 validateSquareNumber(squareNumber); From 1cc9b82d43d56f6279f42027f8399faae30cae88 Mon Sep 17 00:00:00 2001 From: persi Date: Mon, 4 Aug 2025 18:48:32 +0900 Subject: [PATCH 7/9] =?UTF-8?q?refactor:=20=EC=9D=B4=EB=AF=B8=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=EB=90=9C=20=EC=B9=B8=EC=97=90=20=EB=8C=80=ED=95=B4=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EB=8C=80=EC=8B=A0=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=EB=B0=98=ED=99=98=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LuckyVicky/backend/pachinko/service/PachinkoService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java b/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java index 3d6facf..24d42c6 100644 --- a/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java +++ b/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java @@ -118,7 +118,7 @@ public String selectSquare(User user, int squareNumber) { // 캐시 검증 if (selectedSquares.contains(squareNumber)) { - throw new IllegalStateException("이미 선택된 칸입니다."); + return "이미 선택된 칸입니다."; } // lock 획득 시도 From 5e36476d7e0e12a446575ebab735b64012d0b77a Mon Sep 17 00:00:00 2001 From: persi Date: Sun, 7 Sep 2025 14:30:44 +0900 Subject: [PATCH 8/9] =?UTF-8?q?refactor:=20=EB=B3=B4=EC=84=9D=20=EC=97=AC?= =?UTF-8?q?=EB=B6=80=20=ED=99=95=EC=9D=B8=20=ED=9B=84=20=EC=B0=A8=EA=B0=90?= =?UTF-8?q?=20=EC=8B=9C=EC=97=90=20=EB=B9=84=EA=B4=80=EC=A0=81=20=EB=9D=BD?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=20->=20=EC=95=88=EC=A0=84=EC=84=B1=20?= =?UTF-8?q?=ED=99=95=EB=B3=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/global/api_payload/ErrorCode.java | 3 +- .../handler/PachinkoWebSocketHandler.java | 2 +- .../PachinkoLoadTestWithVerification.java | 4 +-- .../pachinko/service/PachinkoService.java | 21 +++++------ .../user/repository/UserJewelRepository.java | 3 ++ .../user/service/UserJewelService.java | 35 +++++++++++++++++++ 6 files changed, 51 insertions(+), 17 deletions(-) create mode 100644 src/main/java/LuckyVicky/backend/user/service/UserJewelService.java 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 b7f5efd..29b1410 100644 --- a/src/main/java/LuckyVicky/backend/pachinko/handler/PachinkoWebSocketHandler.java +++ b/src/main/java/LuckyVicky/backend/pachinko/handler/PachinkoWebSocketHandler.java @@ -118,7 +118,7 @@ private void processSquareSelection(WebSocketSession session, User user, long cu } case "다른 사용자가 이전에 선택한 칸입니다." -> sendMessage(session, selectedSquare + "번째 칸은 이미 다른 사용자에 의해 선택되었습니다."); case "본인이 이전에 선택한 칸입니다." -> sendMessage(session, selectedSquare + "번째 칸은 본인이 이전에 선택한 칸입니다."); - case null, default -> sendMessage(session, "이미 3칸을 선택하셔서 더 이상 칸을 선택할 수 없습니다."); + case "이미 3칸을 선택하셔서 더 이상 칸을 선택할 수 없습니다." -> sendMessage(session, "이미 3칸을 선택하셔서 더 이상 칸을 선택할 수 없습니다."); } } diff --git a/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoLoadTestWithVerification.java b/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoLoadTestWithVerification.java index 64b7c95..2aafe1b 100644 --- a/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoLoadTestWithVerification.java +++ b/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoLoadTestWithVerification.java @@ -19,8 +19,8 @@ public class PachinkoLoadTestWithVerification { - private static final int USERS_PER_SQUARE = 200; - private static final int TOTAL_SQUARES = 5; + private static final int USERS_PER_SQUARE = 30; + private static final int TOTAL_SQUARES = 36; private static final String TOKEN_URL = "http://localhost:8080/token/generate"; private static final String WS_URL = "ws://localhost:8080/pachinko"; private static final String VERIFY_URL = "http://localhost:8080/game/pachinko/selected-squares"; diff --git a/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java b/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java index 24d42c6..cd52cce 100644 --- a/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java +++ b/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java @@ -20,6 +20,7 @@ import LuckyVicky.backend.user.domain.UserJewel; import LuckyVicky.backend.user.repository.UserJewelRepository; import LuckyVicky.backend.user.repository.UserRepository; +import LuckyVicky.backend.user.service.UserJewelService; import jakarta.annotation.PostConstruct; import jakarta.transaction.Transactional; import java.security.SecureRandom; @@ -59,6 +60,7 @@ public class PachinkoService { private final UserJewelRepository userJewelRepository; private final UserRepository userRepository; private final DisplayBoardService displayBoardService; + private final UserJewelService userJewelService; private final FcmService fcmService; @Getter @@ -132,17 +134,17 @@ public String selectSquare(User user, int squareNumber) { return "이미 선택된 칸입니다."; } - // DB 갱신 - userPachinkoRepository.save(PachinkoConverter.saveUserPachinko(user, currentRound, squareNumber)); - log.info("user pachinko에 선택한 칸인 {}을 저장했습니다.", squareNumber); + // 보석 여부 확인 후 차감 + userJewelService.deductUserJewel(user); + log.info("빠칭코 칸 선택을 위해 B급 보석 하나를 지불하여 DB에서 보석을 차감했습니다."); // 캐시 갱신 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 "정상적으로 선택 완료되었습니다."; } finally { @@ -154,13 +156,6 @@ 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..155c763 --- /dev/null +++ b/src/main/java/LuckyVicky/backend/user/service/UserJewelService.java @@ -0,0 +1,35 @@ +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); + } +} From cb76f3ab104ddcd117e802594812aa80d593cc99 Mon Sep 17 00:00:00 2001 From: persi Date: Sun, 7 Sep 2025 14:44:30 +0900 Subject: [PATCH 9/9] =?UTF-8?q?refactor:=20=EB=B3=B4=EC=84=9D=20=EC=97=AC?= =?UTF-8?q?=EB=B6=80=20=ED=99=95=EC=9D=B8=20=EC=A4=91=EB=B3=B5=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pachinko/handler/PachinkoWebSocketHandler.java | 9 ++------- .../backend/pachinko/service/PachinkoService.java | 12 ++---------- .../backend/user/service/UserJewelService.java | 1 + 3 files changed, 5 insertions(+), 17 deletions(-) diff --git a/src/main/java/LuckyVicky/backend/pachinko/handler/PachinkoWebSocketHandler.java b/src/main/java/LuckyVicky/backend/pachinko/handler/PachinkoWebSocketHandler.java index 29b1410..aeca503 100644 --- a/src/main/java/LuckyVicky/backend/pachinko/handler/PachinkoWebSocketHandler.java +++ b/src/main/java/LuckyVicky/backend/pachinko/handler/PachinkoWebSocketHandler.java @@ -98,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; @@ -116,9 +112,8 @@ private void processSquareSelection(WebSocketSession session, User user, long cu broadcastMessage(user.getNickname() + "가 " + selectedSquare + "을 선택했습니다."); checkGameStatusAndCloseSessionsIfNeeded(); } - case "다른 사용자가 이전에 선택한 칸입니다." -> sendMessage(session, selectedSquare + "번째 칸은 이미 다른 사용자에 의해 선택되었습니다."); - case "본인이 이전에 선택한 칸입니다." -> sendMessage(session, selectedSquare + "번째 칸은 본인이 이전에 선택한 칸입니다."); - case "이미 3칸을 선택하셔서 더 이상 칸을 선택할 수 없습니다." -> sendMessage(session, "이미 3칸을 선택하셔서 더 이상 칸을 선택할 수 없습니다."); + case "이미 선택된 칸입니다." -> sendMessage(session, selectedSquare + "번째 칸은 이미 다른 사용자에 의해 선택되었습니다."); + case "다른 사용자가 해당 칸을 선택 중입니다." -> sendMessage(session, selectedSquare + "번째 칸은 다른 사용자가 선택중인 칸입니다."); } } diff --git a/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java b/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java index cd52cce..e659ca5 100644 --- a/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java +++ b/src/main/java/LuckyVicky/backend/pachinko/service/PachinkoService.java @@ -64,7 +64,7 @@ public class PachinkoService { private final FcmService fcmService; @Getter - private final Set selectedSquares = ConcurrentHashMap.newKeySet(); + private final Set selectedSquares = ConcurrentHashMap.newKeySet(); // 캐시에 미리 선택되지 않았다는 데이터 넣어두자 private final ConcurrentHashMap squareLocks = new ConcurrentHashMap<>(); public Set viewSelectedSquares() { // 읽기 전용 뷰 반환 @@ -100,14 +100,6 @@ public void startNewRound() { assignRewardsToSquares(currentRound); } - @Transactional - public boolean noMoreJewel(User user) { - UserJewel userJewel = userJewelRepository.findByUserAndJewelType(user, PACHINKO_NEED_JEWEL_TYPE) - .orElseThrow(() -> new GeneralException(ErrorCode.USER_JEWEL_NOT_FOUND)); - - return userJewel.getCount() < PACHINKO_NEED_JEWEL_COUNT; - } - @Transactional public boolean canSelectMore(User user, Long round) { return userPachinkoRepository.countByUserAndRound(user, round) < PACHINKO_USER_MAX_SQUARES; @@ -134,7 +126,7 @@ public String selectSquare(User user, int squareNumber) { return "이미 선택된 칸입니다."; } - // 보석 여부 확인 후 차감 + // 보석 개수 확인 후 차감 userJewelService.deductUserJewel(user); log.info("빠칭코 칸 선택을 위해 B급 보석 하나를 지불하여 DB에서 보석을 차감했습니다."); diff --git a/src/main/java/LuckyVicky/backend/user/service/UserJewelService.java b/src/main/java/LuckyVicky/backend/user/service/UserJewelService.java index 155c763..4052d01 100644 --- a/src/main/java/LuckyVicky/backend/user/service/UserJewelService.java +++ b/src/main/java/LuckyVicky/backend/user/service/UserJewelService.java @@ -25,6 +25,7 @@ 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); }