fix: 과팅 락 맵 누수 및 불필요한 락 최적화 - #336
Conversation
- addParticipant: synchronized 제거, ConcurrentHashMap#compute로 원자화 - recordChoice: 전역 synchronized 제거, putIfAbsent로 원자화 (세션 간 병목 제거) - removeSocket/getBySocketId: O(n) 풀스캔 제거, socketId->memberId 역인덱스로 O(1) 조회
세션별 sessionLock이 이미 원자성을 보장하므로 인스턴스 전역 synchronized는 서로 무관한 세션들끼리 시작 처리를 불필요하게 직렬화시키는 중복 락이었음
sessions가 ConcurrentHashMap이라 단순 조회(get)/clear에는 synchronized가 필요 없었고, 상태 변경 메서드(start/terminate)는 이미 별도의 sessionLock으로 보호되어 이 synchronized와 애초에 상호 배제 관계도 아니었음
|
Warning Review limit reached
Next review available in: 12 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthrough블라인드데이트의 락 기반 동시성 처리가 단일 스레드 이벤트 큐와 원자적 저장소 연산 중심으로 변경되었습니다. 연결·퇴장·세션 종료가 큐에서 순차 처리되고, 참여자 소켓 조회에는 역방향 인덱스가 사용됩니다. Changes블라인드데이트 이벤트 직렬화
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant WebSocket as WebSocket 클라이언트
participant Handler as 연결 핸들러
participant Queue as BlindDateEventQueue
participant Storage as 참여자·세션 저장소
participant Scheduler as BlindDateSessionSchedulerImpl
WebSocket->>Handler: 연결 또는 연결 해제 실행
Handler->>Queue: 이벤트 제출
Queue->>Handler: 이벤트 순차 실행
Handler->>Storage: 참여자·세션 상태 변경
Handler->>Scheduler: 세션 시작 요청
Scheduler->>Queue: 세션 종료 작업 제출
Queue->>Storage: 세션 종료
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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 |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@src/main/java/com/dongsoop/dongsoop/blinddate/repository/BlindDateParticipantStorageImpl.java`:
- Around line 43-79: Update addParticipant so socketIdToMemberId.put(socketId,
memberId) executes inside the participants.compute lambda before returning in
both the new-participant and same-session existing-participant branches. Remove
the separate post-compute index update, preserving the existing behavior when
the lambda throws for a conflicting session.
In
`@src/main/java/com/dongsoop/dongsoop/blinddate/service/BlindDateServiceImpl.java`:
- Around line 91-94: Replace the unconditional sessionLock.clear() and
memberLock.clear() cleanup in the shutdown flow with lifecycle-safe cleanup:
remove only locks that are no longer in use, or otherwise coordinate lock
ownership so unlockBySessionId and unlockByMemberId cannot create replacement
locks while existing owners are active. Preserve safe unlocking for concurrent
requests and prevent IllegalMonitorStateException.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 5e11b31b-9b14-4bbc-93da-1879d8d4f99d
📒 Files selected for processing (7)
src/main/java/com/dongsoop/dongsoop/blinddate/handler/BlindDateConnectHandler.javasrc/main/java/com/dongsoop/dongsoop/blinddate/repository/BlindDateParticipantStorageImpl.javasrc/main/java/com/dongsoop/dongsoop/blinddate/repository/BlindDateSessionStorageImpl.javasrc/main/java/com/dongsoop/dongsoop/blinddate/service/BlindDateServiceImpl.javasrc/test/java/com/dongsoop/dongsoop/blinddate/BlindDateConcurrencyTest.javasrc/test/java/com/dongsoop/dongsoop/blinddate/BlindDateIntegrationTest.javasrc/test/java/com/dongsoop/dongsoop/blinddate/WebSocketTestConfig.java
| public ParticipantInfo addParticipant(String sessionId, Long memberId, String socketId) { | ||
| ParticipantInfo participant = participants.compute(memberId, (id, existing) -> { | ||
| // 처음 참여하는 경우 | ||
| if (existing == null) { | ||
| AtomicInteger atomicCounter = nameCounters.computeIfAbsent(sessionId, k -> new AtomicInteger(1)); | ||
| int counter = atomicCounter.getAndIncrement(); | ||
| String anonymousName = "익명" + counter; | ||
|
|
||
| ParticipantInfo created = ParticipantInfo.create(sessionId, memberId, socketId, anonymousName); | ||
|
|
||
| log.info("[BlindDate] Participant added: sessionId={}, memberId={}, socketId={}, name={}", | ||
| sessionId, memberId, socketId, anonymousName); | ||
|
|
||
| return created; | ||
| } | ||
|
|
||
| // 참여중인 경우 | ||
| // 같은 세션이면 소켓만 추가 | ||
| if (existing.getSessionId().equals(sessionId)) { | ||
| existing.addSocket(socketId); | ||
| log.info("[BlindDate] Socket added to existing participant: memberId={}, socketId={}, totalSockets={}", | ||
| memberId, socketId, existing.getSocketIds().size()); | ||
|
|
||
| return existing; | ||
| } | ||
|
|
||
| // 다른 세션에 이미 참여 중 | ||
| throw new IllegalStateException( | ||
| String.format("[BlindDate] Member %d already in session %s, cannot join session %s", | ||
| memberId, existing.getSessionId(), sessionId)); | ||
| }); | ||
|
|
||
| // socketId -> memberId 인덱스 갱신 (compute가 예외 없이 끝난 경우에만 도달) | ||
| socketIdToMemberId.put(socketId, memberId); | ||
|
|
||
| return participant; | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
addParticipant에서 participants.compute()와 socketIdToMemberId.put()이 원자적으로 묶이지 않아 레이스가 발생합니다.
compute()가 반환된 뒤 별도로 socketIdToMemberId.put(socketId, memberId)를 호출하는 구조라, compute가 끝나고 put이 실행되기 전의 짧은 시간 창에 다른 스레드가 removeSocket(socketId)를 호출하면 socketIdToMemberId.remove(socketId)가 null을 반환해 실제로는 존재하는 참여자를 "not found"로 처리하고 IllegalArgumentException을 던지게 됩니다. 신규 참여(existing == null) 및 기존 참여자에 소켓 추가(existing.getSessionId().equals(sessionId)) 두 분기 모두 동일한 문제를 가집니다.
주석에 "이 메서드 자체로 스레드 안전함"이라고 적혀 있지만, 인덱스 갭 윈도우로 인해 실제로는 그렇지 않습니다. socketIdToMemberId.put(...)을 compute 람다 내부(각 분기에서 반환하기 전)로 옮기면, 같은 락(비트 락)이 풀리기 전에 인덱스가 갱신되어 다른 스레드가 참여자를 관찰하는 시점에는 인덱스도 항상 일관된 상태가 됩니다.
🔒️ 제안 수정
public ParticipantInfo addParticipant(String sessionId, Long memberId, String socketId) {
- ParticipantInfo participant = participants.compute(memberId, (id, existing) -> {
+ return participants.compute(memberId, (id, existing) -> {
// 처음 참여하는 경우
if (existing == null) {
AtomicInteger atomicCounter = nameCounters.computeIfAbsent(sessionId, k -> new AtomicInteger(1));
int counter = atomicCounter.getAndIncrement();
String anonymousName = "익명" + counter;
ParticipantInfo created = ParticipantInfo.create(sessionId, memberId, socketId, anonymousName);
log.info("[BlindDate] Participant added: sessionId={}, memberId={}, socketId={}, name={}",
sessionId, memberId, socketId, anonymousName);
+ socketIdToMemberId.put(socketId, memberId);
return created;
}
// 참여중인 경우
// 같은 세션이면 소켓만 추가
if (existing.getSessionId().equals(sessionId)) {
existing.addSocket(socketId);
log.info("[BlindDate] Socket added to existing participant: memberId={}, socketId={}, totalSockets={}",
memberId, socketId, existing.getSocketIds().size());
+ socketIdToMemberId.put(socketId, memberId);
return existing;
}
// 다른 세션에 이미 참여 중
throw new IllegalStateException(
String.format("[BlindDate] Member %d already in session %s, cannot join session %s",
memberId, existing.getSessionId(), sessionId));
});
-
- // socketId -> memberId 인덱스 갱신 (compute가 예외 없이 끝난 경우에만 도달)
- socketIdToMemberId.put(socketId, memberId);
-
- return participant;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public ParticipantInfo addParticipant(String sessionId, Long memberId, String socketId) { | |
| ParticipantInfo participant = participants.compute(memberId, (id, existing) -> { | |
| // 처음 참여하는 경우 | |
| if (existing == null) { | |
| AtomicInteger atomicCounter = nameCounters.computeIfAbsent(sessionId, k -> new AtomicInteger(1)); | |
| int counter = atomicCounter.getAndIncrement(); | |
| String anonymousName = "익명" + counter; | |
| ParticipantInfo created = ParticipantInfo.create(sessionId, memberId, socketId, anonymousName); | |
| log.info("[BlindDate] Participant added: sessionId={}, memberId={}, socketId={}, name={}", | |
| sessionId, memberId, socketId, anonymousName); | |
| return created; | |
| } | |
| // 참여중인 경우 | |
| // 같은 세션이면 소켓만 추가 | |
| if (existing.getSessionId().equals(sessionId)) { | |
| existing.addSocket(socketId); | |
| log.info("[BlindDate] Socket added to existing participant: memberId={}, socketId={}, totalSockets={}", | |
| memberId, socketId, existing.getSocketIds().size()); | |
| return existing; | |
| } | |
| // 다른 세션에 이미 참여 중 | |
| throw new IllegalStateException( | |
| String.format("[BlindDate] Member %d already in session %s, cannot join session %s", | |
| memberId, existing.getSessionId(), sessionId)); | |
| }); | |
| // socketId -> memberId 인덱스 갱신 (compute가 예외 없이 끝난 경우에만 도달) | |
| socketIdToMemberId.put(socketId, memberId); | |
| return participant; | |
| } | |
| public ParticipantInfo addParticipant(String sessionId, Long memberId, String socketId) { | |
| return participants.compute(memberId, (id, existing) -> { | |
| // 처음 참여하는 경우 | |
| if (existing == null) { | |
| AtomicInteger atomicCounter = nameCounters.computeIfAbsent(sessionId, k -> new AtomicInteger(1)); | |
| int counter = atomicCounter.getAndIncrement(); | |
| String anonymousName = "익명" + counter; | |
| ParticipantInfo created = ParticipantInfo.create(sessionId, memberId, socketId, anonymousName); | |
| log.info("[BlindDate] Participant added: sessionId={}, memberId={}, socketId={}, name={}", | |
| sessionId, memberId, socketId, anonymousName); | |
| socketIdToMemberId.put(socketId, memberId); | |
| return created; | |
| } | |
| // 참여중인 경우 | |
| // 같은 세션이면 소켓만 추가 | |
| if (existing.getSessionId().equals(sessionId)) { | |
| existing.addSocket(socketId); | |
| log.info("[BlindDate] Socket added to existing participant: memberId={}, socketId={}, totalSockets={}", | |
| memberId, socketId, existing.getSocketIds().size()); | |
| socketIdToMemberId.put(socketId, memberId); | |
| return existing; | |
| } | |
| // 다른 세션에 이미 참여 중 | |
| throw new IllegalStateException( | |
| String.format("[BlindDate] Member %d already in session %s, cannot join session %s", | |
| memberId, existing.getSessionId(), sessionId)); | |
| }); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/main/java/com/dongsoop/dongsoop/blinddate/repository/BlindDateParticipantStorageImpl.java`
around lines 43 - 79, Update addParticipant so socketIdToMemberId.put(socketId,
memberId) executes inside the participants.compute lambda before returning in
both the new-participant and same-session existing-participant branches. Remove
the separate post-compute index update, preserving the existing behavior when
the lambda throws for a conflicting session.
addParticipant는 compute() 기반이라 예외 발생 시 참가자 맵에 변화가 없음. 따라서 join() 실패 시 회원을 지우는 코드는 이미 다른 세션에 정상 등록된 참가자 정보를 잘못 삭제할 수 있어 제거함.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@src/main/java/com/dongsoop/dongsoop/blinddate/handler/BlindDateConnectHandler.java`:
- Around line 124-126: 분리된 예외 처리로 addParticipant() 실패와 이후 findAllBySessionId(),
getMaxSessionMemberCount(), BlindDateJoinResult 생성 실패를 구분하세요. addParticipant()
실패 시 기존처럼 참가자 제거를 수행하지 말고, 후속 처리 실패 시에만 현재 socketId에 해당하는 참가자 정보를 보상 제거한 뒤 세션
속성을 정리하고 반환하세요. BlindDateConnectHandler의 현재 주석도 compute() 내부 예외에만 맞도록 수정하세요.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 104d6ec5-859f-4c0d-8119-b72139499740
📒 Files selected for processing (1)
src/main/java/com/dongsoop/dongsoop/blinddate/handler/BlindDateConnectHandler.java
execute()는 isProcessing(진행중)일 때만 tryRemoveMember를 호출했는데, tryRemoveMember 내부는 state가 WAITING이 아니면 즉시 반환해서 두 조건이 서로 모순되어 항상 아무 것도 실행되지 않았음. isWaiting으로 수정.
BlindDateSessionSchedulerImpl이 sessionLock 없이 getState()를 읽는 경로가 있어, 락 없이 상태를 변경/조회하는 스레드 간 가시성이 보장되지 않았음.
BlindDateMatchingLock/BlindDateMemberLock/BlindDateSessionLock을 제거하고 BlindDateEventQueue(단일 스레드 실행기)로 대체. 참가자/세션 상태를 건드리는 모든 처리(입장, 재접속, 세션 시작 판단, 퇴장, 세션 종료)를 이 큐에서 순서대로 처리하여 락 관리로 인한 버그 클래스(범위 오류, 순서 오류, 누수 등)를 원천적으로 제거함. 단일 인스턴스·현재 트래픽 규모에서는 순차 처리로도 충분하다고 판단. 테스트는 execute()가 비동기로 바뀐 것에 맞춰 BlindDateEventQueue#awaitIdle()로 큐 처리 완료를 기다리도록 수정. 락 자체를 검증하던 테스트는 제거.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/main/java/com/dongsoop/dongsoop/blinddate/repository/BlindDateParticipantStorageImpl.java (1)
21-23: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win재연결 경로가 새 소켓 역인덱스를 갱신하지 않습니다.
BlindDateConnectHandler.tryHandleReconnection(Line [150]-[166])은ParticipantInfo.addSocket(socketId)을 직접 호출합니다. 따라서 재연결 소켓은socketIdToMemberId에 등록되지 않고, 이후getBySocketId는null을 반환하며removeSocket도 참여자를 찾지 못해 예외를 발생시킵니다. 역인덱스까지 갱신하는 저장소 메서드를 추가해 재연결 경로가 이를 사용하도록 변경해야 합니다.Also applies to: 84-91, 126-128
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/java/com/dongsoop/dongsoop/blinddate/repository/BlindDateParticipantStorageImpl.java` around lines 21 - 23, 재연결 시 새 소켓이 역방향 인덱스에 등록되지 않습니다. BlindDateParticipantStorageImpl에 ParticipantInfo의 소켓 추가와 socketIdToMemberId 갱신을 함께 수행하는 저장소 메서드를 추가하고, BlindDateConnectHandler.tryHandleReconnection 및 관련 직접 addSocket 호출을 해당 메서드 사용으로 변경하세요. 이후 getBySocketId와 removeSocket이 재연결 소켓에서도 정상적으로 참여자를 찾도록 기존 연결 경로와 동일한 인덱스 갱신을 보장하세요.
♻️ Duplicate comments (1)
src/main/java/com/dongsoop/dongsoop/blinddate/handler/BlindDateConnectHandler.java (1)
94-132: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
addParticipant()성공 이후 실패 시 참가자가 고아 상태로 남습니다 (과거 리뷰와 동일 이슈).
catch (Exception e)(Line 113)는addParticipant()뿐 아니라 이후findAllBySessionId(),getMaxSessionMemberCount(),BlindDateJoinResult생성 중 예외까지 모두 처리합니다. 이 경우 참가자는 이미 정상 저장되었지만 세션 속성만 제거한 채null을 반환하여, 저장된 참가자 정보가 남습니다.또한 Line 127의 로그는 "rolling back participant"라고 되어 있지만 실제로는 어떤 롤백도 수행하지 않습니다 —
sendJoinEvent()실패 시에도 이미addParticipant()로 반영된 참가자 정보가 그대로 남아 고아 상태가 됩니다. 이후 재연결 시tryHandleReconnection()이 이 고아 참가자를 정상 참가자로 오인해 소켓만 추가하는 결과를 낳을 수 있습니다.
addParticipant()자체의 실패와 그 이후 단계의 실패를 분리하고, 후자의 경우에만 현재socketId에 대해 보상 제거를 수행하는 것을 권장합니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/java/com/dongsoop/dongsoop/blinddate/handler/BlindDateConnectHandler.java` around lines 94 - 132, Update join so addParticipant() failure is handled separately from failures in findAllBySessionId(), getMaxSessionMemberCount(), or BlindDateJoinResult construction. Track the participant successfully added by addParticipant(), and on any later failure—including sendJoinEvent() failure—remove only the current socketId’s participant through the existing participant-storage removal API before clearing sessionAttributes and returning null; keep the addParticipant() failure path from removing unrelated participants.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@src/main/java/com/dongsoop/dongsoop/blinddate/executor/BlindDateEventQueue.java`:
- Around line 25-33: Update BlindDateEventQueue.submit so exceptions from
executor.execute itself, including RejectedExecutionException after shutdown,
are caught and handled without propagating to the caller thread. Preserve the
existing event.run error logging and add equivalent defensive handling around
task submission, using the queue’s existing logging approach.
In
`@src/main/java/com/dongsoop/dongsoop/blinddate/handler/BlindDateConnectHandler.java`:
- Around line 150-166: Update handle() to catch SessionTerminatedException
separately from the generic exception path and send the client a failure event
through the same mechanism used by sendJoinEvent(). Ensure the response is
emitted even when tryHandleReconnection() fails, instead of allowing
BlindDateEventQueue.submit() to only log and swallow the exception.
---
Outside diff comments:
In
`@src/main/java/com/dongsoop/dongsoop/blinddate/repository/BlindDateParticipantStorageImpl.java`:
- Around line 21-23: 재연결 시 새 소켓이 역방향 인덱스에 등록되지 않습니다.
BlindDateParticipantStorageImpl에 ParticipantInfo의 소켓 추가와 socketIdToMemberId 갱신을
함께 수행하는 저장소 메서드를 추가하고, BlindDateConnectHandler.tryHandleReconnection 및 관련 직접
addSocket 호출을 해당 메서드 사용으로 변경하세요. 이후 getBySocketId와 removeSocket이 재연결 소켓에서도 정상적으로
참여자를 찾도록 기존 연결 경로와 동일한 인덱스 갱신을 보장하세요.
---
Duplicate comments:
In
`@src/main/java/com/dongsoop/dongsoop/blinddate/handler/BlindDateConnectHandler.java`:
- Around line 94-132: Update join so addParticipant() failure is handled
separately from failures in findAllBySessionId(), getMaxSessionMemberCount(), or
BlindDateJoinResult construction. Track the participant successfully added by
addParticipant(), and on any later failure—including sendJoinEvent()
failure—remove only the current socketId’s participant through the existing
participant-storage removal API before clearing sessionAttributes and returning
null; keep the addParticipant() failure path from removing unrelated
participants.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 141e7b79-15e2-4200-b470-2ab9b24a6a95
📒 Files selected for processing (17)
src/main/java/com/dongsoop/dongsoop/blinddate/entity/SessionInfo.javasrc/main/java/com/dongsoop/dongsoop/blinddate/executor/BlindDateEventQueue.javasrc/main/java/com/dongsoop/dongsoop/blinddate/handler/BlindDateConnectHandler.javasrc/main/java/com/dongsoop/dongsoop/blinddate/handler/BlindDateDisconnectHandler.javasrc/main/java/com/dongsoop/dongsoop/blinddate/lock/BlindDateMatchingLock.javasrc/main/java/com/dongsoop/dongsoop/blinddate/lock/BlindDateMemberLock.javasrc/main/java/com/dongsoop/dongsoop/blinddate/lock/BlindDateSessionLock.javasrc/main/java/com/dongsoop/dongsoop/blinddate/repository/BlindDateParticipantStorageImpl.javasrc/main/java/com/dongsoop/dongsoop/blinddate/repository/BlindDateSessionStorageImpl.javasrc/main/java/com/dongsoop/dongsoop/blinddate/scheduler/BlindDateSessionSchedulerImpl.javasrc/main/java/com/dongsoop/dongsoop/blinddate/service/BlindDateServiceImpl.javasrc/test/java/com/dongsoop/dongsoop/blinddate/BlindDateConcurrencyTest.javasrc/test/java/com/dongsoop/dongsoop/blinddate/BlindDateIntegrationTest.javasrc/test/java/com/dongsoop/dongsoop/blinddate/BlindDateLockVerificationTest.javasrc/test/java/com/dongsoop/dongsoop/blinddate/BlindDateRepositoryConcurrencyTest.javasrc/test/java/com/dongsoop/dongsoop/blinddate/WebSocketTestConfig.javasrc/test/java/com/dongsoop/dongsoop/blinddate/lock/BlindDateLockTest.java
💤 Files with no reviewable changes (5)
- src/main/java/com/dongsoop/dongsoop/blinddate/lock/BlindDateSessionLock.java
- src/main/java/com/dongsoop/dongsoop/blinddate/lock/BlindDateMatchingLock.java
- src/test/java/com/dongsoop/dongsoop/blinddate/lock/BlindDateLockTest.java
- src/main/java/com/dongsoop/dongsoop/blinddate/lock/BlindDateMemberLock.java
- src/test/java/com/dongsoop/dongsoop/blinddate/BlindDateLockVerificationTest.java
executor가 종료된 상태에서 submit()이 호출되면 RejectedExecutionException이 호출자(웹소켓 처리) 스레드로 그대로 전파될 수 있었음. shutdown()은 과팅 라운드 종료 시점이 아니라 애플리케이션 종료 시점에만 호출해야 한다는 점을 주석으로 명시.
큐 기반 처리로 바뀌면서 SessionTerminatedException이 큐의 범용 catch에 잡혀 로그만 남고 클라이언트는 응답 없이 대기하는 문제가 있었음. handle()에서 별도로 잡아 죽은 참가자 기록을 정리하고 TERMINATED 상태 이벤트를 전송하도록 수정.
tryHandleReconnection이 existingParticipant.addSocket()을 직접 호출해 역인덱스를 건너뛰던 것을, addParticipant()를 통해 소켓 추가와 인덱스 갱신이 함께 이뤄지도록 수정. 재접속 소켓이 나중에 끊겨도 정상적으로 정리됨.
finalizeSession()의 재접속 방지 의도(참가자 기록을 의도적으로 유지)를 깨뜨리고 있었음. 클라이언트 알림만 보내고 기록은 그대로 둠.
addParticipant() 성공 이후(정원 조회, 알림 전송 등) 실패하면 참가자가 등록된 채로 남아 고아가 되던 문제. 등록 이후 단계를 별도 try로 분리하고, 실패 시 클라이언트에 재시도를 요청하는 이벤트를 보낸 뒤 방금 추가한 소켓을 DisconnectHandler에 위임해 되돌리도록 수정.
Summary
BlindDateMemberLock/BlindDateSessionLock의 맵을 정리하지 않아 라운드가 반복될수록 무한히 누적되던 누수 수정BlindDateParticipantStorageImpl.addParticipant/recordChoice의 불필요한 인스턴스 전역synchronized제거,compute/putIfAbsent기반 원자 연산으로 대체removeSocket/getBySocketId의 O(n) 전체 스캔을socketId -> memberId역인덱스로 O(1) 조회로 변경BlindDateConnectHandler.tryStart()와BlindDateSessionStorageImpl의 조회 메서드들에 걸려 있던, 실제로는 아무 원자성도 지켜주지 못하던 불필요한synchronized제거Test plan
BlindDateStorageConcurrencyTest(addParticipant 동시성, 소켓 다중 접속) 통과Summary by CodeRabbit