Files
comrelay/docs/notification-review.md
ttipo fa4bc40761 docs: 8단계 진행현황·API 계약 갱신 + 알림/Redis 검토 문서 추가
- integration-progress/api-contract: 8-1 페이지네이션·8-2 알림·8-3 Redis 캐싱 반영.
- notification-review.md: 알림 시스템 전수 검토(유형/빠짐/주의) + 보완 이력.
- redis-cache-review.md: Redis 캐시 전수 검토 + 보완 이력.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 13:13:07 +09:00

12 KiB

알림(Notification) 시스템 전체 검토

검토일: 2026-06-19 · 대상: 8-2단계 실시간 알림(인앱 + SSE) 구현 전체 관련 계약: api-contract.md §8 · 진행 기록: integration-progress.md §8-2 코드 기준: backend/src/modules/notification/, task.service.ts·member.service.tsnotify 호출부, frontend/src/stores/notification.store.ts·layouts/AppShell.vue

이 문서는 세 가지 질문에 답한다 — ① 어떤 알림을 구현했는가, ② 빠진 부분은 없는가, ③ 잘못된 부분은 없는가.

갱신(2026-06-19): 본 검토의 빠짐(gap) 전건 + 확장(R6) 보완 완료 — P1(G1·G2·G3) + P2(G4·G5) + P3(G6·G7) + R5(답글 구분) + R6(Redis pub/sub 다중 인스턴스 실시간). 알림 유형 6→12종, 미보완 gap 0건, 남은 건 경미한 견고성 R1~R4 뿐. (아래 §2·§5·§6·§7 에 반영)


1. 아키텍처 요약

  • 수신함 모델: 활동 피드(저장소 전체 로그)와 별개로, 사용자별 Notification(recipient·type·payload·read) 수신함을 둔다.
  • 적재 경로: 도메인 서비스(Task/Member)가 행위 시점에 NotificationService.notify({recipientIds, actorId, type, payload}) 호출 → 행위자 제외 + DB 적재(베스트 에포트) → 실시간 전달(REDIS_HOST 설정 시 Redis pub/sub fan-out, 미설정 시 인메모리 Subject).
  • 실시간 전송: SSE(@Sse('stream')), 쿠키 인증, 25초 하트비트. 표준 응답 래퍼는 @SkipTransform() 으로 우회.
  • 프론트: store 싱글톤이 EventSource 보유(화면 전환에 안 끊김), 상단바 종에 미읽음 뱃지 + 드롭다운. type+payload→문구/이동경로 파생(escapeHtml).

2. 구현된 알림 유형 (12종)

업무 8종 + 멤버 3종 + 댓글/답글 1종. (P1·P2·P3 보완 반영)

# type 트리거(코드) 수신자 payload 핵심 프론트 문구 / 이동
1 task.assigned 업무 생성(create) · 수정 시 추가된 담당자(update) 담당자(추가분) repoId·taskSeq·taskTitle·actorName "{actor}님이 {업무}를 지시했습니다" → 업무 상세 blue
2 task.unassigned 수정 시 제외된 담당자(update) 담당자(제거분) "{actor}님이 {업무} 담당에서 회원님을 제외했습니다" → 업무 상세 red
3 task.approval_requested 상태 전이 →review(transition) 지시자(issuer) "{actor}님이 {업무} 승인을 요청했습니다" → 업무 상세 amber
4 task.approved 상태 전이 →done 담당자 전원 "{업무} 업무가 승인되었습니다" → 업무 상세 green
5 task.changes_requested 상태 전이 →changes 담당자 전원 "{actor}님이 {업무} 수정을 요청했습니다" → 업무 상세 red
6 task.started 상태 전이 →prog(시작/재개) 지시자(issuer) "{actor}님이 {업무}를 시작했습니다" → 업무 상세 blue
7 task.due_changed 수정 시 마감 변경(update) 담당자 전원 〃 + dueDate "{actor}님이 {업무}의 마감기한을 {날짜}로 변경/해제했습니다" → 업무 상세 blue
8 task.removed 업무 삭제(remove) 지시자 + 담당자 〃(삭제 전 스냅샷) "{actor}님이 {업무} 업무를 삭제했습니다" → 저장소(업무 소멸) red
9 task.commented 댓글(addComment) · 답글(addReply) 지시자 + 담당자 + (답글 시) 원댓글 작성자 〃 + (답글 시) isReply "{actor}님이 {업무}에 댓글/답글을 남겼습니다" → 업무 상세 (기본)
10 member.invited 멤버 초대(invite) 피초대자 repoId·repoName·roleType·actorName "{actor}님이 {저장소}에 초대했습니다" → 저장소 accent
11 member.role_changed 멤버 역할 변경(update) 역할이 바뀐 당사자 repoId·repoName·roleType·actorName "{actor}님이 …역할을 {관리자/멤버}로 변경했습니다" → 저장소 accent
12 member.removed 멤버 제거(remove) 제거된 당사자 repoId·repoName·actorName "{actor}님이 {저장소}에서 회원님을 제외했습니다" → 저장소 red

