Skip to content

fix: 과팅 락 맵 누수 및 불필요한 락 최적화 - #336

Merged
rdyjun merged 13 commits into
mainfrom
fix/blinddate-lock-map-leak
Jul 19, 2026
Merged

fix: 과팅 락 맵 누수 및 불필요한 락 최적화#336
rdyjun merged 13 commits into
mainfrom
fix/blinddate-lock-map-leak

Conversation

@rdyjun

@rdyjun rdyjun commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

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 동시성, 소켓 다중 접속) 통과
  • blinddate 패키지 전체 테스트 통과

Summary by CodeRabbit

  • 개선 사항
    • 상태 변경을 단일 순차 처리 흐름으로 전환해 동시성 상황에서도 세션 진행이 더 안정적으로 동작합니다.
    • 소켓 기준 참여자 조회/제거를 최적화해 응답성과 처리 효율을 높였습니다.
    • 세션 종료 및 자동 정리(브로드캐스트 포함) 타이밍을 큐 기반으로 일관되게 적용합니다.
    • 세션 상태 가시성을 보강해 상태 판별의 정확도를 개선했습니다.
  • 테스트
    • 이벤트 처리 완료 대기를 추가해 동시성 시나리오 검증 타이밍을 안정화했습니다.

autocommit added 4 commits July 19, 2026 14:04
- 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와 애초에 상호 배제 관계도 아니었음
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@rdyjun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 12 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f74589e3-a7ea-4476-943f-c7b3188daba4

📥 Commits

Reviewing files that changed from the base of the PR and between 24c6706 and a023851.

📒 Files selected for processing (4)
  • src/main/java/com/dongsoop/dongsoop/blinddate/handler/BlindDateConnectHandler.java
  • src/test/java/com/dongsoop/dongsoop/blinddate/BlindDateConcurrencyTest.java
  • src/test/java/com/dongsoop/dongsoop/blinddate/BlindDateIntegrationTest.java
  • src/test/java/com/dongsoop/dongsoop/blinddate/WebSocketTestConfig.java
📝 Walkthrough

Walkthrough

블라인드데이트의 락 기반 동시성 처리가 단일 스레드 이벤트 큐와 원자적 저장소 연산 중심으로 변경되었습니다. 연결·퇴장·세션 종료가 큐에서 순차 처리되고, 참여자 소켓 조회에는 역방향 인덱스가 사용됩니다.

Changes

블라인드데이트 이벤트 직렬화

