Conversation
- BombRelayGameSchedulerConfig 추가로 폭탄 끝말잇기 스케줄러 설정 정의 - BombRelayGameTimingProperties, BombRelayDictionaryProperties 추가로 타이밍 및 API 프로퍼티 관리 - MiniGameType에 BOMB_RELAY 게임 타입 추가 - 테스트 환경에서 bombRelayGameScheduler 적용 (@Profile 및 테스트 전용 Bean 추가) - 관련 application.yml 및 application-test.yml 설정 업데이트 - StreamKey에 BOMB_RELAY_EVENTS 추가 - 폭탄 끝말잇기 관련 도메인 및 설정 통합
- BombRelayGame, BombRelayPlayer, BombRelayPlayers 도메인 클래스 추가 - BombRelayGameState 및 BombRelayGameErrorCode 열거형 정의 - BombRelayScore를 활용한 점수 및 생존/탈락 순위 체계 구현 - BombRelayGameTest, BombRelayPlayerTest, BombRelayPlayersTest 단위 테스트 작성 - 게임 상태, 라운드, 턴 및 단어 검증 로직 추가 - 생존자와 탈락자 순위를 기준으로 결과 생성 로직 구현 - 코드 가독성 및 상태 관리 안정성을 고려한 설계 및 테스트 보강
- BombRelayFinishedEvent, BombRelayProgressEvent, BombRelayStateChangedEvent 생성 - WordCommandEvent, WordResultEvent 클래스 구현 - BombRelayGame 데이터를 기반으로 이벤트 생성 메서드 추가 - TraceInfo 및 이벤트 ID 적용으로 추적 가능성 강화 - 데이터 구조 및 가독성을 고려한 설계 및 구현
- BombRelayGameProgressHandler 클래스 추가 - 단어 수락/거절 로직 및 로컬/사전 검증 처리 구현 - 검증 결과에 따른 WordResultEvent와 BombRelayProgressEvent 발행 - 게임 단어 로직 처리 및 로그 기록 추가 - BombRelayGameService 클래스 추가 - 폭탄 끝말잇기 게임 시작, 상태 전환 및 타이머 관리 로직 구현 - 게임 종료 및 결과 처리 로직 구현 - WordValidator 클래스 추가로 단어 유효성 검증을 위한 사전 API 호출 구현 - 캐싱 매커니즘 및 API 장애 시 허용 처리 로직 포함 - 관련 단위 테스트 및 통합 테스트 작성 - BombRelayGameProgressHandlerTest, WordValidatorTest 등 추가
- BombRelayGameMessagePublisher 클래스 구현 - 상태(State), 진행도(Progress), 단어 결과(WordResult) 메시지 발행 - 게임 종료 시 상태 메시지 발행 처리 추가 - BombRelayGameWebSocketController 클래스 구현 - 단어 입력 요청 처리 및 WordCommandEvent 이벤트 발행 로직 추가 - WordCommandEventConsumer 클래스 구현 - 단어 입력 이벤트 처리 및 예외 관리 로직 포함 - Response 객체 추가 - BombRelayProgressResponse, BombRelayStateResponse, WordResultResponse 클래스 생성 - 이벤트 데이터를 메시지 응답 형태로 직렬화 처리 - WordCommand 데이터 요청 클래스 추가 - 단어 입력 데이터 유효성 검증 및 관리
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
Walkthrough폭탄 끝말잇기(Bomb Relay) 미니게임을 새로 도입했습니다. 도메인(게임/플레이어/점수/상태), 애플리케이션 서비스(생명주기·스케줄링·타이머), 구성(타이밍·사전·스케줄러), 인프라(사전검증·이벤트·WS 퍼블리셔), UI(웹소켓 엔드포인트) 및 광범위한 단위·통합 테스트가 추가되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
actor Client
participant WS as WebSocket\r\nController
participant StreamPub as Stream\r\nPublisher
participant Consumer as Word\r\nCommand\r\nConsumer
participant Handler as Game\r\nProgress\r\nHandler
participant GameSvc as BombRelay\r\nGameService
participant Validator as Word\r\nValidator
participant Events as Application\r\nEventPublisher
participant Messenger as Message\r\nPublisher
Client->>WS: submitWord(joinCode, WordCommand)
WS->>WS: validate payload
WS->>StreamPub: publish(WordCommandEvent)
StreamPub->>Consumer: deliver(WordCommandEvent)
Consumer->>Handler: handleWord(joinCode, playerName, word)
Handler->>GameSvc: getBombRelayGame(room)
Handler->>Handler: game.validateWord(playerName, word)
alt REJECTED (local)
Handler->>Events: publish(WordResultEvent.rejected)
else NEEDS_DICTIONARY_CHECK
Handler->>Validator: isValidWord(word)
alt dict invalid
Handler->>Events: publish(WordResultEvent.rejected)
else dict valid
Handler->>GameSvc: acceptWord/update state
Handler->>Events: publish(WordResultEvent.accepted)
Handler->>Events: publish(BombRelayProgressEvent)
end
else ACCEPTED
Handler->>GameSvc: acceptWord/update state
Handler->>Events: publish(WordResultEvent.accepted)
Handler->>Events: publish(BombRelayProgressEvent)
end
Events->>Messenger: onEvent -> send WebSocketMessage
Messenger->>Client: push updates
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment Tip You can validate your CodeRabbit configuration file in your editor.If your editor has YAML language server, you can enable auto-completion and validation by adding |
There was a problem hiding this comment.
Actionable comments posted: 22
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@backend/src/main/java/coffeeshout/bombrelay/application/BombRelayGameProgressHandler.java`:
- Around line 26-54: handleWord currently calls BombRelayGame.validateWord(...)
then later game.acceptWord(), allowing two threads to interleave and
double-accept or double-publish; fix by moving validation+accept into an atomic
domain operation: add BombRelayGame.validateAndAcceptWord(String playerName,
String word) (internally synchronized or using a lock) that returns a
WordValidationResult/acceptance flag, replace the separate validateWord(...) +
acceptWord() calls in handleWord with a single call to
validateAndAcceptWord(...), and then publish WordResultEvent/
BombRelayProgressEvent only based on that single atomic result; alternatively
(if you prefer consumer-side serialization) enforce single-threaded handling per
joinCode at the consumer level (serialize by joinCode) but do not keep the
current two-step validate/accept flow in handleWord.
In
`@backend/src/main/java/coffeeshout/bombrelay/application/BombRelayGameService.java`:
- Around line 100-104: Public no-op method resetBombTimer in
BombRelayGameService is misleading; either remove it or make it private and
rename to communicate intentional no-op. Locate the method
resetBombTimer(BombRelayGame game, String joinCode) in class
BombRelayGameService and: if there are no call sites, delete the method and move
the explanatory comment into class-level or method-level documentation;
otherwise change its signature to private and rename to something explicit like
intentionallySkipBombTimerReset(...) and keep the comment inside the new/private
method to document the rationale. Ensure any references are updated or
compilation errors resolved after the change.
In
`@backend/src/main/java/coffeeshout/bombrelay/config/BombRelayGameSchedulerConfig.java`:
- Around line 18-25: The scheduler currently hardcodes setPoolSize(2) in
BombRelayGameSchedulerConfig which can cause timer delays under load; change
this to read a configurable property (e.g., bombRelay.scheduler.poolSize)
injected into the BombRelayGameSchedulerConfig (via constructor or
`@Value/`@ConfigurationProperties) and use that value when calling
scheduler.setPoolSize(...); ensure a sensible default is provided if the
property is absent and update any tests or wiring that instantiate the scheduler
to supply the configurable value.
In
`@backend/src/main/java/coffeeshout/bombrelay/config/BombRelayGameTimingProperties.java`:
- Around line 18-36: The constructor BombRelayGameTimingProperties should
validate the timer relationship by checking that
maxBombTimer.compareTo(minBombTimer) >= 0 (throw IllegalArgumentException if the
max is less than the min) in addition to the existing validatePositive checks,
and the randomBombDuration() method should use a thread-safe inclusive
upper-bound random generation (e.g., ThreadLocalRandom.current().nextLong(minMs,
maxMs + 1)) instead of Math.random() * (maxMs - minMs) to allow selecting
maxBombTimer and avoid off-by-one exclusion; update randomBombDuration() to
compute minMs/maxMs from minBombTimer/maxBombTimer and return
Duration.ofMillis(randomValue).
In `@backend/src/main/java/coffeeshout/bombrelay/domain/BombRelayGame.java`:
- Around line 111-140: validateWord and acceptWord are split causing a race
between validation, external dictionary checks, and acceptWord (leading to
inconsistent turnIndex/currentWord/usedWords); fix by making submission
acceptance atomic: either serialize all state-changing paths (submissions and
timer) with a per-game lock or single-thread executor so
validate+dictionary+accept runs without interleaving, or add a re-check snapshot
immediately before acceptWord (compare currentTurn/turnIndex/currentWord and
usedWords) and reject if changed. Update logic around validateWord, acceptWord,
turnIndex, currentWord and usedWords (and any timer path that mutates state) to
use the chosen serialization or snapshot revalidation approach so state updates
are atomic.
In `@backend/src/main/java/coffeeshout/bombrelay/domain/BombRelayPlayers.java`:
- Around line 27-29: The bug comes from applying the ever-increasing
BombRelayGame.turnIndex directly in BombRelayPlayers.getByTurnIndex against a
shrinking survivors list; replace this fragile modulus logic with a domain-level
rotation cursor that tracks the current survivor index (store e.g. currentCursor
in BombRelayPlayers or BombRelayGame), use that cursor to return the current
player and advance for next turn, and when removing a player adjust the cursor
(if removedIndex < cursor then decrement cursor; always normalize cursor =
cursor % survivors.size()) so removals do not shift the next player incorrectly;
update getByTurnIndex to use/return by that cursor (or add getCurrentAndAdvance)
and remove reliance on raw turnIndex.
In `@backend/src/main/java/coffeeshout/bombrelay/domain/BombRelayScore.java`:
- Around line 11-16: 주석에 남아 있는 반박(→ 아니다 ...) 문구를 제거하고 정렬 기준을 단일 문장으로 명확히 정리하세요:
BombRelayScore에서 랭킹은 fromAscending을 사용해 오름차순(생존자(0) 1등, 1라운드 탈락(1) 꼴등)으로 정렬된다는
설명만 남기고 fromDescending 관련 혼동되는 내용은 삭제하거나 주석에서 제거하세요; 필요한 경우
BombRelayScore.fromAscending 및 BombRelayScore.fromDescending 메서드 이름을 언급해 의도를 명확히
표기합니다.
- Around line 31-35: Validate inputs in BombRelayScore.ofEliminated by checking
eliminatedRound and maxRounds before computing the score: ensure maxRounds > 0,
eliminatedRound is between 1 and maxRounds (or a defined sentinel for survivors)
and throw an IllegalArgumentException if invalid; update the method
(BombRelayScore.ofEliminated) to perform these checks and only then call new
BombRelayScore(maxRounds - eliminatedRound + 1) so domain integrity is enforced
at the factory boundary.
In `@backend/src/main/java/coffeeshout/bombrelay/domain/KoreanCharUtils.java`:
- Around line 28-34: Add null/empty input validation inside the utility (Option
A): in getLastChar, getFirstChar and the isKorean-related methods referenced
(e.g., isKorean) check if word is null or word.isEmpty() and fail-fast by
throwing an IllegalArgumentException with a clear message (e.g., "word must not
be null or empty") so callers cannot pass invalid inputs; this centralizes
validation in the KoreanCharUtils methods rather than relying on callers.
In
`@backend/src/main/java/coffeeshout/bombrelay/domain/WordValidationResult.java`:
- Around line 3-24: The record currently allows invalid combinations (e.g.,
Status.REJECTED with a null BombRelayGameErrorCode) which can cause NPEs; add a
compact constructor inside WordValidationResult to validate the invariants: if
status == Status.REJECTED then require non-null errorCode, otherwise require
errorCode == null, and throw an IllegalArgumentException (or
NullPointerException) on violation; keep the existing static factories
(rejected, needsDictionaryCheck, accepted) unchanged so callers still work but
invalid direct construction is prevented by the compact constructor.
In
`@backend/src/main/java/coffeeshout/bombrelay/infra/messaging/consumer/WordCommandEventConsumer.java`:
- Around line 19-28: WordCommandEventConsumer.accept currently catches
InvalidStateException and Exception and only logs them, causing events to be
silently dropped; change it so InvalidStateException is converted into a
rejection path (emit or call the existing rejection mechanism/produce a
WordResultEvent failure for the client) while any other Exception is logged and
rethrown to let RedisStreamListenerStarter's stream-level failure/retry handle
it; specifically update accept to use progressHandler.handleWord and catch
InvalidStateException to invoke the rejection/emission routine, and in the
general catch block rethrow the exception after logging so
RedisStreamListenerStarter can perform retry/failure handling.
In `@backend/src/main/java/coffeeshout/bombrelay/infra/WordValidator.java`:
- Around line 25-48: The current cache (ConcurrentHashMap cache) in isValidWord
uses get/put causing duplicate external API calls on concurrent misses and is
unbounded; replace this with an atomic bounded cache strategy: either (A) switch
the cache usage to cache.computeIfAbsent(word, w -> queryDictionary(w)) inside
isValidWord to prevent duplicate lookups (still unbounded), or (B) introduce a
bounded/TTL cache (e.g., Caffeine) as the cache field and use its get(key, k ->
queryDictionary(k)) to provide both deduplication and eviction; update the field
declaration (cache) and isValidWord/constructor to initialize and use the chosen
cache implementation and ensure queryDictionary exceptions are
propagated/handled consistently.
In
`@backend/src/main/java/coffeeshout/bombrelay/ui/BombRelayGameWebSocketController.java`:
- Around line 24-26: The submitWord handler currently trusts client-sent
playerName (WordCommand) and publishes events via WordCommandEvent.create using
that value, enabling turn spoofing; update
BombRelayGameWebSocketController.submitWord to derive the player identity from
the server-side auth/session context (e.g., the WebSocket Principal or session
mapping) instead of command.playerName(), or, if opting for the alternative,
validate that command.playerName() matches the server-side
joinCode→session→player mapping before creating the BaseEvent; ensure
streamPublisher.publish(StreamKey.BOMB_RELAY_EVENTS, ...) uses the verified
server-side player identifier.
In `@backend/src/main/java/coffeeshout/bombrelay/ui/request/WordCommand.java`:
- Around line 5-8: The WordCommand record currently uses `@NotBlank` on playerName
and word but has no length limits; add `@Size`(max = ...) annotations to both
fields (e.g. `@Size`(max = 50) for playerName and `@Size`(max = 30 or 50) for word
or adjust to your policy) to prevent excessively long inputs, and import
javax.validation.constraints.Size; update the record declaration (WordCommand)
to include these annotations alongside `@NotBlank` so validation rejects oversized
payloads before downstream processing.
In
`@backend/src/main/java/coffeeshout/bombrelay/ui/response/BombRelayProgressResponse.java`:
- Around line 11-20: The response currently exposes domain type PlayerProgress
via BombRelayProgressResponse.from(BombRelayProgressEvent event) which couples
UI to domain and passes event.players() directly; create a UI DTO (e.g.,
PlayerProgressResponse) and change BombRelayProgressResponse.players to use that
DTO, mapping each PlayerProgress from BombRelayProgressEvent.players() to the
DTO inside BombRelayProgressResponse.from, and return an immutable/defensive
copy (e.g., unmodifiableList or copyOf) of the mapped list; if you prefer the
minimal change, instead wrap event.players() with a defensive copy when
assigning to BombRelayProgressResponse.players to avoid exposing mutable domain
collections while leaving types unchanged.
In
`@backend/src/test/java/coffeeshout/bombrelay/application/BombRelayGameProgressHandlerTest.java`:
- Around line 77-80: The test currently uses verify(eventPublisher,
atLeastOnce()) with ArgumentCaptor<WordResultEvent> which can miss duplicate
emissions and order regressions; update the assertions in
BombRelayGameProgressHandlerTest to explicitly verify exact call counts and
ordering by replacing atLeastOnce() with times(1) when checking the single
expected WordResultEvent and BombRelayProgressEvent, and additionally add an
InOrder (e.g., InOrder inOrder = inOrder(eventPublisher)) to assert that
publishEvent(WordResultEvent) occurs before
publishEvent(BombRelayProgressEvent); apply the same change pattern to the other
affected blocks referenced around the captor usage (lines similar to 112-114 and
147-150).
In `@backend/src/test/java/coffeeshout/bombrelay/domain/BombRelayGameTest.java`:
- Around line 140-157: The test currently computes a WordValidationResult but
never asserts anything; update BombRelayGameTest to assert the duplicate-word
rejection by ensuring the reused word also satisfies the first-letter rule and
then asserting the validation result equals the ALREADY_USED_WORD case.
Concretely, use game.acceptWord(...) and makeValidWord(...) to advance turns so
the next word's last character matches the first character of the originally
used validWord (or create a self-loop word like "자자" via makeValidWord to
guarantee first-letter match), then call game.validateWord(thirdTurnName,
validWord) and assert that the returned WordValidationResult is
WordValidationResult.ALREADY_USED_WORD (or the equivalent enum/constant used by
validateWord).
In
`@backend/src/test/java/coffeeshout/bombrelay/domain/BombRelayPlayersTest.java`:
- Around line 57-59: The test in BombRelayPlayersTest currently only asserts
survivorCount() and getSurvivors().hasSize(2), which can miss wrong exclusions;
update the test to assert the actual survivor identities too by checking that
the expected eliminated player is not present (e.g., assert that
players.getSurvivors() does not contain the eliminated player's name) or assert
the survivors' name set equals the expected set, referencing
players.survivorCount() and players.getSurvivors() to locate where to add the
additional assertion.
In `@backend/src/test/java/coffeeshout/bombrelay/domain/KoreanCharUtilsTest.java`:
- Around line 79-93: Add edge-case assertions to the KoreanCharUtilsTest (inside
the 한국어_판별 nested class) to cover empty string, whitespace-only, strings
containing digits and strings with special characters; specifically call
KoreanCharUtils.isKorean(...) for "" , " " , "사과123" and "사과!" and assert the
expected boolean (treat these as non-Korean and assert isKorean(...).isFalse()).
This ensures KoreanCharUtils.isKorean is validated against these real-world edge
inputs.
- Around line 43-76: The tests only cover 4 of the 16 entries in DUEUM_MAP;
extend KoreanCharUtilsTest by adding individual test methods that call
KoreanCharUtils.isValidFirstChar for each missing rule so all 16 two-eum rules
are verified: for the ㄴ group add tests for '뇨'→'요', '뉴'→'유', '니'→'이'; for the ㄹ
group add tests for '랴'→'야', '려'→'여', '례'→'예', '료'→'요', '래'→'내', '로'→'노',
'뢰'→'뇌', '루'→'누', '르'→'느'; follow the existing test pattern
(assertThat(KoreanCharUtils.isValidFirstChar(original, transformed)).isTrue())
and include the original→original case where appropriate to mirror the existing
"원래 글자도 허용" test.
In `@backend/src/test/resources/application-test.yml`:
- Around line 103-104: The test configuration is pointing to the real external
dictionary API via the api-url and api-key properties, making tests flaky;
update the test config (properties named api-url and api-key in
application-test.yml) to point to a controlled test endpoint: either replace
api-url with your local WireMock/MockServer URL (e.g.
http://localhost:<port>/api/search) and use a test api-key so you can stub
success/failure/timeouts for deterministic tests, or if you prefer the simpler
route set api-url to a deliberately non-routable URL to exercise your
fallback/error paths and adjust tests accordingly; ensure all tests and any HTTP
client setup that read api-url/api-key use the test config values.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 65bfe620-e964-4ab2-a204-7d3dbc89eadf
📒 Files selected for processing (40)
backend/src/main/java/coffeeshout/bombrelay/application/BombRelayGameProgressHandler.javabackend/src/main/java/coffeeshout/bombrelay/application/BombRelayGameService.javabackend/src/main/java/coffeeshout/bombrelay/config/BombRelayDictionaryProperties.javabackend/src/main/java/coffeeshout/bombrelay/config/BombRelayGameSchedulerConfig.javabackend/src/main/java/coffeeshout/bombrelay/config/BombRelayGameTimingProperties.javabackend/src/main/java/coffeeshout/bombrelay/domain/BombRelayGame.javabackend/src/main/java/coffeeshout/bombrelay/domain/BombRelayGameErrorCode.javabackend/src/main/java/coffeeshout/bombrelay/domain/BombRelayGameState.javabackend/src/main/java/coffeeshout/bombrelay/domain/BombRelayPlayer.javabackend/src/main/java/coffeeshout/bombrelay/domain/BombRelayPlayers.javabackend/src/main/java/coffeeshout/bombrelay/domain/BombRelayScore.javabackend/src/main/java/coffeeshout/bombrelay/domain/KoreanCharUtils.javabackend/src/main/java/coffeeshout/bombrelay/domain/WordValidationResult.javabackend/src/main/java/coffeeshout/bombrelay/domain/event/BombRelayFinishedEvent.javabackend/src/main/java/coffeeshout/bombrelay/domain/event/BombRelayProgressEvent.javabackend/src/main/java/coffeeshout/bombrelay/domain/event/BombRelayStateChangedEvent.javabackend/src/main/java/coffeeshout/bombrelay/domain/event/WordCommandEvent.javabackend/src/main/java/coffeeshout/bombrelay/domain/event/WordResultEvent.javabackend/src/main/java/coffeeshout/bombrelay/infra/WordValidator.javabackend/src/main/java/coffeeshout/bombrelay/infra/messaging/BombRelayGameMessagePublisher.javabackend/src/main/java/coffeeshout/bombrelay/infra/messaging/consumer/WordCommandEventConsumer.javabackend/src/main/java/coffeeshout/bombrelay/ui/BombRelayGameWebSocketController.javabackend/src/main/java/coffeeshout/bombrelay/ui/request/WordCommand.javabackend/src/main/java/coffeeshout/bombrelay/ui/response/BombRelayProgressResponse.javabackend/src/main/java/coffeeshout/bombrelay/ui/response/BombRelayStateResponse.javabackend/src/main/java/coffeeshout/bombrelay/ui/response/WordResultResponse.javabackend/src/main/java/coffeeshout/global/redis/stream/StreamKey.javabackend/src/main/java/coffeeshout/minigame/domain/MiniGameType.javabackend/src/main/resources/application.ymlbackend/src/test/java/coffeeshout/bombrelay/application/BombRelayGameProgressHandlerTest.javabackend/src/test/java/coffeeshout/bombrelay/domain/BombRelayGameTest.javabackend/src/test/java/coffeeshout/bombrelay/domain/BombRelayPlayerTest.javabackend/src/test/java/coffeeshout/bombrelay/domain/BombRelayPlayersTest.javabackend/src/test/java/coffeeshout/bombrelay/domain/BombRelayScoreTest.javabackend/src/test/java/coffeeshout/bombrelay/domain/KoreanCharUtilsTest.javabackend/src/test/java/coffeeshout/bombrelay/infra/WordValidatorTest.javabackend/src/test/java/coffeeshout/global/config/IntegrationTestConfig.javabackend/src/test/java/coffeeshout/global/config/ServiceTestConfig.javabackend/src/test/java/coffeeshout/global/outbox/OutboxE2ETest.javabackend/src/test/resources/application-test.yml
backend/src/main/java/coffeeshout/bombrelay/application/BombRelayGameProgressHandler.java
Show resolved
Hide resolved
backend/src/main/java/coffeeshout/bombrelay/application/BombRelayGameService.java
Outdated
Show resolved
Hide resolved
backend/src/main/java/coffeeshout/bombrelay/config/BombRelayGameSchedulerConfig.java
Show resolved
Hide resolved
backend/src/main/java/coffeeshout/bombrelay/config/BombRelayGameTimingProperties.java
Show resolved
Hide resolved
backend/src/test/java/coffeeshout/bombrelay/domain/BombRelayGameTest.java
Show resolved
Hide resolved
backend/src/test/java/coffeeshout/bombrelay/domain/BombRelayPlayersTest.java
Show resolved
Hide resolved
backend/src/test/java/coffeeshout/bombrelay/domain/KoreanCharUtilsTest.java
Show resolved
Hide resolved
backend/src/test/java/coffeeshout/bombrelay/domain/KoreanCharUtilsTest.java
Show resolved
Hide resolved
- BombRelayGameTimingProperties에 roundResultDelay 프로퍼티 추가 - BombRelayStateChangedEvent에 eliminatedPlayerName 필드 추가 - BombRelayGameService에서 게임 상태 전환 시 타이밍 프로퍼티 활용 - BombRelayStateResponse에 탈락자 정보 반영 - application.yml 및 application-test.yml 타이밍 설정 보완 - 관련 로직 및 WebSocketResponse 데이터 직렬화 업데이트
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@backend/src/main/java/coffeeshout/bombrelay/application/BombRelayGameService.java`:
- Around line 77-98: handleBombExploded can be invoked more than once causing
double-elimination; make it idempotent by checking a cancellation/processed flag
before proceeding: change BombRelayGame.cancelBombTimer() to return a boolean
(or add cancelBombTimerIfActive()) that returns true if it actually cancelled an
active timer, or add a synchronized processed flag on BombRelayGame (e.g.,
isBombExplodedProcessed) that you set true when handling starts; then in
handleBombExploded(BombRelayGame, String) check the returned boolean or the
processed flag and return immediately if already handled, only proceeding with
eliminateCurrentPlayer(), updateState(), eventPublisher.publishEvent(...) and
scheduling startNextRound when not previously processed.
- Around line 147-156: The BombRelayGame.bombTimerFuture field is accessed
without synchronization causing TOCTOU and visibility races between scheduler
threads; update the game to use an atomic holder or synchronized access: replace
the plain ScheduledFuture<?> field with an AtomicReference<ScheduledFuture<?>>
(or make setBombTimerFuture() and cancelBombTimer() synchronized) and change
scheduleBombTimer() to store the future via game.getBombTimerRef().set(...) (or
call the synchronized setter), and change cancelBombTimer() to use atomic
compareAndSet / getAndSet to null before calling cancel() to ensure the
null-check and cancel() are atomic and visible across threads (refer to methods
scheduleBombTimer, setBombTimerFuture, cancelBombTimer and
BombRelayGame.bombTimerFuture in your changes).
In `@backend/src/main/resources/application.yml`:
- Around line 61-62: YAML stream keys are wrapped in brackets (e.g.
"[bombrelay]") which do not match the StreamKey enum values and cause
StreamPublisher.publish() to fail when it checks
redisStreamProperties.keys().containsKey(key.getRedisKey()); update
application.yml to remove the surrounding brackets for all redis.stream.keys
entries so the keys exactly match the enum (e.g. bombrelay, room, room:join,
cardgame:select, etc.), then re-run tests to ensure StreamPublisher.publish()
and the redisStreamProperties validation succeed.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 13af94f5-58cd-4289-a959-85940e4004e7
📒 Files selected for processing (7)
backend/src/main/java/coffeeshout/bombrelay/application/BombRelayGameService.javabackend/src/main/java/coffeeshout/bombrelay/config/BombRelayGameTimingProperties.javabackend/src/main/java/coffeeshout/bombrelay/domain/event/BombRelayStateChangedEvent.javabackend/src/main/java/coffeeshout/bombrelay/infra/messaging/BombRelayGameMessagePublisher.javabackend/src/main/java/coffeeshout/bombrelay/ui/response/BombRelayStateResponse.javabackend/src/main/resources/application.ymlbackend/src/test/resources/application-test.yml
backend/src/main/java/coffeeshout/bombrelay/application/BombRelayGameService.java
Show resolved
Hide resolved
backend/src/main/java/coffeeshout/bombrelay/application/BombRelayGameService.java
Show resolved
Hide resolved
- minBombTimer 및 maxBombTimer 값 변경 - 게임 진행 시간 조정으로 플레이 균형 최적화
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
backend/src/main/resources/application.yml (1)
61-62:⚠️ Potential issue | 🔴 CriticalRedis Stream 키 대괄호 표기는 발행/구독 키 불일치를 유발합니다
Line 61-62의
"[bombrelay]"는 코드에서 사용하는bombrelay와 문자열이 달라, 발행 검증 단계에서 예외가 날 수 있습니다.
대안은 두 가지입니다.
- 옵션 A: YAML 키에서 대괄호를 제거 (
bombrelay,room,room:join등 전부 통일)
- 장점: publisher/listener/enum 키 일관성 확보, 런타임 실패 제거
- 단점: 기존에 대괄호 키로 쌓인 스트림이 있다면 마이그레이션 고려 필요
- 옵션 B: 코드에서 키 정규화 계층 추가 (대괄호 포함/미포함 모두 허용)
- 장점: 하위 호환 유지 가능
- 단점: 복잡도 증가, 키 정책 모호화
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/main/resources/application.yml` around lines 61 - 62, The YAML key "[bombrelay]" currently includes square brackets which mismatch the code's expected key "bombrelay" and can cause publish/subscribe verification failures; fix by renaming the YAML key to bombrelay (and similarly normalize other keys like room or room:join) so config strings match the publisher/listener/enum values used in code, or alternatively implement a key-normalization layer in the publishing/consuming paths (e.g., in your publisher and listener entrypoints) to strip or add brackets so both "[bombrelay]" and "bombrelay" map to the same internal key.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@backend/src/main/resources/application.yml`:
- Around line 105-106: The bomb timer values in application.yml (min-bomb-timer
and max-bomb-timer) currently read 18000ms/40000ms which contradicts the PR spec
of 10000ms–25000ms; either restore these keys to 10000ms and 25000ms to match
requirements (adjust min-bomb-timer and max-bomb-timer) or, if the 18–40s change
is intentional, keep the current values and update the spec/tests/docs that
reference the 10–25s rule so they reflect the new 18000ms/40000ms range.
- Line 111: The current property api-key uses a permissive default
(${STDICT_API_KEY:}) which allows startup with an empty key; change it to
require the env var (use ${STDICT_API_KEY} i.e. remove the trailing colon) and
add a startup validation that fails fast if the api-key is blank (validate the
bound property and exit/throw on missing STDICT_API_KEY), or alternatively move
the permissive default into a profile-specific file (e.g., application-local.yml
or test profile) so only local/test runs get a default while production remains
required.
---
Duplicate comments:
In `@backend/src/main/resources/application.yml`:
- Around line 61-62: The YAML key "[bombrelay]" currently includes square
brackets which mismatch the code's expected key "bombrelay" and can cause
publish/subscribe verification failures; fix by renaming the YAML key to
bombrelay (and similarly normalize other keys like room or room:join) so config
strings match the publisher/listener/enum values used in code, or alternatively
implement a key-normalization layer in the publishing/consuming paths (e.g., in
your publisher and listener entrypoints) to strip or add brackets so both
"[bombrelay]" and "bombrelay" map to the same internal key.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 3b03fc3c-bdce-42b8-bd5a-2854e81fd1ca
📒 Files selected for processing (1)
backend/src/main/resources/application.yml
- prod 및 dev 환경 docker-compose.yml에 STDICT_API_KEY 변수 추가 - .
- BombRelayGameTimingProperties에 minBombTimer > maxBombTimer 검증 로직 추가 - WordCommand에 단어 길이 제한(@SiZe(max = 50)) 유효성 검사 도입 - BombRelayGameTest에서 단어 재사용(ALREADY_USED_WORD) 검증 테스트 보강 - BombRelayGameService의 불필요한 resetBombTimer 메서드 제거
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@backend/src/test/java/coffeeshout/bombrelay/domain/BombRelayGameTest.java`:
- Around line 250-264: The test method named 생존자는_공동_1등이다 does not match its
assertions: it currently runs game.startRound(), game.startPlaying(),
game.eliminateCurrentPlayer(), then checks game.getResult() and asserts
rank1Count == 1 (single winner). Fix by either renaming the test to 단독_생존자는_1등이다
to match the single-winner scenario, or change the setup to produce a
shared-first scenario (e.g., start with 4+ players, run rounds via
game.startRound()/game.startPlaying() and eliminate players such that at least
two survivors remain) and then assert that result.getRank() contains multiple
entries with rank == 1 (rank1Count >= 2).
- Around line 234-244: The tests in BombRelayGameTest call tryFinish() directly
from PLAYING, which doesn't mirror real flow where isGameOver() must be true
first; change both tests to set the game into a game-over condition (so
game.isGameOver() == true) before invoking game.tryFinish(): for the first test
ensure game.isGameOver() returns true then assert tryFinish() is true and state
equals BombRelayGameState.DONE, and for the second test set isGameOver true,
call tryFinish() once then assert the second call to game.tryFinish() returns
false. Ensure you use the existing game fixture and any provided helper methods
to transition to game-over rather than altering tryFinish() implementation.
- Around line 293-298: The helper findOtherPlayerName currently hardcodes player
names ("한스", "꾹이", "루키") which breaks if PlayerFixture changes; update it to
derive candidates dynamically (e.g., from the current game instance via
game.getPlayers() or from PlayerFixture constants) and then filter out
currentTurnName, returning the first match or a clear test failure message;
reference the findOtherPlayerName method and use the game object or
PlayerFixture's exposed constants/collection instead of literal strings.
- Around line 103-109: Test uses BombRelayGame.setUp with only one player which
violates the domain minimum of 2 players and may mask real behavior; change the
test to setUp with two players (e.g., 한스 and another player) and then call
validateWord without calling startRound/startPlaying to assert it throws
InvalidStateException in DESCRIPTION state, or if you intentionally want to test
a 1-player edge case, add a comment clarifying that setUp(List.of(한스)) is
deliberate so reviewers know the single-player scenario is expected; update
references in the test to BombRelayGame, setUp, validateWord and optionally
startRound/startPlaying accordingly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: d8608800-a5d0-453d-a0ad-fe5e044e0e7f
📒 Files selected for processing (7)
backend/docker/dev/docker-compose.ymlbackend/docker/prod/.env.examplebackend/docker/prod/docker-compose.ymlbackend/src/main/java/coffeeshout/bombrelay/application/BombRelayGameService.javabackend/src/main/java/coffeeshout/bombrelay/config/BombRelayGameTimingProperties.javabackend/src/main/java/coffeeshout/bombrelay/ui/request/WordCommand.javabackend/src/test/java/coffeeshout/bombrelay/domain/BombRelayGameTest.java
backend/src/test/java/coffeeshout/bombrelay/domain/BombRelayGameTest.java
Show resolved
Hide resolved
backend/src/test/java/coffeeshout/bombrelay/domain/BombRelayGameTest.java
Show resolved
Hide resolved
backend/src/test/java/coffeeshout/bombrelay/domain/BombRelayGameTest.java
Show resolved
Hide resolved
backend/src/test/java/coffeeshout/bombrelay/domain/BombRelayGameTest.java
Show resolved
Hide resolved
| if (result.requiresDictionaryCheck()) { | ||
| if (!wordValidator.isValidWord(word)) { |
There was a problem hiding this comment.
저도 첨엔 같이 넣었는데 보다보니 레벨이 다른 두 단계라 일부러 나눠놨습니다.
requiresDictionaryCheck은 로컬 검증 통과해서 사전 확인이 필요한 상태인지 판별이고, isValidWord는 실제 외부 API 호출이에요. requireDicktionaryCheck에서 거부되면 외부 API를 찌를 필요도 없으니까 나눠놨습니다.
There was a problem hiding this comment.
제가 잘못 알고 있었네요. and로 묶어도 선행되는 조건문이 false면 바로 나오는 게 맞아서 반영했습니다.
| private final TaskScheduler taskScheduler; | ||
| private final ApplicationEventPublisher eventPublisher; | ||
| private final BombRelayGameTimingProperties timing; | ||
| private final GameDurationMetricService gameDurationMetricService; |
There was a problem hiding this comment.
사용되지 않는 필드는 제거하는 게 좋을 거 같습니다
| @NotNull Duration description, | ||
| @NotNull Duration prepare, | ||
| @NotNull Duration minBombTimer, | ||
| @NotNull Duration maxBombTimer, | ||
| @NotNull Duration roundResultDelay, | ||
| @NotNull Duration resultDelay |
There was a problem hiding this comment.
여기도 @DurationMin 을 사용하거나 별도의 Duration 유틸리티로 분리해서 검증을 관리하는 방향이 괜찮지 않나 생각합니다.
There was a problem hiding this comment.
@DurationMin 적용할게요
|
|
||
| public record BombRelayStateChangedEvent( | ||
| String joinCode, | ||
| String state, |
There was a problem hiding this comment.
String 대신에 BombRelayGameState 를 활용하고 웹소켓쪽에서 .name을 써서 파싱하는 방향으로 하는 것이 적절하지 않을까 생각합니다.
There was a problem hiding this comment.
이거 확인해보니까 모든 게임이 전부 String으로 state를 받고 있습니다.
다른 pr 파서 따로 해결할까 했는데, 하는 김에.. 여기서 다 하죠..
| public static BombRelayFinishedEvent of(BombRelayGame game, String joinCode) { | ||
| return new BombRelayFinishedEvent(joinCode, game.getState().name()); | ||
| } | ||
| } |
There was a problem hiding this comment.
제가 이해한 바로는 BombRelayFinishedEvent는 게임이 완전 종료될 때 사용되는 것으로 보입니다. 그런데 별도의 BombRelayFinishedEvent를 두기보다 BombRelayStateChangedEvent 를 eventListner로 받아올 때 State가 Done인 경우, publishFineshed 이벤트를 던지는 방향이 불필요한 생성을 막는데 도움이 되지 않을까 생각합니다.
만약 시간 지연을 통해서 보내야 한다면 MEssagePublisher에서 TaskScheduler를 불러와서 처리하면 될 거 같다고 생각합니다. 물론 메시지 레이어에 타이밍 로직이 섞이는 문제점은 발생합니다. 아니면 클라이언트 레벨에서 DONE인 경우 자체 타임아웃으로 변경해도 괜찮지 않나 생각합니다. (이 방향이 더 낫다고는 생각함)
There was a problem hiding this comment.
FinishedEvent 별도로 둔 이유는 발행 시점이 다르기 때문이에요. finishGame()에서 StateChangedEvent(DONE)은 즉시 발행해서 폭발 연출 시작하고, FinishedEvent는 roundResultDelay(4초) 후에 발행해서 최종 정리 메시지 보내고 있습니다. 같은 이벤트를 시간차로 두 번 발행하면 EventListener에서 구분이 안 되기도 해요.
그리고 꾹이가 말한 것처럼 Publisher에서 DONE 감지 후 타이머 거는 방향은, 메시징 레이어에 비즈니스 타이밍 로직이 들어가게 되는 게 맞습니다. "4초 후에 보내"는 Service 레이어 판단이지 Publisher 관심사가 아니라는 거에 저도 동의합니다.
또 클라이언트 자체 타임아웃은 이미 프론트에서 DONE 받으면 3500ms 후 결과 이동 하고 있는데, FinishedEvent가 하는 건 결과 이동 트리거가 아니라 state 토픽에 최종 정리 메시지 발행이라 서버가 보장해야 하는 영역이라고 생각해요.
그럼에도 수정을 해야한다면, 이건 위 리뷰처럼 단순한 필드값 상태를 변경하는 정도가 아니라서 따로 pr을 파야할 것 같아요. 현재 BlindTimer, SpeedTouch, RacingGame 전부 같은 형식이거든요
| private final BombRelayDictionaryProperties properties; | ||
| private final HttpClient httpClient; | ||
| private final ObjectMapper objectMapper; | ||
| private final ConcurrentMap<String, Boolean> cache = new ConcurrentHashMap<>(); |
There was a problem hiding this comment.
지금 바로 고려 안해도 되지만 확장성과 메모리 관리(TTL)를 위해 Redis에 보관하는 방안이 낫지 않을까 생각합니다~
- GameDurationMetricService 관련 import 및 필드 삭제 - 생성자에서 GameDurationMetricService 주입 코드 제거 - 코드 간소화 및 유지보수성 향상
- SpeedTouch, BombRelay, BlindTimer, RacingGame 관련 Event 클래스에서 상태 필드 타입을 Enum으로 변경 - WebSocketResponse 직렬화 시 .name() 호출 추가 - 타입 안정성을 위해 코드 전반적인 리팩토링 진행 - 관련 Response 클래스와 MessagePublisher 클라이언트 코드 수정 및 업데이트
- Duration 필드에 @DurationMin(nanos = 1) 애노테이션 추가로 유효성 검사 대체 - SpeedTouch, BombRelay, BlindTimer, CardGame 관련 validatePositive 메서드 제거 - 코드 간소화 및 유지보수성 향상
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
backend/src/main/java/coffeeshout/blindtimer/infra/messaging/BlindTimerGameMessagePublisher.java (1)
39-45: 🧹 Nitpick | 🔵 Trivial일관성 향상을 위한 팩토리 메서드 추가 고려
publishStateChanged는BlindTimerStateResponse.from(event)팩토리 메서드를 사용하지만,publishFinished는 생성자를 직접 호출하고 있습니다. 두 가지 접근 방식이 혼재되어 있어 코드 일관성이 다소 떨어집니다.개선 방안:
- 팩토리 메서드 추가 (권장):
BlindTimerStateResponse에fromFinished(BlindTimerFinishedEvent)메서드를 추가하여 finished 상태의 응답 생성 로직을 캡슐화합니다. 이렇게 하면0, 0값의 의미(종료 시 남은 시간 없음)가 명확해지고, 향후 finished 응답 형식 변경 시 한 곳만 수정하면 됩니다.- 현행 유지: 단순한 케이스라면 현재 방식도 동작에는 문제없습니다.
♻️ 팩토리 메서드 추가 예시
BlindTimerStateResponse.java에 추가:public static BlindTimerStateResponse fromFinished(BlindTimerFinishedEvent event) { return new BlindTimerStateResponse(event.state().name(), 0, 0); }
BlindTimerGameMessagePublisher.java에서 사용:`@EventListener` public void publishFinished(BlindTimerFinishedEvent event) { messagingTemplate.convertAndSend( String.format(STATE_DESTINATION_FORMAT, event.joinCode()), - WebSocketResponse.success(new BlindTimerStateResponse(event.state().name(), 0, 0)) + WebSocketResponse.success(BlindTimerStateResponse.fromFinished(event)) ); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/main/java/coffeeshout/blindtimer/infra/messaging/BlindTimerGameMessagePublisher.java` around lines 39 - 45, The publishFinished method in BlindTimerGameMessagePublisher constructs BlindTimerStateResponse directly while publishStateChanged uses BlindTimerStateResponse.from(event); add a factory method on BlindTimerStateResponse (e.g., public static BlindTimerStateResponse fromFinished(BlindTimerFinishedEvent event)) that returns the finished-state response (encapsulating the "0, 0" values), then update publishFinished to call that new fromFinished factory to keep construction logic consistent with publishStateChanged.
♻️ Duplicate comments (3)
backend/src/main/java/coffeeshout/bombrelay/config/BombRelayGameTimingProperties.java (1)
28-33: 🛠️ Refactor suggestion | 🟠 Major
randomBombDuration()상한값 미포함 및 스레드 안전성 문제.
Math.random() * (maxMs - minMs)수식은[0.0, 1.0)범위를 반환하므로 결과가[minMs, maxMs)가 되어 상한값(maxBombTimer)이 절대 선택되지 않습니다. 또한Math.random()은 내부적으로 동기화를 사용하여 멀티스레드 환경에서 성능 저하가 발생할 수 있습니다.문제점:
- 상한값 제외: "18
40초" 설정 시 실제로는 1839.999초만 가능- 스레드 경합: 동시 게임 시작 시 성능 이슈 가능성
권장 수정:
🔧 ThreadLocalRandom 사용으로 개선
+import java.util.concurrent.ThreadLocalRandom; + public Duration randomBombDuration() { final long minMs = minBombTimer.toMillis(); final long maxMs = maxBombTimer.toMillis(); - final long randomMs = minMs + (long) (Math.random() * (maxMs - minMs)); + final long randomMs = ThreadLocalRandom.current().nextLong(minMs, maxMs + 1); return Duration.ofMillis(randomMs); }
ThreadLocalRandom.nextLong(origin, bound)는[origin, bound)범위이므로maxMs + 1을 사용하면 상한값이 포함됩니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/main/java/coffeeshout/bombrelay/config/BombRelayGameTimingProperties.java` around lines 28 - 33, randomBombDuration() currently uses Math.random(), which excludes the upper bound and isn't ideal for multithreaded use; change it to use ThreadLocalRandom.current().nextLong(origin, bound) inside randomBombDuration (e.g., compute minMs and maxMs then call ThreadLocalRandom.current().nextLong(minMs, maxMs + 1L) to include the upper bound), ensure you import java.util.concurrent.ThreadLocalRandom, and guard against overflow if maxMs == Long.MAX_VALUE (handle by using nextLong(minMs, maxMs) in that edge case or clamp maxMs + 1 safely).backend/src/main/java/coffeeshout/bombrelay/application/BombRelayGameService.java (2)
73-94:⚠️ Potential issue | 🟠 Major
handleBombExploded()중복 호출 시 잘못된 플레이어가 탈락할 수 있습니다.스케줄러 콜백이 두 번 실행되거나 네트워크 지연으로 재시도될 경우, 첫 번째 호출에서 플레이어 A가 탈락한 후 두 번째 호출에서 다음 턴의 플레이어 B가 의도치 않게 탈락합니다.
cancelBombTimer()가 실제 취소 성공 여부를 반환하지 않아 멱등성 보장이 불가능합니다.문제 흐름:
- 스레드 1:
cancelBombTimer()→ 타이머 취소- 스레드 2:
cancelBombTimer()→ 이미 취소됨 (no-op)- 스레드 1:
eliminateCurrentPlayer()→ 플레이어 A 탈락, turnIndex 증가- 스레드 2:
eliminateCurrentPlayer()→ 플레이어 B 탈락 (오류)권장 방안:
cancelBombTimer()를 boolean 반환 메서드로 변경하고, 실제 취소 성공 시에만 진행:🔒 멱등성 보장 예시
public void handleBombExploded(BombRelayGame game, String joinCode) { - game.cancelBombTimer(); + if (!game.cancelBombTimerIfActive()) { + log.debug("폭발 이미 처리됨: joinCode={}", joinCode); + return; + } final String eliminatedName = game.getCurrentTurnPlayer().getName();
BombRelayGame.cancelBombTimerIfActive()구현:public synchronized boolean cancelBombTimerIfActive() { if (bombTimerFuture != null && !bombTimerFuture.isDone()) { bombTimerFuture.cancel(false); bombTimerFuture = null; return true; } return false; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/main/java/coffeeshout/bombrelay/application/BombRelayGameService.java` around lines 73 - 94, The handler can be invoked twice causing the wrong player to be eliminated; change the cancellation to return a boolean (or add a new synchronized method like cancelBombTimerIfActive() that checks bombTimerFuture and cancels only if active) and in handleBombExploded() call that boolean-returning cancellation and proceed to call eliminateCurrentPlayer() and publish events only when the cancel method returned true; ensure the new cancel method nulls the future after cancelling and is synchronized to guarantee idempotence.
137-146:⚠️ Potential issue | 🟠 Major
bombTimerFuture필드 접근 시 동기화가 여전히 필요합니다.
BombRelayGame.bombTimerFuture는@Setter로 생성된 단순 setter를 사용하며,volatile이나 동기화 없이 스케줄러 스레드 간 공유됩니다.BombRelayGameSchedulerConfig의poolSize=2설정에 따라 여러 스케줄러 스레드가 동시에 접근할 수 있습니다.구체적인 문제:
cancelBombTimer()의 null 체크와cancel()호출 사이에 TOCTOU 경합 발생 가능- 비휘발성 필드로 인해 한 스레드의 쓰기가 다른 스레드에서 즉시 관찰되지 않을 수 있음
권장 해결책:
- AtomicReference 사용 (권장):
AtomicReference<ScheduledFuture<?>>도입 후getAndSet(null)활용
- 장점: 기존 상태 관리 패턴(
AtomicReference<BombRelayGameState>)과 일관성- synchronized 메서드:
cancelBombTimer()와setBombTimerFuture()를 동기화
- 장점: 변경 최소화
🔒 AtomicReference 적용 예시 (BombRelayGame)
-@Setter -private ScheduledFuture<?> bombTimerFuture; +private final AtomicReference<ScheduledFuture<?>> bombTimerRef = new AtomicReference<>(); +public void setBombTimerFuture(ScheduledFuture<?> future) { + bombTimerRef.set(future); +} +public boolean cancelBombTimerIfActive() { + ScheduledFuture<?> future = bombTimerRef.getAndSet(null); + if (future != null && !future.isDone()) { + future.cancel(false); + return true; + } + return false; +}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/main/java/coffeeshout/bombrelay/application/BombRelayGameService.java` around lines 137 - 146, The bombTimerFuture field is not thread-safe; replace the plain ScheduledFuture<?> field in BombRelayGame with an AtomicReference<ScheduledFuture<?>> and update setBombTimerFuture(ScheduledFuture<?>) to use atomicRef.set(..) (or setAndGet) and change cancelBombTimer() to use atomicRef.getAndSet(null) then cancel the returned future if non-null, preventing TOCTOU races; also update BombRelayGameService.scheduleBombTimer(...) to call the new setter (or atomicRef.set) so all scheduler threads observe updates safely.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@backend/src/main/java/coffeeshout/bombrelay/application/BombRelayGameService.java`:
- Around line 100-116: The event publication order in finishGame is ambiguous to
consumers: BombRelayFinishedEvent is scheduled after timing.roundResultDelay()
via taskScheduler.schedule(() ->
eventPublisher.publishEvent(BombRelayFinishedEvent.of(game, joinCode)),
Instant.now().plus(timing.roundResultDelay())), while MiniGameFinishedEvent is
published immediately with eventPublisher.publishEvent(new
MiniGameFinishedEvent(joinCode, MiniGameType.BOMB_RELAY.name())); add a concise
comment above these calls in BombRelayGameService.finishGame describing the
intended ordering and reason (i.e., MiniGameFinishedEvent is immediate for
scoreboard/cleanup and BombRelayFinishedEvent is delayed for round-result
display), referencing finishGame, BombRelayFinishedEvent, MiniGameFinishedEvent,
taskScheduler.schedule, and tryFinish for context so future maintainers
understand this is intentional.
- Around line 61-71: startNextRound currently calls startRound() and
startPlaying() unconditionally which can cause race-induced state mismatches;
modify startNextRound to first validate and atomically transition game state
(e.g., require current state is PREPARE or ROUND_RESULT) before calling
startRound()/startPlaying() — either use a compareAndSet style method on the
game's state field or add an early-return when the state is not
PREPARE/ROUND_RESULT; keep event publishing (BombRelayStateChangedEvent.of,
BombRelayProgressEvent.of) and scheduleBombTimer(game, joinCode) only after the
atomic transition succeeds to avoid duplicate/overlapping rounds.
In
`@backend/src/main/java/coffeeshout/racinggame/domain/event/RaceFinishedEvent.java`:
- Around line 6-9: Unify the component order between RaceFinishedEvent and
RaceStateChangedEvent by changing RaceFinishedEvent's record components to
(String joinCode, RacingGameState state); update the record declaration for
RaceFinishedEvent and adjust its factory method RaceFinishedEvent.of(RacingGame
racingGame, String joinCode) to construct the record with new ordering (pass
joinCode first, then racingGame.getState()), and then run a quick project-wide
search to fix any call sites that construct RaceFinishedEvent directly so the
parameter order matches RaceStateChangedEvent.
---
Outside diff comments:
In
`@backend/src/main/java/coffeeshout/blindtimer/infra/messaging/BlindTimerGameMessagePublisher.java`:
- Around line 39-45: The publishFinished method in
BlindTimerGameMessagePublisher constructs BlindTimerStateResponse directly while
publishStateChanged uses BlindTimerStateResponse.from(event); add a factory
method on BlindTimerStateResponse (e.g., public static BlindTimerStateResponse
fromFinished(BlindTimerFinishedEvent event)) that returns the finished-state
response (encapsulating the "0, 0" values), then update publishFinished to call
that new fromFinished factory to keep construction logic consistent with
publishStateChanged.
---
Duplicate comments:
In
`@backend/src/main/java/coffeeshout/bombrelay/application/BombRelayGameService.java`:
- Around line 73-94: The handler can be invoked twice causing the wrong player
to be eliminated; change the cancellation to return a boolean (or add a new
synchronized method like cancelBombTimerIfActive() that checks bombTimerFuture
and cancels only if active) and in handleBombExploded() call that
boolean-returning cancellation and proceed to call eliminateCurrentPlayer() and
publish events only when the cancel method returned true; ensure the new cancel
method nulls the future after cancelling and is synchronized to guarantee
idempotence.
- Around line 137-146: The bombTimerFuture field is not thread-safe; replace the
plain ScheduledFuture<?> field in BombRelayGame with an
AtomicReference<ScheduledFuture<?>> and update
setBombTimerFuture(ScheduledFuture<?>) to use atomicRef.set(..) (or setAndGet)
and change cancelBombTimer() to use atomicRef.getAndSet(null) then cancel the
returned future if non-null, preventing TOCTOU races; also update
BombRelayGameService.scheduleBombTimer(...) to call the new setter (or
atomicRef.set) so all scheduler threads observe updates safely.
In
`@backend/src/main/java/coffeeshout/bombrelay/config/BombRelayGameTimingProperties.java`:
- Around line 28-33: randomBombDuration() currently uses Math.random(), which
excludes the upper bound and isn't ideal for multithreaded use; change it to use
ThreadLocalRandom.current().nextLong(origin, bound) inside randomBombDuration
(e.g., compute minMs and maxMs then call
ThreadLocalRandom.current().nextLong(minMs, maxMs + 1L) to include the upper
bound), ensure you import java.util.concurrent.ThreadLocalRandom, and guard
against overflow if maxMs == Long.MAX_VALUE (handle by using nextLong(minMs,
maxMs) in that edge case or clamp maxMs + 1 safely).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: f0cc372d-f093-4831-927e-2187d759be7f
📒 Files selected for processing (19)
backend/src/main/java/coffeeshout/blindtimer/config/BlindTimerGameTimingProperties.javabackend/src/main/java/coffeeshout/blindtimer/domain/event/BlindTimerFinishedEvent.javabackend/src/main/java/coffeeshout/blindtimer/domain/event/BlindTimerStateChangedEvent.javabackend/src/main/java/coffeeshout/blindtimer/infra/messaging/BlindTimerGameMessagePublisher.javabackend/src/main/java/coffeeshout/blindtimer/ui/response/BlindTimerStateResponse.javabackend/src/main/java/coffeeshout/bombrelay/application/BombRelayGameService.javabackend/src/main/java/coffeeshout/bombrelay/config/BombRelayGameTimingProperties.javabackend/src/main/java/coffeeshout/bombrelay/domain/event/BombRelayFinishedEvent.javabackend/src/main/java/coffeeshout/bombrelay/domain/event/BombRelayStateChangedEvent.javabackend/src/main/java/coffeeshout/bombrelay/infra/messaging/BombRelayGameMessagePublisher.javabackend/src/main/java/coffeeshout/bombrelay/ui/response/BombRelayStateResponse.javabackend/src/main/java/coffeeshout/cardgame/config/CardGameTimingProperties.javabackend/src/main/java/coffeeshout/racinggame/domain/event/RaceFinishedEvent.javabackend/src/main/java/coffeeshout/racinggame/domain/event/RaceStateChangedEvent.javabackend/src/main/java/coffeeshout/racinggame/infra/messaging/RacingGameMessagePublisher.javabackend/src/main/java/coffeeshout/speedtouch/config/SpeedTouchGameTimingProperties.javabackend/src/main/java/coffeeshout/speedtouch/domain/event/SpeedTouchFinishedEvent.javabackend/src/main/java/coffeeshout/speedtouch/domain/event/SpeedTouchStateChangedEvent.javabackend/src/main/java/coffeeshout/speedtouch/infra/messaging/SpeedTouchGameMessagePublisher.java
backend/src/main/java/coffeeshout/bombrelay/application/BombRelayGameService.java
Show resolved
Hide resolved
backend/src/main/java/coffeeshout/bombrelay/application/BombRelayGameService.java
Show resolved
Hide resolved
backend/src/main/java/coffeeshout/racinggame/domain/event/RaceFinishedEvent.java
Show resolved
Hide resolved
- requiresDictionaryCheck와 단어 유효성 검사 조건을 단일 if문으로 통합 - 불필요한 중첩 조건 제거로 코드 가독성 향상 및 유지보수성 강화
|
🗑️ Branch |
* [fix] Redis Stream 비동기 이벤트 Trace Context 전파 (#1105) * feat: RedisStream Trace 데이터 및 ContextSnapshotFactory 적용 - RedisStreamThreadPoolConfig에 ContextSnapshotFactory 추가 - Traceable 인터페이스 적용 및 관련 로직 추가 - RouletteWinnerEvent, RouletteShownEvent, TapCommandEvent에서 TraceInfo 필드 구현 - 이벤트 생성 시 TraceInfoExtractor 적용 - Runnable Task에 ContextSnapshotFactory 기반 TaskDecorator 설정 - RedisStream의 트래킹 및 Trace 데이터 활용을 위한 확장. * feat: 테스트 의존성 micrometer-tracing-test 추가 - 테스트 환경에서 micrometer-tracing을 사용하는 테스트 구현을 위해 의존성 추가 - build.gradle.kts 업데이트 * test: Redis Stream Trace Context 전파 및 TracerProvider 단위 테스트 추가 - RedisStreamContextPropagationTest 구현 - Observation 내에서 이벤트 발행 시 TraceId 전파 검증 로직 추가 - Observation 외부에서 발행된 이벤트의 처리 정상 동작 테스트 - 여러 이벤트 동시 발행 및 TraceId 전파 테스트 - TracerProviderTest 구현 - 유효한 TraceInfo가 주어졌을 때 Span 생성 및 TraceId 복원 테스트 - 빈 TraceInfo 시 바로 task 실행 및 Span 미생성 테스트 - 별도 스레드에서 Trace Context 복원 테스트 포함 - 예외 발생 시 Span 종료 로직 테스트 - Trace Context 관련 기능의 안정성 및 신뢰성 강화 * test: TracerProvider 및 Redis Stream 테스트 개선 - TracerProviderTest - SimpleSpan을 활용한 Span 종료 시점 검증 추가 - 예외 발생 시 Span 종료 여부 검증 로직 강화 - 불필요한 주석 수정 및 코드 가독성 향상 - RedisStreamContextPropagationTest - E2E 전파 흐름 문서화 보완 및 메서드명 개선 - 여러 이벤트 동시 발행 시 각 이벤트의 traceId 독립성 검증 추가 - CompletableFuture 및 CountDownLatch를 활용해 동시성 테스트 개선 - 빈 traceId 이벤트의 처리 로직 검증 강화 * fix: TracerProvider에서 예외 발생 시 Span 오류 기록 추가 - 예외 발생 시 `span.error()`를 호출해 Span에 오류 정보 기록 - Span 종료 전 예외 재throw 처리 추가 - 안정성 및 오류 추적 로직 강화 * test: TracerProviderTest 예외 발생 시 Span 오류 기록 및 종료 검증 추가 - 예외 발생 시 `span.error()` 호출로 오류 정보 기록 검증 로직 추가 - finally 블록에서 `span.end()` 호출 여부 확인 - 테스트 가독성을 높이기 위해 기존 불필요한 주석 수정 및 구조 개선 * test: RedisStreamContextPropagationTest에 TracerProvider 호출 검증 추가 - TracerProvider.executeWithTraceContext 호출 여부 검증 로직 추가 - 각 이벤트 처리 시 TraceInfo의 traceId 올바른 전달 여부 확인 - 빈 TraceInfo 이벤트 처리 시 TracerProvider 호출 검증 강화 - 기존 주석 개선 및 메서드명 변경으로 테스트 가독성 향상 * test: TracerProvider 및 Redis Stream 컨텍스트 테스트 개선 - TracerProviderTest - finally 블록에서 `executor.shutdownNow()` 호출 추가 - 비동기 태스크 처리 완료 후 리소스 정리 로직 강화 - RedisStreamContextPropagationTest - finally 블록에서 `executor.shutdownNow()` 호출 추가 - CompletableFuture 및 스레드풀 리소스 정리 로직 개선 - 테스트 안정성 및 리소스 관리 강화 * chore: GameRecoveryService 로깅 수준 info에서 debug로 변경 - 복구 메시지 저장, 조회 시 로깅 수준을 debug로 조정하여 로그 노이즈 감소 - 중요하지 않은 로깅 항목의 가독성 및 유지보수성 향상 * chore: 로그 패턴 설정 개선 (traceId 가독성 및 추적성 강화) - 콘솔 로그 패턴 변경: traceId 앞 8자리만 출력하여 가독성 향상 - 파일 로그 패턴 변경: traceId, spanId 전체 출력으로 검색 및 추적성 강화 - 로깅 구조 개선 및 유지보수성 향상 * [refactor] 카드 게임 리팩터링 (#1107) * refactor: 카드 게임 흐름 오케스트라 구현 및 시간 설정 외부화 * refactor: 패키지 구조에서 `minigame` 제거 및 경로 단순화 * refactor: 테스트 클래스 패키지 경로 정리 및 불필요한 import 제거 * test: 카드 게임 통합 테스트 개선 및 조기 종료 테스트 검증 * refactor: 카드 게임 흐름 오케스트라 리팩토링 및 총 라운드 수 설정 추가 * refactor: 카드 게임 흐름 오케스트라 joinCode 의존성 제거 및 테스트 스케줄러 모킹 추가 * refactor: 조기 종료 트리거 인터페이스 확장 및 카드 게임 흐름 리팩토링 * test: 카드 게임 통합 테스트에 타이밍 값 상수화 및 카드 정보 메시지 검증 추가 * refactor: 카드 게임 알림 예외 처리 추가 및 스케줄러 빈 초기화 수정 * refactor: TaskScheduler로 카드 게임 흐름 스케줄러 리팩토링 및 ScheduledExecutorService 제거 * refactor: 라운드 정보 체계 개선 및 관련 코드 전반 리팩토링 * refactor: 카드 선택 로직에 라운드 종료 반환값 추가 및 관련 코드 리팩토링 * refactor: 타이밍 속성 Duration 타입으로 변경 및 관련 코드 리팩토링 * refactor: CardGameRound를 통한 동시성 문제 해소 * [feat] Transactional Outbox 패턴 도입 — createRoom Dual Write Problem 해결 (#1109) * feat: Outbox 즉시 발행 및 PENDING 복구 워커 추가 - 트랜잭션 커밋 직후 Redis Stream에 이벤트를 즉시 발행하는 OutboxAfterCommitRelay 구현 - OutboxEventRecorder로 Outbox 이벤트 저장 및 Redis 발행 트리거 처리 - PENDING 상태 이벤트를 복구하고 Redis로 릴레이하는 OutboxRelayWorker 추가 - OutboxEvent 상태 관리 로직을 담당하는 OutboxEventProcessor 구현 - OutboxEvent 엔티티 및 상태 Enum(OutboxStatus) 정의 - Outbox 테이블의 PUBLISHED 레코드 주기적 정리를 위한 스케줄러 추가 - Redis 장애 시에도 이벤트 유실 방지를 위한 보완 로직 적용 - 즉시 발행 실패 시 비동기 워커 기반 재시도 처리로 안정성 강화 * feat: Room 생성 및 참가 이벤트 Outbox 경로로 발행 로직 변경 - Room 생성 이벤트를 OutboxEventRecorder로 DB에 기록 후 Redis로 발행하도록 변경 - 트랜잭션 커밋 직후 AFTER_COMMIT으로 Redis Stream에 즉시 발행 (0ms 지연) - Redis 장애 발생 시 Worker가 500ms 후 재시도 처리 추가 - Room 참가 이벤트는 요청-응답 패턴에 따라 기존 방식 유지 * test: Outbox 관련 단위 및 통합 테스트 추가 - OutboxAfterCommitRelayTest: AFTER_COMMIT 리스너 동작 검증 테스트 추가 - OutboxRelayWorkerTest: PENDING 상태 이벤트 복구 및 릴레이 관련 테스트 구현 - OutboxEventProcessorTest: 이벤트 상태 전이, 실패 처리 등 로직 검증 테스트 추가 - OutboxE2ETest: MySQL + Redis 기반 Outbox 패턴 E2E 테스트 작성 - OutboxEventRecorderTest: Outbox 이벤트 기록 및 저장 관련 동작 테스트 추가 - OutboxEventTest: 이벤트 상태 전이 및 실패 처리 테스트 구현 - Outbox 시스템 전반의 동작 검증과 안정성을 확인하기 위한 테스트 강화 * feat: OutboxEvent에 updatedAt 필드 추가 및 갱신 로직 구현 - OutboxEvent 엔티티에 updatedAt 필드 추가 - 상태 변경 메서드(markInProgress, markPublished 등) 호출 시 updatedAt 값 갱신 처리 - recoverStaleInProgressEvents 쿼리에서 createdAt 대신 updatedAt을 기준으로 변경 - OutboxE2ETest에 스케줄러 관련 경합 방지 설정 추가 (no-op TaskScheduler 적용) * feat: recoverStaleInProgressEvents 쿼리에 updatedAt 갱신 로직 추가 - status를 'PENDING'으로 변경 시 updatedAt에 CURRENT_TIMESTAMP 설정 - 이벤트 상태 복구 시 갱신 시간 반영으로 데이터 일관성 강화 * refactor: OutboxEventRepository에서 불필요한 메서드 제거 - findById 메서드 삭제 - 사용되지 않는 import 정리 - 코드 간소화 및 유지보수성 향상 * refactor: LocalDateTime에서 Instant로 변경하여 시간 처리 일관성 향상 - OutboxEvent, Repository, Worker 등 관련 클래스에서 LocalDateTime → Instant로 데이터 타입 변경 - 시간 간격 계산 로직 Duration 사용으로 단순화 - 시간 데이터 처리 시 타임존 이슈 제거 및 정확성 개선 - 전체 변경 사항에 따라 관련 메서드 및 쿼리 수정 * refactor: recoverStaleInProgressEvents 메서드 매개변수 추가를 통한 시간 갱신 처리 개선 - 쿼리에서 updatedAt 값을 :now 매개변수로 변경하여 CURRENT_TIMESTAMP 의존성 제거 - 메서드 호출 시 즉시 갱신 시간을 전달받아 데이터 일관성과 테스트 편의성 강화 * refactor: Instant에서 LocalDateTime으로 변경하여 시간 처리 일관성 향상 - OutboxEvent, Repository, Worker 등 관련 클래스에서 Instant → LocalDateTime으로 데이터 타입 변경 - 시간 계산 로직 Duration -> minusXxx API로 간소화 - 관련 메서드 및 쿼리 전체 수정 - 타임존 의존성 제거 및 테스트 편의성 강화 * test: OutboxE2ETest에 CardGameFlowScheduler 목(mock) 빈 등록 로직 추가 - E2E 테스트 컨텍스트에서 CardGameFlowScheduler 빈 누락 문제 해결을 위해 Mockito 기반 목 추가 - @Profile("!test") 설정으로 인해 테스트 프로파일에서 빈이 등록되지 않는 상황 보완 - 테스트 환경 의존성 개선 및 경합 방지 설정 강화 * refactor: LocalDateTime에서 Instant로 변경하여 시간 처리 일관성 및 정확성 강화 - OutboxEvent, Repository, Worker 등 관련 클래스에서 LocalDateTime → Instant로 데이터 타입 변경 - 시간 간격 계산 로직 minusXxx API 및 ChronoUnit 사용으로 간소화 - Hibernate CURRENT_TIMESTAMP 의존성 제거 후 즉시 갱신 시간(now) 전달로 데이터 일관성 개선 - 타임존 이슈 제거 및 테스트 편의성 강화 - 관련 메서드, 쿼리 전체 수정 및 로직 정비 * feat: OutboxEvent 테이블 생성 - OutboxEvent 테이블 DDL 추가 - 상태 + ID 인덱스(idx_outbox_status_id) 정의 - UTF-8MB4 인코딩 및 InnoDB 엔진 설정 적용 * [feat] 스피드 터치 미니게임 추가 (#1111) * feat: 스피드 터치 미니게임 스케줄러 및 타이밍 설정 추가 - SpeedTouchGameSchedulerConfig 추가로 스케줄러 설정 정의 - SpeedTouchGameTimingProperties 추가로 타이밍 관련 프로퍼티 관리 - MiniGameType에 SPEED_TOUCH 게임 타입 추가 - 테스트 환경에서 speedTouchGameScheduler 적용 (@Profile 및 테스트 전용 Bean 추가) - 관련 application.yml 및 application-test.yml 설정 업데이트 - awaitility 의존성 추가로 비동기 테스트 지원 강화 * feat: 스피드 터치 미니게임 도메인 및 테스트 클래스 추가 - SpeedTouchGame, SpeedTouchPlayer, SpeedTouchPlayers 도메인 클래스 추가 - SpeedTouchGameState 및 SpeedTouchGameErrorCode 열거형 정의 - SpeedTouchScore를 활용한 랭킹 및 점수 계산 로직 구현 - 도메인 관련 단위 테스트(SpeedTouchGameTest, SpeedTouchPlayerTest, SpeedTouchPlayersTest) 추가 - 게임 상태 관리, 플레이어 진행도 추적 및 결과 계산 기능 정상 동작 검증 - 간결하고 명확한 점수, 상태 계산 및 예외 처리 로직 제공 * feat: 스피드 터치 미니게임 도메인 이벤트 클래스 추가 - SpeedTouchFinishedEvent, SpeedTouchProgressEvent, SpeedTouchStateChangedEvent, TouchProgressCommandEvent 클래스 생성 - SpeedTouchGame 데이터 활용한 상태, 진행도, 결과 생성 로직 구현 - TraceInfo 및 이벤트 ID 생성 적용하여 이벤트 추적성 강화 - 이벤트 상태 및 데이터 전달 구조 개선 * feat: 스피드 터치 미니게임 애플리케이션 서비스 및 테스트 작성 - SpeedTouchGameProgressHandler 및 SpeedTouchGameService 클래스 추가 - 스피드 터치 진행도 처리 및 게임 종료 로직 구현 - SpeedTouchGameService를 활용한 스케줄러 기반 타이밍 관리 추가 - 애플리케이션 이벤트 발행 및 상태 전환 로직 포함 - 관련 단위 테스트(SpeedTouchGameProgressHandlerTest, SpeedTouchGameServiceTest) 작성 - Touch, 이벤트 발행, 게임 종료 등의 케이스 검증 - 코드 가독성과 유지보수성을 고려한 구조 설계 및 테스트 보강 * feat: 스피드 터치 미니게임 WebSocket 컨트롤러 및 메시지 발행 로직 구현 - WebSocket 기반 SpeedTouchGameWebSocketController 추가 - 터치 요청 처리 및 TouchProgressCommandEvent 이벤트 발행 - SpeedTouchGameMessagePublisher 구현 - 게임 상태(State), 진행도(Progress), 완료(Finished) 메시지 발행 로직 포함 - 스피드 터치 게임 통합 테스트(SpeedTouchGameIntegrationTest) 추가 - 게임 상태 전환 및 진행도 브로드캐스트 검증 - 메시지 응답 객체(SpeedTouchStateResponse, SpeedTouchProgressResponse) 추가 - 게임 상태 및 진행도 데이터 직렬화 지원 - TouchProgressEventConsumer 추가 - 터치 프로세스 처리 및 예외 로깅 강화 - 관련 도메인 이벤트 처리 및 통합 로직 보강 * refactor: SpeedTouch 상수 도입 및 tryFinish 원자성 보장 - SpeedTouchPlayer의 FIRST_NUMBER, LAST_NUMBER 상수 도입 - 반복문 및 조건문에서 상수 활용으로 가독성 및 유지보수성 강화 - SpeedTouchGame에 AtomicBoolean을 사용한 tryFinish 원자성 보장 - 중복적인 게임 종료 호출 방지 및 안정성 강화 - SpeedTouchGameService의 finishGame 로직 수정 - tryFinish 결과에 따라 중복 처리 방지 및 안정성 확보 - 관련 테스트 코드 업데이트: - 상수 적용 및 시나리오 검증 테스트 추가 - 예외 처리 및 종료 처리 검증 로직 강화 - build.gradle.kts에 awaitilityVersion 상수화 * refactor: SpeedTouchGameTimingProperties에 타이밍 유효성 검증 로직 추가 및 SpeedTouchFinishedEvent 필드 순서 변경 - SpeedTouchGameTimingProperties에 유효성 검증 메서드(validatePositive) 추가 - prepare, playing 등의 타이밍이 0보다 크지 않을 경우 예외 발생 처리 - SpeedTouchFinishedEvent 필드 순서를 joinCode, state 순으로 변경 - 필드 정렬로 가독성 및 일관성 개선 - 관련 생성자 및 팩토리 메서드 로직 함께 수정 * feat: 테스트 스케줄러 speedTouchGameScheduler 빈 추가 - 테스트 환경에서 스피드 터치 게임 스케줄러 빈(speedTouchGameScheduler) 생성 - OutboxE2ETest에서 테스트 스케줄러로 활용 가능하도록 설정 - @bean name 설정을 통한 빈 식별성 강화 - 관련 테스트 경합 방지 및 확장성 개선 * refactor: SpeedTouch 도메인 클래스 필드에 volatile 추가하여 동시성 문제 해결 - SpeedTouchGame의 state 및 startTime 필드에 volatile 키워드 추가 - 멀티스레드 환경에서의 데이터 일관성 보장 - SpeedTouchPlayer의 currentNumber 및 finishTime 필드에 volatile 키워드 추가 - 상태 변경 시 최신 값 반영 보장 - 동시성 안정성 및 코드의 신뢰성 강화 * refactor: SpeedTouchGameState의 duration 필드 제거 및 enum 생성자 단순화 - duration 필드와 관련 생성자 제거 - 상태 값만 가지는 단순화된 enum 구조로 변경 - 코드 간소화 및 불필요한 필드 제거로 유지보수성 향상 * refactor: SpeedTouchPlayer 동기화 및 @Getter 추가로 스레드 안전성 강화 - currentNumber, finishTime 필드에서 volatile 제거 후 동기화(synchronized) 적용 - 멀티스레드 환경에서의 데이터 일관성 및 안정성 보장 - currentNumber, finishTime에 대한 getter 메서드 추가 - @Getter 어노테이션 클래스 선언부로 이동 - 관련 메서드 동기화로 상태 변경 시 안정성 강화 * refactor: SpeedTouchPlayers 동기화 및 불변성 강화 - players 리스트 초기화 시 Collections.synchronizedList에서 List.copyOf로 변경 - 불변 리스트 활용으로 동기화 및 불필요한 오버헤드 제거 - isAllFinished 메서드에 synchronized 추가 - 멀티스레드 환경에서의 안정성 보장 - getPlayers 메서드 반환값을 Collections.unmodifiableList에서 불변 리스트로 단순화 - 코드 간결화 및 일관성 강화 * refactor: SpeedTouchGame 상태 관리 방식 개선 및 tryFinish 로직 변경 - SpeedTouchGameState 상태를 AtomicReference로 변경 - 멀티스레드 환경에서의 안정성 보장 및 불필요한 volatile 제거 - tryFinish 메서드 로직 변경 - AtomicReference의 compareAndSet으로 원자성 및 명확한 상태 전환 로직 구현 - SpeedTouchGameService에서 불필요한 상태 업데이트 로직(remove updateState) 제거 - 중복 코드 제거로 코드 간결화 - 관련 메서드 및 예외 처리 로직(getState, validatePlaying 등) 수정 - state 활용 방식 일관성 유지 및 안정성 강화 * test: tryFinish 테스트에 상태 검증 추가 - tryFinish 호출 시 true 반환 후 상태가 DONE으로 변경되는지 검증하는 테스트 추가 - 테스트 케이스 명확성 및 안정성 강화 * refactor: 불필요한 awaitility 의존성 제거 - 테스트 의존성에서 awaitility 제거 - build.gradle.kts 의존성 정의 간결화 및 유지보수성 향상 * [chore] 백엔드 CLAUDE.MD 추가 (#1116) * feat: add architecture documentation and production code conventions * feat: update documentation for production code and testing conventions * docs: update documentation on events, testing, and production conventions - Clarified Spring bean requirements for event consumers - Updated test base class usage and removed redundant `@IntegrationTest` annotation - Added guideline to use `final` for local variables within methods - Refined testing guidelines for time and random value handling in business logic * docs: refine production conventions and testing guidelines - Added single responsibility principle for class design - Expanded notes on metadata handling for `eventId` and `timestamp` - Updated example to use `markdown` syntax in skill documentation * docs: update production conventions - Added `final` requirement for instance variables - Clarified service responsibility to avoid transaction script pattern * docs: update formatting in testing and production conventions - Improved table formatting for better readability - Removed unnecessary character from production conventions - Refined testing guideline structure in skill documentation * [feat] 뇌피셜 초시계(블라인드 타이머) 미니게임 백엔드 구현 (#1120) * feat: 블라인드 타이머 미니게임 추가 - BlindTimerGameSchedulerConfig 추가로 블라인드 타이머 스케줄러 설정 정의 - BlindTimerGameTimingProperties 추가로 타이밍 관련 프로퍼티 관리 - MiniGameType에 BLIND_TIMER 게임 타입 추가 - 테스트 환경에서 blindTimerGameScheduler 적용 (@Profile 및 테스트 전용 Bean 추가) - 관련 application.yml 및 application-test.yml 설정 업데이트 - StreamKey에 BLIND_TIMER_EVENTS 추가 - BlindTimer 관련 도메인 및 설정 통합 * feat: 블라인드 타이머 미니게임 도메인 및 테스트 클래스 추가 - BlindTimerGame, BlindTimerPlayer, BlindTimerPlayers 도메인 클래스 구현 - BlindTimerGameState 및 BlindTimerGameErrorCode 열거형 정의 - BlindTimerScore를 활용한 오차 기반 게임 점수 계산 로직 구현 - BlindTimerGameTest, BlindTimerPlayerTest, BlindTimerPlayersTest 단위 테스트 작성 - 게임 상태, 플레이어 진행 상태 및 점수 계산 로직 검증 - 타임아웃 및 게임 종료 관련 처리 로직 추가 - 코드 가독성과 유지보수성을 고려한 설계 및 테스트 보강 * feat: 블라인드 타이머 이벤트 클래스 추가 - BlindTimerFinishedEvent, BlindTimerProgressEvent, BlindTimerStateChangedEvent, StopCommandEvent 클래스 생성 - 게임 상태, 진행도, 완료 및 정지 명령 이벤트 데이터 클래스 구현 - BlindTimerGame 데이터를 활용한 이벤트 생성 메서드 추가 - 이벤트 추적을 위한 TraceInfo 및 이벤트 ID 적용 - 데이터 구조 및 가독성을 고려한 클래스 설계 및 정의 * feat: 블라인드 타이머 게임 진행 처리 및 서비스 로직 추가 - BlindTimerGameProgressHandler 생성 및 STOP 처리 로직 구현 - BlindTimerGameService 추가로 게임 시작, 종료 및 상태 전환 기능 구현 - BlindTimer 관련 애플리케이션 이벤트 발행, 타이밍 관리 및 스케줄링 로직 완성 - STOP 요청 시 이벤트 발행 및 모든 플레이어 정지 시 게임 종료 처리 - BlindTimer 진행 및 상태 변경 테스트 작성 - 멀티스레드 환경에서도 안정적인 게임 상태 전환 보장 * feat: 블라인드 타이머 게임 WebSocket 컨트롤러 및 메시지 발행 로직 추가 - BlindTimerGameWebSocketController 구현으로 STOP 요청 처리 및 StopCommandEvent 이벤트 발행 - BlindTimerGameMessagePublisher 추가로 STATE, PROGRESS, DONE 상태 메시지 브로드캐스트 - BlindTimerGameIntegrationTest 작성으로 상태 전환 및 STOP 요청 테스트 - BlindTimerProgressResponse, BlindTimerStateResponse로 메시지 직렬화 지원 - StopCommandEventConsumer 구현으로 비동기 STOP 요청 처리 로직 완성 * feat: 블라인드 타이머 및 스피드 터치 게임 예외 처리 및 로직 개선 - BlindTimerScore에 음수 오차 값에 대한 예외 처리 로직 추가 - SpeedTouchGame 상태 변경 시 state 설정 순서 조정으로 안정성 강화 - 게임 도메인 로직 명확성 및 데이터 유효성 보장 * refactor: BlindTimerGameSchedulerConfig 및 SpeedTouchGameSchedulerConfig에서 불필요한 initialize() 호출 제거 - 스케줄러 초기화 중복 호출 제거로 코드 간소화 - 불필요한 로직 제거로 유지보수성 향상 * refactor: BlindTimerGame의 state 필드 접근 제한자 변경 및 state 설정 순서 개선 - state 필드에 @Getter(AccessLevel.NONE) 추가하여 직접 접근 제한 - startPlaying 메서드 내 state 설정 순서 변경으로 코드 가독성 강화 및 명확성 개선 * refactor: BlindTimerGame 및 SpeedTouchGame의 finishGame, getGame 메서드 접근 제한자 변경 - finishGame 및 getGame 메서드 public으로 변경하여 외부 접근 가능하도록 수정 - 코드 일관성 및 메서드 호출 범위 명확성 강화 * refactor: BlindTimer 도메인 및 테스트 전반에 Duration 적용 - BlindTimerGame, BlindTimerPlayer 등 도메인 클래스에서 long 타입 대신 Duration 사용으로 시간 표현 명확화 - BlindTimer 관련 테스트에서 Duration으로 변경된 메서드 호출 방식 수정 - 시간 계산 및 로직의 가독성과 일관성 개선 - toMillis, ofMillis 등의 사용을 최소화하여 타입 안정성 확보 * refactor: BlindTimerScore 오차 계산 메서드 수정 및 테스트 보강 - ofNormal 메서드에 targetTime과 stoppedElapsed를 활용한 오차 계산 방식 적용 - BlindTimerGame에서 BlindTimerScore 생성 로직 수정으로 코드 간소화 - BlindTimerScoreTest에 오차 방향과 관계없이 양수 값 유지 테스트 추가 - 테스트 케이스 전반에서 Duration 기반 로직 반영 및 가독성 향상 * refactor: BlindTimerGameServiceTest에서 중복 import 제거 - Duration 중복 import 제거로 코드 정리 - 테스트 클래스의 가독성 향상 및 불필요한 코드 제거 * refactor: BlindTimerGameService의 timeoutFuture 스케줄링 로직 수정 - game 시작 시간을 기준으로 timeout 계산 로직 변경 - 코드 가독성 및 타이밍 정확성 개선 * [feat] 폭탄 끝말잇기 미니게임 백엔드 구현 (#1125) * chore: 폭탄 끝말잇기 인프라 설정 - BombRelayGameSchedulerConfig 추가로 폭탄 끝말잇기 스케줄러 설정 정의 - BombRelayGameTimingProperties, BombRelayDictionaryProperties 추가로 타이밍 및 API 프로퍼티 관리 - MiniGameType에 BOMB_RELAY 게임 타입 추가 - 테스트 환경에서 bombRelayGameScheduler 적용 (@Profile 및 테스트 전용 Bean 추가) - 관련 application.yml 및 application-test.yml 설정 업데이트 - StreamKey에 BOMB_RELAY_EVENTS 추가 - 폭탄 끝말잇기 관련 도메인 및 설정 통합 * feat: 폭탄 끝말잇기 미니게임 도메인 및 테스트 구현 - BombRelayGame, BombRelayPlayer, BombRelayPlayers 도메인 클래스 추가 - BombRelayGameState 및 BombRelayGameErrorCode 열거형 정의 - BombRelayScore를 활용한 점수 및 생존/탈락 순위 체계 구현 - BombRelayGameTest, BombRelayPlayerTest, BombRelayPlayersTest 단위 테스트 작성 - 게임 상태, 라운드, 턴 및 단어 검증 로직 추가 - 생존자와 탈락자 순위를 기준으로 결과 생성 로직 구현 - 코드 가독성 및 상태 관리 안정성을 고려한 설계 및 테스트 보강 * feat: 폭탄 끝말잇기 이벤트 클래스 추가 - BombRelayFinishedEvent, BombRelayProgressEvent, BombRelayStateChangedEvent 생성 - WordCommandEvent, WordResultEvent 클래스 구현 - BombRelayGame 데이터를 기반으로 이벤트 생성 메서드 추가 - TraceInfo 및 이벤트 ID 적용으로 추적 가능성 강화 - 데이터 구조 및 가독성을 고려한 설계 및 구현 * feat: 폭탄 끝말잇기 게임 진행 및 단어 검증 로직 구현 - BombRelayGameProgressHandler 클래스 추가 - 단어 수락/거절 로직 및 로컬/사전 검증 처리 구현 - 검증 결과에 따른 WordResultEvent와 BombRelayProgressEvent 발행 - 게임 단어 로직 처리 및 로그 기록 추가 - BombRelayGameService 클래스 추가 - 폭탄 끝말잇기 게임 시작, 상태 전환 및 타이머 관리 로직 구현 - 게임 종료 및 결과 처리 로직 구현 - WordValidator 클래스 추가로 단어 유효성 검증을 위한 사전 API 호출 구현 - 캐싱 매커니즘 및 API 장애 시 허용 처리 로직 포함 - 관련 단위 테스트 및 통합 테스트 작성 - BombRelayGameProgressHandlerTest, WordValidatorTest 등 추가 * feat: 폭탄 끝말잇기 WebSoc정ket 메시지 발행 및 단어 처리 로직 추가 - BombRelayGameMessagePublisher 클래스 구현 - 상태(State), 진행도(Progress), 단어 결과(WordResult) 메시지 발행 - 게임 종료 시 상태 메시지 발행 처리 추가 - BombRelayGameWebSocketController 클래스 구현 - 단어 입력 요청 처리 및 WordCommandEvent 이벤트 발행 로직 추가 - WordCommandEventConsumer 클래스 구현 - 단어 입력 이벤트 처리 및 예외 관리 로직 포함 - Response 객체 추가 - BombRelayProgressResponse, BombRelayStateResponse, WordResultResponse 클래스 생성 - 이벤트 데이터를 메시지 응답 형태로 직렬화 처리 - WordCommand 데이터 요청 클래스 추가 - 단어 입력 데이터 유효성 검증 및 관리 * feat: 폭탄 끝말잇기 게임 로직 개선 및 탈락자 정보 추가 - BombRelayGameTimingProperties에 roundResultDelay 프로퍼티 추가 - BombRelayStateChangedEvent에 eliminatedPlayerName 필드 추가 - BombRelayGameService에서 게임 상태 전환 시 타이밍 프로퍼티 활용 - BombRelayStateResponse에 탈락자 정보 반영 - application.yml 및 application-test.yml 타이밍 설정 보완 - 관련 로직 및 WebSocketResponse 데이터 직렬화 업데이트 * feat: 폭탄 끝말잇기 타이밍 설정 업데이트 - minBombTimer 및 maxBombTimer 값 변경 - 게임 진행 시간 조정으로 플레이 균형 최적화 * feat: STDICT_API_KEY 환경 변수 추가 - prod 및 dev 환경 docker-compose.yml에 STDICT_API_KEY 변수 추가 - . * feat: 폭탄 끝말잇기 유효성 검사 및 단어 처리 로직 개선 - BombRelayGameTimingProperties에 minBombTimer > maxBombTimer 검증 로직 추가 - WordCommand에 단어 길이 제한(@SiZe(max = 50)) 유효성 검사 도입 - BombRelayGameTest에서 단어 재사용(ALREADY_USED_WORD) 검증 테스트 보강 - BombRelayGameService의 불필요한 resetBombTimer 메서드 제거 * refactor: BombRelayGameService에서 불필요한 GameDurationMetricService 의존성 제거 - GameDurationMetricService 관련 import 및 필드 삭제 - 생성자에서 GameDurationMetricService 주입 코드 제거 - 코드 간소화 및 유지보수성 향상 * refactor: 상태(State) 필드 타입 String에서 Enum으로 수정 - SpeedTouch, BombRelay, BlindTimer, RacingGame 관련 Event 클래스에서 상태 필드 타입을 Enum으로 변경 - WebSocketResponse 직렬화 시 .name() 호출 추가 - 타입 안정성을 위해 코드 전반적인 리팩토링 진행 - 관련 Response 클래스와 MessagePublisher 클라이언트 코드 수정 및 업데이트 * refactor: 게임 타이밍 프로퍼티 클래스의 유효성 검사 로직 단순화 - Duration 필드에 @DurationMin(nanos = 1) 애노테이션 추가로 유효성 검사 대체 - SpeedTouch, BombRelay, BlindTimer, CardGame 관련 validatePositive 메서드 제거 - 코드 간소화 및 유지보수성 향상 * refactor: BombRelayGameProgressHandler의 사전 검증 로직 간소화 - requiresDictionaryCheck와 단어 유효성 검사 조건을 단일 if문으로 통합 - 불필요한 중첩 조건 제거로 코드 가독성 향상 및 유지보수성 강화 * chore: application.yml 사전 API URL 업데이트 - dictionary.api-url 값을 `/api/search`에서 `/api/search.do`로 변경 - 사전 API 경로 변경에 따른 설정 조정 * feat: WordValidator의 서버 및 클라이언트 오류 처리 로직 개선 - 서버 오류(5xx) 응답 시 단어 허용 로직 유지 - 클라이언트 오류(4xx) 응답 시 단어 거절 로직 추가 - 단위 테스트에 클라이언트 오류 처리 테스트 케이스 추가 * feat: WordValidator에서 JSON에서 XML 기반 API로 변경 및 로직 개선 - 사전 API 응답 형식을 JSON에서 XML로 전환 대응 - ObjectMapper 종속성 제거 및 XML 파싱 로직 추가 - XML 전용 DocumentBuilderFactory 생성 및 XXE 취약점 방어 설정 적용 - 서버 에러코드("error_code") 처리 로직 추가 - application.yml의 사전 API URL 변경 (https://stdict.korean.go.kr → https://krdict.korean.go.kr) - 단위 테스트에서 JSON 데이터를 XML로 교체 및 새로운 테스트 케이스 추가 - 캐시 동작 유지 및 API 장애 시 단어 허용 로직 개선 * refactor: DelayedPlayerRemovalService 테스트 시간 외부 변수화 (#1133) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> * [feat] 닉네임 자동 생성 및 필터링 기능 구현 (#1130) * refactor: QrCodeConfig 파일 패키지 이동 * feat: 닉네임 검증 기능 추가 * feat: 랜덤 닉네임 생성 기능 추가 * feat: outbox relay delay 설정 추가 및 로컬 환경 오버라이드 적용 * feat: InvalidStateException으로 닉네임 생성 실패 처리 로직 개선 * refactor: 랜덤 닉네임 생성 메서드명에 호스트/게스트 의도 명시 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: BadWordFiltering을 ProfanityChecker 인터페이스로 분리 및 우회 패턴 전처리 추가 - ProfanityChecker 도메인 포트 인터페이스 도입 - VaneProfanityChecker(infra)에서 BadWordFiltering 라이브러리 래핑 - 특수문자/숫자 삽입 우회, 자음 축약 패턴 전처리 추가 - PlayerNameValidator가 ProfanityChecker 인터페이스에 의존하도록 변경 - PlayerNameValidatorTest를 mock 기반 단위 테스트로 전환 - VaneProfanityCheckerTest 신규 추가 (라이브러리 통합 우회 패턴 검증) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: PlayerNameGenerator 클래스 이름 변경 및 PlayerName.MAX_NAME_LENGTH 사용 * refactor: PlayerNameGenerator 결정론적 테스트 방식으로 변경 * fix: joinCode 빈 문자열/공백을 미전달과 동일하게 처리 ?joinCode= 또는 공백만 전달 시 호스트용 닉네임 생성 경로로 분기 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add WordPicker bean for random word selection * refactor: add null and blank name validation to PlayerNameValidator * fix: update OutboxE2ETest to use `create` for Hibernate DDL auto configuration --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: MinJun Choi <mj043000@naver.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
✅ 체크리스트
🔥 연관 이슈
🚀 작업 내용
개요
폭탄 끝말잇기(BOMB_RELAY) 미니게임 백엔드를 구현합니다.
api key도 서버에 추가 완료했습니다.
게임 규칙
단어 검증 흐름
턴제라서 한 번에 1명만 입력하므로 외부 API 지연이 문제되지 않는다.
주요 설계 판단
WebSocket 엔드포인트
/app/room/{joinCode}/bomb-relay/word{playerName, word}/topic/room/{joinCode}/bomb-relay/state/topic/room/{joinCode}/bomb-relay/progress/topic/room/{joinCode}/bomb-relay/word-result검증
Summary by CodeRabbit
새로운 기능
구성
테스트