#19 feat: LLM 응답 불가 시 사장님 웹 실시간 알림 SSE 구현#20
Conversation
📝 WalkthroughWalkthroughSSE 기반 실시간 알림 기능을 추가합니다. 클라이언트가 변경 사항SSE 연결 및 알림 기능
Sequence Diagram(s)sequenceDiagram
participant Client
participant SseController
participant SseService
participant SseEmitter
Client->>SseController: GET /api/v1/sse/connect
SseController->>SseService: connect()
SseService->>SseEmitter: new SseEmitter(Long.MAX_VALUE)
SseService-->>SseController: return SseEmitter
Client->>SseController: POST /api/v1/sse/notify (customer_name, waiting)
SseController->>SseService: send(customerName, waiting)
SseService->>SseEmitter: sendEvent(name: "inquiry", payload: {customerName, waiting})
🎯 3 (Moderate) | ⏱️ ~20 minutes 🚥 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 docstrings
🧪 Generate unit tests (beta)
Comment |
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/nailagent/backend/global/sse/SseService.java`:
- Around line 14-23: connect() currently assigns callbacks that set the shared
field emitter to null, so a late completion/timeout of an old SseEmitter can
null-out a newly created emitter; fix by capturing the newly created emitter
into a final local variable (e.g., final SseEmitter current = new
SseEmitter(...)) and use current in onCompletion/onTimeout callbacks so they
only clear the field if the field still equals that captured instance. Do the
same safety check in send(): when catching IOException and deciding to null the
field, only set emitter = null if emitter == the local reference used for
sending (or otherwise compare to the captured instance) so you never clear a
newer active emitter.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 3d0ac08e-8ba0-4bd7-a0b5-6231b4fec287
📒 Files selected for processing (3)
src/main/java/com/nailagent/backend/global/sse/SseController.javasrc/main/java/com/nailagent/backend/global/sse/SseNotifyRequest.javasrc/main/java/com/nailagent/backend/global/sse/SseService.java
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/main/java/com/nailagent/backend/global/sse/SseService.java (2)
40-43: ⚡ Quick winIOException 발생 시 클라이언트에 에러 상태를 전달해주면 더 좋아요
현재는 전송 실패 시 로그만 남기고
emitter를 null로 설정하는데,current.completeWithError(e)를 호출해주시면 클라이언트 측에서도 연결 오류를 인지할 수 있습니다.🔧 개선 제안
} catch (IOException e) { log.error("SSE 전송 실패", e); - if (emitter == current) emitter = null; + if (emitter == current) { + emitter = null; + current.completeWithError(e); + } }🤖 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/nailagent/backend/global/sse/SseService.java` around lines 40 - 43, In SseService inside the catch(IOException e) block (where you currently log the error and set emitter = null), call current.completeWithError(e) to notify the connected client of the error before clearing the emitter; ensure current is non-null and handle any exceptions from completeWithError, then set emitter = null only if current == emitter to preserve existing guard logic.
20-24: ⚖️ Poor tradeoff경쟁 조건 수정 잘 적용하셨습니다! 👍
이전 리뷰에서 지적된 race condition 문제를
newEmitter지역 변수 캡처와 identity check (emitter == newEmitter)로 해결하셨네요. 이제 이전 emitter의 콜백이 새로운 emitter를 null로 만드는 문제가 방지됩니다.더 방어적으로 가려면
AtomicReference<SseEmitter>와compareAndSet을 사용하는 방법도 고려해보세요. Spring 싱글톤 서비스이기 때문에 여러 스레드가 동시에connect()를 호출하거나send()와connect()가 동시에 실행될 가능성이 있어, 현재 방식은 필드 읽기/쓰기가 원자적이지 않아 미묘한 race가 남아있을 수 있습니다.🔒 AtomicReference 기반 개선안 (선택사항)
import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.io.IOException; import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; `@Slf4j` `@Service` public class SseService { - private SseEmitter emitter; + private final AtomicReference<SseEmitter> emitterRef = new AtomicReference<>(); public SseEmitter connect() { + SseEmitter newEmitter = new SseEmitter(Long.MAX_VALUE); + SseEmitter oldEmitter = emitterRef.getAndSet(newEmitter); - if (emitter != null) { - emitter.complete(); + if (oldEmitter != null) { + oldEmitter.complete(); } - SseEmitter newEmitter = new SseEmitter(Long.MAX_VALUE); - emitter = newEmitter; - newEmitter.onCompletion(() -> { if (emitter == newEmitter) emitter = null; }); - newEmitter.onTimeout(() -> { if (emitter == newEmitter) emitter = null; }); + newEmitter.onCompletion(() -> emitterRef.compareAndSet(newEmitter, null)); + newEmitter.onTimeout(() -> emitterRef.compareAndSet(newEmitter, null)); return newEmitter; }🤖 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/nailagent/backend/global/sse/SseService.java` around lines 20 - 24, Replace the non-atomic field reads/writes on the SseEmitter field in SseService with an AtomicReference<SseEmitter> to eliminate remaining race windows: change the emitter field to AtomicReference<SseEmitter>, use emitter.compareAndSet(null, newEmitter) (or a loop with compareAndSet) when installing the new SseEmitter in connect(), and update the onCompletion/onTimeout callbacks to clear the AtomicReference only if compareAndSet(newEmitter, null) succeeds; also update send()/other users to use emitter.get() atomically. This keeps the identity checks (newEmitter) but makes updates and clears atomic.
🤖 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.
Nitpick comments:
In `@src/main/java/com/nailagent/backend/global/sse/SseService.java`:
- Around line 40-43: In SseService inside the catch(IOException e) block (where
you currently log the error and set emitter = null), call
current.completeWithError(e) to notify the connected client of the error before
clearing the emitter; ensure current is non-null and handle any exceptions from
completeWithError, then set emitter = null only if current == emitter to
preserve existing guard logic.
- Around line 20-24: Replace the non-atomic field reads/writes on the SseEmitter
field in SseService with an AtomicReference<SseEmitter> to eliminate remaining
race windows: change the emitter field to AtomicReference<SseEmitter>, use
emitter.compareAndSet(null, newEmitter) (or a loop with compareAndSet) when
installing the new SseEmitter in connect(), and update the
onCompletion/onTimeout callbacks to clear the AtomicReference only if
compareAndSet(newEmitter, null) succeeds; also update send()/other users to use
emitter.get() atomically. This keeps the identity checks (newEmitter) but makes
updates and clears atomic.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 0b009303-7a19-44af-986f-c0bf9e970512
📒 Files selected for processing (1)
src/main/java/com/nailagent/backend/global/sse/SseService.java
💡 개요
Issue Number: #19
🪐 주요 변경 사항
✅ 상세 내용
SseService구현 (emitter 관리, 연결/전송/해제)GET /api/v1/sse/connect- 사장님 웹 SSE 연결 엔드포인트POST /api/v1/sse/notify- AI 서버가 응답 불가 시 호출하는 알림 트리거 엔드포인트customer_name,waiting(Boolean)🔔 참고 사항
POST /api/v1/sse/notify→ 백엔드 → 사장님 웹으로 이벤트 전송EventSource로/api/v1/sse/connect연결 후inquiry이벤트 수신변경 목적: AI 서버에서 LLM이 응답하지 못할 때 사장님 웹에 Server-Sent Events(SSE)로 실시간 알림을 전달하기 위함입니다.
주요 변경 내용:
connect(): Long.MAX_VALUE 타임아웃으로 SseEmitter 생성 및 onCompletion/onTimeout 콜백 처리하여 연결 상태 관리send(customerName, waiting): 고객명과 대기 상태를 담은inquiry이벤트 전송 (미연결 시 로그 기록, 전송 실패 시 emitter 해제)GET /api/v1/sse/connect: 사장님 웹의 SSE 연결 요청 처리POST /api/v1/sse/notify: AI 서버의 알림 전송 요청 처리 (요청 바디:customer_name,waiting)customerName,waiting필드, Jakarta validation 및 JSON 매핑 포함)영향 범위:
/api/v1/sse/connect,/api/v1/sse/notify)