모든 경우 행위자 본인은 수신 대상에서 자동 제외(notify 내부 Set 중복 제거 + actorId 필터). 빈 수신자면 적재/푸시 생략.


3. 검토 결과 한눈에

구분 건수 비고
정상 동작 확인 9 런타임 실시간 수신 포함 확인됨
⚠️ 빠진 부분(gap) 7 → 0 (전건 보완 완료) 활동 대비 알림 격차 해소
🐛 주의/개선 6 → 4 (R5·R6 해소) 기능 버그는 없음. 남은 항목은 경미한 견고성(R1~R4)

종합 판정: 핵심 업무 협업 플로우(지시→진행→승인/수정요청→댓글, 초대)의 알림은 정상 구현·동작한다. 치명적 버그 없음. 빠짐(gap) 7건은 P1/P2/P3 로 전건 보완 완료(2026-06-19) — 의도적 제외 항목(저장소 설정 변경·파일 첨부 등)을 빼면 알림화할 도메인 이벤트는 모두 커버됨. 남은 것은 견고성·확장 항목(§6, 특히 R6 다중 인스턴스).


4. 정상 동작 확인 항목

  1. 핵심 플로우 6종 적재·전송 — 위 표대로 트리거·수신자 매핑 정확. 런타임에서 실시간 종 수신 확인됨(사용자 검증).
  2. 행위자 제외 — 자기 행위로 자기 알림이 오지 않음(notify 필터).
  3. 중복 제거 — 지시자=담당자(자가지시·자가담당) 같은 중복 수신자는 1건으로 합쳐짐([...new Set]).
  4. 권한·격리streamFor(userId)/listFor/markRead/markAllRead 모두 인증 사용자 본인 알림으로 한정. 타인 알림 열람·읽음 처리 불가(recipient WHERE 조건).
  5. 베스트 에포트notify 전체가 try/catch 로 감싸져 알림 실패가 본 행위(업무 생성 등)를 막지 않음.
  6. XSS 안전 — payload 의 사용자 입력(actorName·taskTitle·repoName)을 프론트에서 escapeHtml 후 신뢰 태그(<b>)만 v-html 조립.
  7. 연결 수명 — EventSource 를 store 싱글톤이 보유 → 화면 전환에 안 끊김, 로그아웃 시 해제. 멱등 connect().
  8. null 안전 — 지시자 없음(issuer SET NULL)·담당자 0명 시 빈 수신자로 처리.
  9. 읽음 동기화 — 단건/전체 읽음 후 서버 unreadCount 로 보정.

5. ⚠️ 빠진 부분 (Gaps)

핵심 원인: 활동 피드는 11종 이벤트를 적재하지만 알림은 6종만 발생시킨다. 활동에는 있으나 알림이 없는 이벤트가 아래다.

# 누락 알림 현재 상태 권고 수신자 우선순위 비고
G1 멤버 역할 변경(member.role_changed) 보완 완료 — 역할 변경 시 당사자에게 알림 역할이 바뀐 당사자 P1 완료 2026-06-19
G2 멤버 제거(member.removed) 보완 완료 — 제거 시 당사자에게 알림 제거된 당사자 P1 완료 2026-06-19
G3 답글 → 원댓글 작성자 보완 완료addReply 가 원댓글 작성자도 수신자에 포함 원댓글 작성자(비참여자여도) P1 완료 2026-06-19
G4 담당자에서 제외 보완 완료update 제거분에 task.unassigned 제외된 담당자 P2 완료 2026-06-19
G5 마감 변경 보완 완료update 마감 변경 시 task.due_changed(payload dueDate) 담당자 P2 완료 2026-06-19
G6 업무 삭제 보완 완료remove 전 스냅샷 후 task.removed(이동=저장소) 담당자·지시자 P3 완료 2026-06-19
G7 업무 시작/재개 보완 완료transitionprog 에서 task.started 지시자 P3 완료 2026-06-19

