Skip to content

refactor(room): 답안 제출 API 성능 개편 (Kafka 비동기 처리 & Redisson 분산 락 적용) - #72

Merged
SungHuii merged 3 commits into
developfrom
refactor/room-submit-kafka-redis
Aug 11, 2026
Merged

refactor(room): 답안 제출 API 성능 개편 (Kafka 비동기 처리 & Redisson 분산 락 적용)#72
SungHuii merged 3 commits into
developfrom
refactor/room-submit-kafka-redis

Conversation

@SungHuii

@SungHuii SungHuii commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

작업 내용

  • 평가 시험 답안 제출 API(POST /api/rooms/{roomId}/submit)의 DB Lock 경합을 완화하고 응답 속도를 획기적으로 개선(< 20ms)하기 위해 Kafka 기반 비동기 Write-Behind 패턴과 Redisson 분산 락을 도입

변경 사항

  • 기존 문제점:
    • RoomServiceImpl에서 roomRepository.findByIdForUpdate(roomId)(비관적 락)를 수행하여 동일한 시험방에 수백 명의 응시자가 동시 제출 시 DB Row Lock 대기 경합이 발생함.
    • 답안 레코드(5건)를 동기식(Synchronous)으로 DB에 저장하여 HTTP 응답 지연시간(Latency)이 DB I/O 처리 속도에 직접 종속됨.
  • 해결 방안:
    • HTTP 요청 처리 스레드는 유효성 검증 후 Kafka 이벤트만 전송하여 < 20ms 내 즉시 응답 반환.
    • 실제 DB 저장은 Kafka Consumer가 백그라운드에서 비동기로 안전하게 일괄 저장.
    • 비관적 락을 제거하고 프로젝트 공통 Redisson 분산 락(lock:room:submit:{roomId}:{userId}) 패턴으로 동시성 전환.

1. momogo-core

  • KafkaTopics.java:
    • ROOM_SUBMIT_EVENTS ("room-submit-events") 토픽 상수 추가.
  • RoomSubmitEventMessage.java:
    • 카프카 재전달 시 중복 저장 방지용 UUID eventId 및 방어적 생성을 포함한 Record 생성.
  • RoomSubmitKafkaProducer.java:
    • 답안 제출 비동기 이벤트 발행 프로듀서 구현 (NotificationKafkaProducer 스타일 준수).
  • RoomServiceImpl.java:
    • findByIdForUpdate (비관적 락) 제거 ➔ 락이 없는 findRoomOrThrow 사용.
    • RedisJwtRegistry와 동일한 Redisson Watchdog 기반 분산 락 적용.
    • 동기 DB 저장 로직을 roomSubmitKafkaProducer.send(...) 이벤트 전송으로 변경.

2. momogo-api

  • RoomSubmitKafkaConsumer.java:
    • Kafka 수신 후 Redis 멱등성 디둡(room:submit:processed:{eventId}) 검증.
    • entityManager.getReference()를 활용해 추가 SELECT 없는 유저 프록시 참조로 DB 저장 최적화.
    • TransactionSynchronizationManager를 통한 DB 커밋 완료 후 Redis 멱등성 키 갱신.

체크리스트

  • 테스트 코드 작성 완료
  • 리뷰어 지정 완료

참고 사항

관련 이슈

Summary by CodeRabbit

  • 새 기능
    • 답안 제출 처리가 비동기 방식으로 개선되었습니다.
    • 제출된 답안이 일괄 저장되고 응시 완료 상태가 자동으로 갱신됩니다.
  • 버그 수정
    • 중복 제출 이벤트를 방지해 동일한 답안이 반복 저장되지 않습니다.
    • 동시 제출 상황에서도 답안 처리 충돌을 줄였습니다.
  • 개선 사항
    • 제출 요청 후에도 안정적으로 처리가 완료되도록 시스템 신뢰성이 향상되었습니다.

SungHuii and others added 2 commits August 11, 2026 16:23
…g and Redisson lock

