Skip to content

feat : 스킬교환 알림 Notification 도메인 연결 및 채팅 Redis Outbox 패턴 적용#156

Merged
ekdan38 merged 34 commits into
mainfrom
feature/notification/message
Apr 16, 2026
Merged

feat : 스킬교환 알림 Notification 도메인 연결 및 채팅 Redis Outbox 패턴 적용#156
ekdan38 merged 34 commits into
mainfrom
feature/notification/message

Conversation

@chuuminggg
Copy link
Copy Markdown
Member

🔥ISSUE

feat : 스킬교환 요청 이벤트를 Notification 도메인에 연결 (0cc0de4)

  • [Before] 기존 SkillExchange.isRequesterRead / isReceiverRead 플래그만으로 빨간점을 표시하던 방식
  • [After] Notification 테이블 + Redis pub/sub 기반 알림으로 연결

refactor : 채팅 Redis 발행을 Transactional Outbox 패턴으로 내재화 (fddb8da)

  • [Before] ChatStompController에서 서비스 호출 후 publishToRedis() / publishReadEvent()를 직접 호출
  • [After] saveMessage() / markAsRead() 내부에서 afterCommit 콜백으로 처리하도록 변경

📒TODO

  1. Notification 테이블 + Redis pub/sub 기반 알림으로 연결.
  2. fddb8da : publishToRedis, publishReadEventprivate doPublishToRedis, doPublishReadEvent로 전환
  3. fddb8da : afterCommit 내부 예외는 Spring이 억제하므로 throw 대신 로그로 처리
  4. fddb8da : Controller는 단순히 서비스만 호출하면 되고, Redis 발행은 서비스가 책임

📋REF

이벤트별 알림 매핑

이벤트 수신자 알림 타입
요청 생성 멘토(receiver) REQUEST_RECEIVED
요청 생성 멘티(requester) REQUEST_SENT
수락 / 거절 멘티(requester) REQUEST_STATUS_CHANGED
취소 by requester 멘토(receiver) REQUEST_STATUS_CHANGED
취소 by receiver 멘티(requester) REQUEST_STATUS_CHANGED
만료 (expire) 멘티 + 멘토 양측 REQUEST_STATUS_CHANGED (시스템)

변경 파일

  • SkillExchangeRequestProcessor — 요청 생성 시 알림 생성
  • SkillExchangeServiceImpl — 수락/거절/취소 시 알림 생성, 보낸/받은 요청 조회 시 Notification 읽음 처리 병행
  • SkillExchangeExpireProcessor — 만료 시 양측 시스템 알림 생성
    기존 SkillExchange 플래그 로직(bulkUpdateRequesterReadStatus 등)은 그대로 유지 (하위 호환).

chuuminggg and others added 30 commits January 21, 2026 23:40
…nk 통합 처리, 시스템 알림과 탈퇴/미설정 유저 구분, toMap merge function 추가로 중복 키 예외 방지)
요청 생성·수락·거절·취소·만료 시 Notification 레코드를 생성하여
알림 센터 노출, /notifications/unread-count 정상화,
WebSocket 실시간 알림 푸시를 지원한다.
기존 SkillExchange isRequesterRead/isReceiverRead 플래그 로직은 유지하며,
getSentRequests/getReceivedRequests 조회 시 Notification 읽음 처리도 병행한다.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
saveMessage/markAsRead 내부에서 afterCommit 콜백으로 Redis 발행을 처리하여
Controller에서 publishToRedis/publishReadEvent를 직접 호출하지 않도록 개선.
publish 메서드를 private으로 변경하고 afterCommit 내 예외는 로그로 처리.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@chuuminggg chuuminggg requested review from ekdan38 and jaey-oung April 14, 2026 11:46
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request implements the Transactional Outbox pattern for chat messages and notifications, ensuring that Redis events are published only after a successful database transaction commit. It also integrates the notification service into the skill exchange workflow, triggering alerts for status changes, sent requests, and received requests. Review feedback focuses on improving the robustness of the transaction synchronization logic by checking for active transactions and preventing potential NullPointerExceptions related to JPA auditing fields that may not be populated before a flush.

.text(saved.getContent())
.messageType(saved.getMessageType().name())
.imageUrl(saved.getFileUrl())
.sentAtEpochMs(saved.getCreatedAt().toInstant(ZoneOffset.UTC).toEpochMilli())
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

saved.getCreatedAt()이 null일 경우 NullPointerException이 발생할 위험이 있습니다. JPA Auditing(@CreatedDate) 필드는 트랜잭션 내에서 엔티티가 아직 flush되지 않은 상태라면 채워지지 않을 수 있습니다. 트랜잭션 커밋 전에 해당 값이 필요하다면, 엔티티 저장 후 명시적으로 flush()를 호출하여 필드 생성을 강제하는 것이 좋습니다. 또한 프로젝트의 시간대 정책에 맞게 변환 로직을 점검해 주시기 바랍니다.

chatMessageRepository.save(message);
chatMessageRepository.flush();
References
  1. In a transactional context, if a newly created entity's ID or auditing fields are needed for subsequent operations before the transaction commits, explicitly call flush() after saving the entity to force generation and persistence.

Comment on lines +111 to +116
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
doPublishToRedis(roomId, savedMessageId, payload);
}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

TransactionSynchronizationManager.registerSynchronization을 호출하기 전에 현재 활성화된 트랜잭션이 있는지 확인하는 것이 안전합니다. NotificationServiceImpl에서 구현된 방식과 마찬가지로 isActualTransactionActive() 체크를 추가하여, 트랜잭션이 없는 환경에서 호출될 경우 IllegalStateException이 발생하는 것을 방지하고 즉시 Redis 발행이 이루어지도록 개선하는 것이 좋습니다.

        if (TransactionSynchronizationManager.isActualTransactionActive()) {
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                @Override
                public void afterCommit() {
                    doPublishToRedis(roomId, savedMessageId, payload);
                }
            });
        } else {
            doPublishToRedis(roomId, savedMessageId, payload);
        }

Comment on lines +185 to +190
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
doPublishReadEvent(roomId, userId, lastReadId);
}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

위의 saveMessage 메서드와 동일하게, 트랜잭션 동기화 등록 전에 활성 트랜잭션 여부를 확인하는 로직을 추가하여 코드의 견고함을 높이는 것이 좋습니다.

        if (TransactionSynchronizationManager.isActualTransactionActive()) {
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                @Override
                public void afterCommit() {
                    doPublishReadEvent(roomId, userId, lastReadId);
                }
            });
        } else {
            doPublishReadEvent(roomId, userId, lastReadId);
        }

chuuminggg and others added 3 commits April 14, 2026 20:50
- chatMessageRepository.flush() 추가로 save 후 @CreatedDate 필드 즉시 반영 보장 (NPE 방지)
- saveMessage/markAsRead의 TransactionSynchronization 등록 전 isActualTransactionActive() 체크 추가
  (트랜잭션 없는 환경에서 IllegalStateException 방지, 즉시 Redis 발행으로 폴백)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
saveMessage 내부에서 트랜잭션 없는 테스트 환경 시 doPublishToRedis가
즉시 호출되어 objectMapper.writeValueAsString()에서 NPE 발생.
ObjectMapper를 @mock으로 등록하여 해결.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Collaborator

@ekdan38 ekdan38 left a comment

Choose a reason for hiding this comment

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

고생하셨습니다~

@ekdan38 ekdan38 merged commit ccfaed2 into main Apr 16, 2026
1 check 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.

2 participants