의도적으로 제외(정상): 저장소 생성/이름변경/공개범위 변경(repo.*)·파일 첨부/삭제(task.file_*)·업무 일반 수정(task.edited)·체크리스트 개별 완료. 노이즈 대비 가치가 낮아 활동 피드로만 표현. (필요 시 추후 추가 가능)

보완 시 주의

  • G1/G2(멤버): member.role_changed/removed 는 활동 적재 직후 notify({recipientIds:[targetUserId], actorId:actingUserId, type, payload:{repoId,repoName,roleType?,actorName}}) 한 줄 추가로 해결. 단 프론트 notification.storetoView 에 두 type 분기 + 타입 정의(NotificationType) 추가 필요.
  • G3(답글): addReply 에서 수신자에 parent.author?.id 를 추가하면 됨(이미 parent 로딩됨). 중복은 notify 가 dedup.

6. 🐛 주의 / 개선 필요 (견고성·UX·확장)

기능 오류(버그)는 발견되지 않았다. 아래는 견고성·정합·확장 관점의 개선점이다.

# 항목 영향 권고
R1 초기 fetch ↔ SSE 구독이 비원자적 connect()await fetchInitial()openStream() — 그 사이 ms 윈도에 생성된 알림은 실시간 푸시 누락(DB엔 남아 새로고침 시 노출). 표준 SSE 한계. 필요 시 구독 먼저→직후 목록 fetch 순서로 바꿔 누락→중복(이미 dedup) 쪽으로 전환
R2 notify 가 요청 경로에서 await 업무 생성/전이/댓글마다 알림 INSERT + 푸시를 동기 대기 → 약간의 지연(실패는 흡수). 영향 미미. 트래픽 증가 시 void(fire-and-forget) 또는 큐로 분리 검토
R3 markAllRead 클라 가드 unreadCount===0 이면 호출 생략 — 클라 카운트가 드리프트로 0이면 서버 미읽음을 못 비움. 드물고 다음 fetch 로 자가 보정. 가드 제거 또는 주기적 재동기 고려
R4 댓글 알림 광범위 + 음소거 없음 task.commented 가 지시자+담당자 전원에게 발송. 대규모 팀·활발한 스레드에서 노이즈. 현재 규모 허용. 추후 사용자별 음소거/멘션 한정 검토
R5 답글이 일반 댓글과 동일 문구 해소addReply payload isReply:true → 프론트 "답글을 남겼습니다"로 구분 표시 2026-06-19
R6 단일 인스턴스 전용 해소(2026-06-19)NotificationServiceREDIS_HOST 설정 시 Redis pub/sub(ioredis)로 fan-out: notify 가 채널 relay:notifications 에 발행, 모든 인스턴스가 구독해 각자 로컬 stream$ 로 전달 → 다중 인스턴스에서도 실시간 도달. 미설정 시 인메모리 폴백. 2026-06-19
R7 recipients[i]saved[i] 순서 의존(참고) save(entities) 반환 순서가 입력 순서와 같다는 전제로 userId 라우팅. TypeORM 보장 범위 내 정상이나 암묵적. 견고화하려면 recipient.id 를 응답에 포함해 직접 매핑(선택)

보안·정합 재확인(이상 없음)

  • 타 사용자 알림 접근 불가(§4-4).
  • 토큰 만료 후 유휴 재연결은 5초 간격 무알림 재시도 → 다음 axios 호출의 refresh 로 자가 복구(기존 문서화된 한계).
  • 알림은 도입 이후 발생분만(소급 없음) — 의도된 동작.

7. 권고 요약 (우선순위)

  • P1 (사용자 체감 빠짐 — 우선 보완 권장) 완료(2026-06-19)
    • G1 멤버 역할 변경 알림
    • G2 멤버 제거 알림
    • G3 답글 → 원댓글 작성자 알림
  • P2 — 완료(2026-06-19): G4 담당자 제외 · G5 마감 변경
  • P3 — 완료(2026-06-19): G6 업무 삭제 · G7 업무 시작 · R5 답글 구분
  • 확장(인프라) — 완료(2026-06-19): R6 Redis pub/sub fan-out 전환(다중 인스턴스 실시간 지원)
  • 남은 경미 항목(선택): R1(초기 fetch↔구독 비원자적)·R2(notify await)·R3(markAllRead 가드)·R4(댓글 광범위) — 모두 동작에 영향 없는 견고성/UX 미세조정

알림화할 도메인 이벤트의 빠짐(gap) 전건 + 확장(R6)까지 보완 완료. 남은 R1~R4 는 경미한 견고성 항목으로 동작 영향 없음.