- Replace pessimistic DB lock (findByIdForUpdate) with Redisson distributed lock
    - Add ROOM_SUBMIT_EVENTS topic to KafkaTopics
    - Create RoomSubmitEventMessage DTO record with eventId idempotency key
    - Implement RoomSubmitKafkaProducer in momogo-core for async event publishing
    - Implement RoomSubmitKafkaConsumer in momogo-api with Redis deduplication
    - Refactor RoomServiceImpl.submitRoomAnswer to delegate DB writes to Kafka
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 42 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0678e500-ca7b-461f-b650-a2a8b6a1f32b

📥 Commits

Reviewing files that changed from the base of the PR and between 9da1aec and 61438eb.

📒 Files selected for processing (5)
  • momogo-api/src/main/java/com/momogo/api/room/consumer/RoomSubmitKafkaConsumer.java
  • momogo-core/src/main/java/com/momogo/core/domain/room/event/RoomSubmitEventMessage.java
  • momogo-core/src/main/java/com/momogo/core/domain/room/exception/RoomErrorCode.java
  • momogo-core/src/main/java/com/momogo/core/domain/room/kafka/producer/RoomSubmitKafkaProducer.java
  • momogo-core/src/main/java/com/momogo/core/domain/room/service/RoomServiceImpl.java
📝 Walkthrough

Walkthrough

답안 제출 처리를 동기 데이터베이스 작업에서 Kafka 기반 비동기 작업으로 변경했습니다. 제출 이벤트, 분산 락, Redis 멱등성 검사, 커밋 후 처리 완료 키 기록을 추가했습니다.

Changes

답안 제출 비동기 처리

Layer / File(s) Summary
제출 이벤트 계약과 Kafka 발행
momogo-core/src/main/java/com/momogo/core/domain/room/event/RoomSubmitEventMessage.java, momogo-core/src/main/java/com/momogo/core/common/config/KafkaTopics.java, momogo-core/src/main/java/com/momogo/core/domain/room/kafka/producer/RoomSubmitKafkaProducer.java
RoomSubmitEventMessage가 이벤트 ID, 사용자 ID, 방 ID, 답안 목록, 제출 시각을 보유합니다. RoomSubmitKafkaProducer는 방 ID를 Kafka 메시지 키로 사용해 room-submit-events 토픽에 이벤트를 발행합니다.
제출 검증과 분산 락
momogo-core/src/main/java/com/momogo/core/domain/room/service/RoomServiceImpl.java
submitRoomAnswer가 중복 문제 ID와 응시 조건을 검증합니다. 방·사용자별 Redisson 락을 최대 3초 동안 획득한 후 제출 이벤트를 발행합니다.
이벤트 소비와 데이터베이스 저장
momogo-api/src/main/java/com/momogo/api/room/consumer/RoomSubmitKafkaConsumer.java
소비자가 Redis 멱등성 키를 확인합니다. 사용자와 문제 데이터를 조회하고 UserRoomAnswer를 저장한 뒤 RoomUser의 응시 완료 상태를 갱신합니다. 데이터베이스 커밋 후 24시간 TTL의 처리 완료 키를 기록합니다.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant RoomServiceImpl
  participant RoomSubmitKafkaProducer
  participant Kafka
  participant RoomSubmitKafkaConsumer
  participant Redis
  participant Database

  Client->>RoomServiceImpl: 답안 제출 요청
  RoomServiceImpl->>RoomSubmitKafkaProducer: 제출 이벤트 전달
  RoomSubmitKafkaProducer->>Kafka: room ID 키로 이벤트 발행
  Kafka->>RoomSubmitKafkaConsumer: 제출 이벤트 전달
  RoomSubmitKafkaConsumer->>Redis: 멱등성 키 확인
  RoomSubmitKafkaConsumer->>Database: 답안 저장과 응시 완료 상태 갱신
  Database-->>RoomSubmitKafkaConsumer: 트랜잭션 커밋 완료
  RoomSubmitKafkaConsumer->>Redis: 처리 완료 키 기록
Loading

Possibly related PRs

Suggested reviewers: junkov0

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 답안 제출 API의 Kafka 비동기 처리와 Redisson 분산 락 적용이라는 주요 변경 사항을 정확하고 간결하게 설명합니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/room-submit-kafka-redis

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: 6

🧹 Nitpick comments (3)
momogo-core/src/main/java/com/momogo/core/domain/room/service/RoomServiceImpl.java (1)

298-304: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

제안: 예외 코드 소속과 인터럽트 처리 메시지를 정리해 주세요.

두 가지 사항입니다.

  1. AuthErrorCode.LOCK_ACQUISITION_FAILED(Line 299)는 인증 도메인 코드입니다. 답안 제출 락 실패는 방 도메인 관심사입니다. RoomErrorCode로 옮기거나 공용 GlobalErrorCode에 두면 도메인 경계가 명확해집니다.
  2. InterruptedException 처리(Line 301-303)에서 원인 예외를 버립니다. 로그에 원인을 남기면 장애 분석이 쉬워집니다.

개념 참고: 에러 코드는 "어느 도메인이 이 실패를 소유하는가"를 기준으로 배치합니다. 호출 위치가 아니라 실패의 의미를 기준으로 삼습니다.

리뷰 근거: 경로 지침의 "서비스/도메인 설계, 공통 예외 처리를 확인" 항목에 따릅니다.

🤖 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
`@momogo-core/src/main/java/com/momogo/core/domain/room/service/RoomServiceImpl.java`
around lines 298 - 304, In the answer-submission lock timeout handling within
RoomServiceImpl, replace the authentication-owned
AuthErrorCode.LOCK_ACQUISITION_FAILED with an appropriate RoomErrorCode or
shared GlobalErrorCode. In the InterruptedException catch, retain thread
interruption and include the caught exception as the cause or in logging before
throwing the internal-server error.

Source: Path instructions

momogo-api/src/main/java/com/momogo/api/room/consumer/RoomSubmitKafkaConsumer.java (1)

53-58: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

제안: 멱등성 검사를 원자적 연산으로 바꾸는 편이 안전합니다.

현재 hasKey 조회와 set 기록이 분리되어 있습니다. 같은 eventId가 서로 다른 컨슈머 스레드에 동시에 전달되면 두 스레드 모두 검사를 통과할 수 있습니다.

roomId를 파티션 키로 사용하므로 동일 파티션 내 순차 처리가 보장됩니다. 따라서 현재 위험은 낮습니다. 다만 파티션 재할당(rebalance) 직후 중복 전달 구간에서는 발생할 수 있습니다.

개선 방법: 처리 시작 시점에 setIfAbsent로 "처리 중" 표시를 선점하고, afterCommit에서 TTL을 확정 값으로 갱신합니다. 롤백 시에는 afterCompletion에서 키를 삭제해 재시도를 허용합니다.

개념 참고: "검사 후 실행" 패턴은 분산 환경에서 원자성이 없습니다. Redis의 SETNX 계열 연산이 검사와 기록을 한 번에 처리합니다.

Line 82-88의 afterCommit 사용은 좋은 선택입니다. DB 커밋 전에 멱등성 키를 기록하면 롤백 시 재처리가 막히기 때문입니다.

🤖 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
`@momogo-api/src/main/java/com/momogo/api/room/consumer/RoomSubmitKafkaConsumer.java`
around lines 53 - 58, RoomSubmitKafkaConsumer의 멱등성 처리에서 hasKey 기반 검사와 후속 기록을
setIfAbsent 원자적 선점으로 변경하세요. 처리 시작 시 dedupKey를 “처리 중” 상태로 등록하고, 선점에 실패하면 기존 중복 스킵
흐름을 유지하세요. 트랜잭션 커밋 후 afterCommit에서 키의 TTL을 확정 값으로 갱신하고, 롤백 또는 완료되지 않은 경우
afterCompletion에서 키를 삭제해 재시도가 가능하도록 하세요.
momogo-core/src/main/java/com/momogo/core/domain/room/kafka/producer/RoomSubmitKafkaProducer.java (1)