Layer / File(s) Summary
이벤트 큐와 연결 생명주기 처리
src/main/java/com/dongsoop/dongsoop/blinddate/executor/BlindDateEventQueue.java, src/main/java/com/dongsoop/dongsoop/blinddate/handler/*
단일 스레드 이벤트 큐를 추가하고 연결과 연결 해제 작업을 큐에 제출하며, 세션 시작·재연결·퇴장 처리를 큐 순서에 따라 수행합니다.
참여자 저장소 원자성 및 인덱스
src/main/java/com/dongsoop/dongsoop/blinddate/repository/BlindDateParticipantStorageImpl.java
소켓-회원 역방향 인덱스를 추가하고 참여자 갱신, 소켓 조회, 선택 기록을 원자적 연산으로 변경했으며 제거·초기화 시 인덱스를 정리합니다.
세션 저장소와 종료 위임
src/main/java/com/dongsoop/dongsoop/blinddate/repository/BlindDateSessionStorageImpl.java, src/main/java/com/dongsoop/dongsoop/blinddate/scheduler/BlindDateSessionSchedulerImpl.java, src/main/java/com/dongsoop/dongsoop/blinddate/service/BlindDateServiceImpl.java
세션 락과 메서드 동기화를 제거하고 세션 종료 및 자동 정리 작업을 이벤트 큐에 위임합니다.
큐 배선 및 동시성 검증
src/test/java/com/dongsoop/dongsoop/blinddate/*
테스트 구성에 이벤트 큐를 주입하고, 연결·퇴장·매칭·동시성 검증 전에 awaitIdle()을 호출하도록 변경했습니다.

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: 세션 종료
Loading

Possibly related PRs

  • dongsooop/backend#301: 동일한 블라인드데이트 핸들러와 저장소 처리 흐름을 다룬 변경입니다.

Poem

토끼가 큐에 이벤트를 담고
하나씩 깡충 처리하네
소켓 길도 인덱스로 찾고
락은 조용히 쉬는 밤
세션 당근도 순서대로 🐇

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning 템플릿의 관련 이슈, 배경, 리뷰 소요 시간 섹션이 없어 설명이 충분히 완성되지 않았습니다. Closes #이슈번호, 배경, 주요 내용, 리뷰 소요 시간 섹션을 템플릿대로 추가해 주세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 25.42% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 과팅 락 누수 수정과 불필요한 락 제거라는 이번 변경의 핵심을 잘 요약합니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/blinddate-lock-map-leak

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between f9ebc47 and 70da2c7.

📒 Files selected for processing (7)
  • src/main/java/com/dongsoop/dongsoop/blinddate/handler/BlindDateConnectHandler.java
  • src/main/java/com/dongsoop/dongsoop/blinddate/repository/BlindDateParticipantStorageImpl.java
  • src/main/java/com/dongsoop/dongsoop/blinddate/repository/BlindDateSessionStorageImpl.java
  • src/main/java/com/dongsoop/dongsoop/blinddate/service/BlindDateServiceImpl.java
  • src/test/java/com/dongsoop/dongsoop/blinddate/BlindDateConcurrencyTest.java
  • src/test/java/com/dongsoop/dongsoop/blinddate/BlindDateIntegrationTest.java
  • src/test/java/com/dongsoop/dongsoop/blinddate/WebSocketTestConfig.java

Comment on lines +43 to 79
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;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.

Suggested change
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.

Comment thread src/main/java/com/dongsoop/dongsoop/blinddate/service/BlindDateServiceImpl.java Outdated
addParticipant는 compute() 기반이라 예외 발생 시 참가자 맵에 변화가 없음.
따라서 join() 실패 시 회원을 지우는 코드는 이미 다른 세션에 정상 등록된
참가자 정보를 잘못 삭제할 수 있어 제거함.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 70da2c7 and 340806e.

📒 Files selected for processing (1)
  • src/main/java/com/dongsoop/dongsoop/blinddate/handler/BlindDateConnectHandler.java

autocommit added 3 commits July 19, 2026 16:32
execute()는 isProcessing(진행중)일 때만 tryRemoveMember를 호출했는데,
tryRemoveMember 내부는 state가 WAITING이 아니면 즉시 반환해서 두 조건이
서로 모순되어 항상 아무 것도 실행되지 않았음. isWaiting으로 수정.
BlindDateSessionSchedulerImpl이 sessionLock 없이 getState()를 읽는 경로가 있어,
락 없이 상태를 변경/조회하는 스레드 간 가시성이 보장되지 않았음.
BlindDateMatchingLock/BlindDateMemberLock/BlindDateSessionLock을 제거하고
BlindDateEventQueue(단일 스레드 실행기)로 대체. 참가자/세션 상태를 건드리는
모든 처리(입장, 재접속, 세션 시작 판단, 퇴장, 세션 종료)를 이 큐에서 순서대로
처리하여 락 관리로 인한 버그 클래스(범위 오류, 순서 오류, 누수 등)를 원천적으로
제거함. 단일 인스턴스·현재 트래픽 규모에서는 순차 처리로도 충분하다고 판단.

테스트는 execute()가 비동기로 바뀐 것에 맞춰 BlindDateEventQueue#awaitIdle()로
큐 처리 완료를 기다리도록 수정. 락 자체를 검증하던 테스트는 제거.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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에 등록되지 않고, 이후 getBySocketIdnull을 반환하며 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

📥 Commits

Reviewing files that changed from the base of the PR and between 340806e and ac595c3.

📒 Files selected for processing (17)
  • src/main/java/com/dongsoop/dongsoop/blinddate/entity/SessionInfo.java
  • src/main/java/com/dongsoop/dongsoop/blinddate/executor/BlindDateEventQueue.java
  • src/main/java/com/dongsoop/dongsoop/blinddate/handler/BlindDateConnectHandler.java
  • src/main/java/com/dongsoop/dongsoop/blinddate/handler/BlindDateDisconnectHandler.java
  • src/main/java/com/dongsoop/dongsoop/blinddate/lock/BlindDateMatchingLock.java
  • src/main/java/com/dongsoop/dongsoop/blinddate/lock/BlindDateMemberLock.java
  • src/main/java/com/dongsoop/dongsoop/blinddate/lock/BlindDateSessionLock.java
  • src/main/java/com/dongsoop/dongsoop/blinddate/repository/BlindDateParticipantStorageImpl.java
  • src/main/java/com/dongsoop/dongsoop/blinddate/repository/BlindDateSessionStorageImpl.java
  • src/main/java/com/dongsoop/dongsoop/blinddate/scheduler/BlindDateSessionSchedulerImpl.java
  • src/main/java/com/dongsoop/dongsoop/blinddate/service/BlindDateServiceImpl.java
  • src/test/java/com/dongsoop/dongsoop/blinddate/BlindDateConcurrencyTest.java
  • src/test/java/com/dongsoop/dongsoop/blinddate/BlindDateIntegrationTest.java
  • src/test/java/com/dongsoop/dongsoop/blinddate/BlindDateLockVerificationTest.java
  • src/test/java/com/dongsoop/dongsoop/blinddate/BlindDateRepositoryConcurrencyTest.java
  • src/test/java/com/dongsoop/dongsoop/blinddate/WebSocketTestConfig.java
  • src/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

autocommit added 5 commits July 19, 2026 20:23
executor가 종료된 상태에서 submit()이 호출되면 RejectedExecutionException이
호출자(웹소켓 처리) 스레드로 그대로 전파될 수 있었음. shutdown()은 과팅 라운드
종료 시점이 아니라 애플리케이션 종료 시점에만 호출해야 한다는 점을 주석으로 명시.
큐 기반 처리로 바뀌면서 SessionTerminatedException이 큐의 범용 catch에 잡혀
로그만 남고 클라이언트는 응답 없이 대기하는 문제가 있었음. handle()에서 별도로
잡아 죽은 참가자 기록을 정리하고 TERMINATED 상태 이벤트를 전송하도록 수정.
tryHandleReconnection이 existingParticipant.addSocket()을 직접 호출해
역인덱스를 건너뛰던 것을, addParticipant()를 통해 소켓 추가와 인덱스 갱신이
함께 이뤄지도록 수정. 재접속 소켓이 나중에 끊겨도 정상적으로 정리됨.
finalizeSession()의 재접속 방지 의도(참가자 기록을 의도적으로 유지)를
깨뜨리고 있었음. 클라이언트 알림만 보내고 기록은 그대로 둠.
addParticipant() 성공 이후(정원 조회, 알림 전송 등) 실패하면 참가자가 등록된
채로 남아 고아가 되던 문제. 등록 이후 단계를 별도 try로 분리하고, 실패 시
클라이언트에 재시도를 요청하는 이벤트를 보낸 뒤 방금 추가한 소켓을
DisconnectHandler에 위임해 되돌리도록 수정.
@rdyjun
rdyjun merged commit d60a03c into main Jul 19, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant