Skip to content

#19 feat: LLM 응답 불가 시 사장님 웹 실시간 알림 SSE 구현#20

Merged
minseo0313 merged 2 commits into
mainfrom
feature/#19-sse-notification
May 22, 2026
Merged

#19 feat: LLM 응답 불가 시 사장님 웹 실시간 알림 SSE 구현#20
minseo0313 merged 2 commits into
mainfrom
feature/#19-sse-notification

Conversation

@minseo0313
Copy link
Copy Markdown
Collaborator

@minseo0313 minseo0313 commented May 22, 2026

💡 개요

Issue Number: #19

🪐 주요 변경 사항

  • LLM이 고객 문의에 응답 불가 시 SSE를 통해 사장님 웹에 실시간 알림 전송

✅ 상세 내용

  • SseService 구현 (emitter 관리, 연결/전송/해제)
  • GET /api/v1/sse/connect - 사장님 웹 SSE 연결 엔드포인트
  • POST /api/v1/sse/notify - AI 서버가 응답 불가 시 호출하는 알림 트리거 엔드포인트
  • 요청 바디: customer_name, waiting (Boolean)

🔔 참고 사항

  • 사장님이 웹을 닫으면 알림 수신 불가 (SSE 특성상 연결 유지 중에만 수신)
  • 웹이 꺼져있을 때 알림은 로그만 남기고 소멸됨
  • AI 서버 → POST /api/v1/sse/notify → 백엔드 → 사장님 웹으로 이벤트 전송
  • 프론트에서 EventSource/api/v1/sse/connect 연결 후 inquiry 이벤트 수신
  • 변경 목적: AI 서버에서 LLM이 응답하지 못할 때 사장님 웹에 Server-Sent Events(SSE)로 실시간 알림을 전달하기 위함입니다.

  • 주요 변경 내용:

    • SseService: SSE 연결 및 이벤트 전송 관리
      • connect(): Long.MAX_VALUE 타임아웃으로 SseEmitter 생성 및 onCompletion/onTimeout 콜백 처리하여 연결 상태 관리
      • send(customerName, waiting): 고객명과 대기 상태를 담은 inquiry 이벤트 전송 (미연결 시 로그 기록, 전송 실패 시 emitter 해제)
      • SseEmitter 콜백 관련 경쟁 조건으로 인한 활성 연결 조기 해제 버그 수정
    • SseController: SSE 엔드포인트 제공
      • GET /api/v1/sse/connect: 사장님 웹의 SSE 연결 요청 처리
      • POST /api/v1/sse/notify: AI 서버의 알림 전송 요청 처리 (요청 바디: customer_name, waiting)
    • SseNotifyRequest: 알림 요청 DTO (customerName, waiting 필드, Jakarta validation 및 JSON 매핑 포함)
  • 영향 범위:

    • API 변경: 2개 신규 엔드포인트 추가 (/api/v1/sse/connect, /api/v1/sse/notify)
    • DB 스키마 변경: 없음
    • 설정 변경: 없음

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 22, 2026

📝 Walkthrough

Walkthrough

SSE 기반 실시간 알림 기능을 추가합니다. 클라이언트가 /api/v1/sse/connect로 연결하면 서버가 SseEmitter를 생성·유지하고, /api/v1/sse/notify 요청으로 inquiry 이벤트에 고객명과 대기 상태를 전송합니다.

변경 사항

SSE 연결 및 알림 기능

Layer / File(s) 요약
알림 요청 DTO 및 유효성 검증
src/main/java/com/nailagent/backend/global/sse/SseNotifyRequest.java
SseNotifyRequest를 추가해 customerNamewaiting 필드를 정의합니다. Jackson customer_name 매핑, Jakarta 유효성 제약, OpenAPI 스키마 메타데이터와 Lombok 생성자를 포함합니다.
SSE 서비스 로직
src/main/java/com/nailagent/backend/global/sse/SseService.java
SseService가 emitter 생명주기(connect)와 이벤트 전송(send)을 관리합니다. 이전 emitter를 완료 처리하고 새 SseEmitter(Long.MAX_VALUE)를 생성·할당하며, 완료/타임아웃 콜백에서 해제합니다. send()는 emitter 존재 여부를 확인하고 inquiry 이벤트로 {customerName, waiting}을 전송하며, 전송 실패 시 emitter를 초기화합니다.
REST 엔드포인트
src/main/java/com/nailagent/backend/global/sse/SseController.java
GET /api/v1/sse/connect 엔드포인트는 SseService.connect() 반환값을 전달하고, POST /api/v1/sse/notify@Valid SseNotifyRequest를 받아 SseService.send(...)를 호출한 뒤 표준 ApiResponse로 성공을 반환합니다.

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})
Loading

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목이 변경사항의 핵심을 명확하게 반영하고 있습니다. SSE를 통한 실시간 알림 구현이라는 주요 변경사항을 구체적으로 설명하고 있으며, 컨텍스트(LLM 응답 불가 시)를 포함하여 의도를 명확히 합니다.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 feature/#19-sse-notification

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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/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

📥 Commits

Reviewing files that changed from the base of the PR and between bd45f29 and 4166a56.

📒 Files selected for processing (3)
  • src/main/java/com/nailagent/backend/global/sse/SseController.java
  • src/main/java/com/nailagent/backend/global/sse/SseNotifyRequest.java
  • src/main/java/com/nailagent/backend/global/sse/SseService.java

Comment thread src/main/java/com/nailagent/backend/global/sse/SseService.java Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/main/java/com/nailagent/backend/global/sse/SseService.java (2)

40-43: ⚡ Quick win

IOException 발생 시 클라이언트에 에러 상태를 전달해주면 더 좋아요

현재는 전송 실패 시 로그만 남기고 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

📥 Commits

Reviewing files that changed from the base of the PR and between 4166a56 and b51e2fc.

📒 Files selected for processing (1)
  • src/main/java/com/nailagent/backend/global/sse/SseService.java

@minseo0313 minseo0313 merged commit 6b3fe02 into main May 22, 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