19-19: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

제안: KafkaTemplate 제네릭 타입을 이벤트 타입으로 좁혀 주세요.

현재 KafkaTemplate<String, Object>는 어떤 객체든 발행할 수 있습니다. KafkaTemplate<String, RoomSubmitEventMessage>로 좁히면 컴파일 시점에 잘못된 페이로드를 차단할 수 있습니다.

다만 애플리케이션 컨텍스트에 KafkaTemplate<String, Object> 빈 하나만 등록되어 있다면 타입 주입이 실패할 수 있습니다. 기존 AiGradingProducer가 같은 빈을 공유하는지 먼저 확인해 주세요. 공유 구조라면 현재 형태를 유지하는 편이 낫습니다.

🤖 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
`@momogo-core/src/main/java/com/momogo/core/domain/room/kafka/producer/RoomSubmitKafkaProducer.java`
at line 19, Review RoomSubmitKafkaProducer and the existing AiGradingProducer
bean wiring; if a dedicated KafkaTemplate<String, RoomSubmitEventMessage> can be
injected safely, narrow the field type and preserve event-specific publishing,
otherwise retain KafkaTemplate<String, Object> when both producers share the
same application-context bean.
🤖 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
`@momogo-api/src/main/java/com/momogo/api/room/consumer/RoomSubmitKafkaConsumer.java`:
- Around line 63-76: Validate every answer’s roomProblemId against the requested
room in RoomServiceImpl.submitRoomAnswer before publishing or processing the
message, rejecting IDs that are missing or belong to another room. Also retain
defensive validation in RoomSubmitKafkaConsumer around problemsById before
constructing UserRoomAnswer entries, and handle invalid messages as
non-retryable by logging and terminating or routing them to the DLT rather than
saving null or cross-room answers.
- Around line 78-80: Update the participant lookup in RoomSubmitKafkaConsumer so
a missing RoomUser does not silently continue after saving the answer:
explicitly fail the submission or log an error and handle the failure according
to the consumer’s existing error flow. Preserve RoomUser::attend for present
participants, and include roomId/userId context in any diagnostic log.

In
`@momogo-core/src/main/java/com/momogo/core/domain/room/event/RoomSubmitEventMessage.java`:
- Around line 17-22: Update the compact constructor of RoomSubmitEventMessage to
explicitly validate answers with Objects.requireNonNull using a Korean
field-specific message before copying it, and add equivalent null validation for
submittedAt. Keep the existing validation and List.copyOf immutability behavior
unchanged for the other fields.

In
`@momogo-core/src/main/java/com/momogo/core/domain/room/kafka/producer/RoomSubmitKafkaProducer.java`:
- Around line 21-32: 제출 파이프라인의 내구성 경계를 보강하세요.
momogo-core/src/main/java/com/momogo/core/domain/room/kafka/producer/RoomSubmitKafkaProducer.java
21-32의 RoomSubmitKafkaProducer.send()가 발행 결과를 나타내는 CompletableFuture를 반환하도록
변경하고, 호출자가 짧은 타임아웃으로 발행 확정을 기다리게 하세요.
momogo-api/src/main/java/com/momogo/api/room/consumer/RoomSubmitKafkaConsumer.java
47-49의 RoomSubmitKafkaConsumer에는 DefaultErrorHandler와
DeadLetterPublishingRecoverer를 등록해 재시도 불가능한 소비 실패를 DLT에 보관하세요.

In
`@momogo-core/src/main/java/com/momogo/core/domain/room/service/RoomServiceImpl.java`:
- Around line 261-300: Update the submission flow around the visible lock
acquisition and roomSubmitKafkaProducer.send call to atomically claim a Redis
marker keyed by roomId and userId while the lock is held, and only publish the
event when the claim succeeds; reject or treat subsequent claims as duplicate
submissions. Add the corresponding consumer-failure compensation to remove the
marker so failed processing can be retried, and retain database
unique-constraint handling in the consumer as the final duplicate safeguard.
- Around line 249-266: submitRoomAnswer에 `@Transactional`(propagation =
Propagation.NOT_SUPPORTED)를 지정해 클래스의 readOnly 트랜잭션이 락 대기 중 시작되지 않도록 변경하세요. 락 획득
후 수행하는 검증 조회와 답안 저장은 별도 트랜잭션 메서드 또는 컴포넌트로 분리해 기존 readOnly 검증과 쓰기 동작을 유지하세요.

---

Nitpick comments:
In
`@momogo-api/src/main/java/com/momogo/api/room/consumer/RoomSubmitKafkaConsumer.java`:
- Around line 53-58: RoomSubmitKafkaConsumer의 멱등성 처리에서 hasKey 기반 검사와 후속 기록을
setIfAbsent 원자적 선점으로 변경하세요. 처리 시작 시 dedupKey를 “처리 중” 상태로 등록하고, 선점에 실패하면 기존 중복 스킵
흐름을 유지하세요. 트랜잭션 커밋 후 afterCommit에서 키의 TTL을 확정 값으로 갱신하고, 롤백 또는 완료되지 않은 경우
afterCompletion에서 키를 삭제해 재시도가 가능하도록 하세요.

In
`@momogo-core/src/main/java/com/momogo/core/domain/room/kafka/producer/RoomSubmitKafkaProducer.java`:
- Line 19: Review RoomSubmitKafkaProducer and the existing AiGradingProducer
bean wiring; if a dedicated KafkaTemplate<String, RoomSubmitEventMessage> can be
injected safely, narrow the field type and preserve event-specific publishing,
otherwise retain KafkaTemplate<String, Object> when both producers share the
same application-context bean.

In
`@momogo-core/src/main/java/com/momogo/core/domain/room/service/RoomServiceImpl.java`:
- Around line 298-304: In the answer-submission lock timeout handling within
RoomServiceImpl, replace the authentication-owned
AuthErrorCode.LOCK_ACQUISITION_FAILED with an appropriate RoomErrorCode or
shared GlobalErrorCode. In the InterruptedException catch, retain thread
interruption and include the caught exception as the cause or in logging before
throwing the internal-server error.
🪄 Autofix

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f8b38573-7c08-43e9-9e9f-e0796faa8f83

📥 Commits

Reviewing files that changed from the base of the PR and between de9b5bd and 9da1aec.

📒 Files selected for processing (5)
  • momogo-api/src/main/java/com/momogo/api/room/consumer/RoomSubmitKafkaConsumer.java
  • momogo-core/src/main/java/com/momogo/core/common/config/KafkaTopics.java
  • momogo-core/src/main/java/com/momogo/core/domain/room/event/RoomSubmitEventMessage.java
  • momogo-core/src/main/java/com/momogo/core/domain/room/kafka/producer/RoomSubmitKafkaProducer.java
  • momogo-core/src/main/java/com/momogo/core/domain/room/service/RoomServiceImpl.java

…rocessing and Redisson lock

    - Replace pessimistic DB lock with Redisson distributed lock outside DB transaction
    - Prevent TOCTOU duplicate submissions using Redis claim markers (setIfAbsent)
    - Enforce synchronous producer ACK wait with 3-second timeout for message durability
    - Implement RoomSubmitKafkaConsumer with atomic Redis SETNX deduplication and rollback cleanup
    - Validate problem room ownership and count synchronously in Service and Consumer
    - Throw NOT_ROOM_PARTICIPANT exception on missing RoomUser during attendance update
    - Add LOCK_ACQUISITION_FAILED error code to RoomErrorCode for domain boundary isolation
    - Add non-null invariant checks to RoomSubmitEventMessage compact constructor
@SungHuii
SungHuii merged commit fba0b47 into develop Aug 11, 2026
2 checks passed
@SungHuii
SungHuii deleted the refactor/room-submit-kafka-redis branch August 11, 2026 12:29
@coderabbitai coderabbitai Bot mentioned this pull request Aug 11, 2026
2 tasks
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