알림 서비스 기능 (Notification Service) - fitpassTeam/fitpass GitHub Wiki

알림 서비스 기능 (Notification Service)

개요

실시간 Server-Sent Events(SSE)와 Redis Pub/Sub을 활용한 알림 시스템입니다. 사용자(User)와 트레이너(Trainer) 모두에게 실시간 알림을 전송하며, 다양한 알림 타입을 지원합니다.

전체 구조 개요

domain/notify/
├── controller/
│   └── NotifyController.java               # SSE 구독 API
├── service/
│   ├── NotifyService.java                 # SSE 연결 및 알림 전송
│   ├── NotificationPublisher.java         # Redis 메시지 발행
│   └── NotificationSubscriber.java        # Redis 메시지 구독
├── entity/
│   └── Notify.java                        # 알림 엔티티
├── dto/
│   ├── NotifyDto.java                     # 응답 DTO
│   └── NotificationEvent.java             # 이벤트 DTO
├── repository/
│   ├── NotifyRepository.java              # 알림 JPA 레포지토리
│   └── EmitterRepository.java             # SSE Emitter 메모리 저장소
├── enums/
│   ├── NotificationType.java              # 알림 유형
│   └── ReceiverType.java                  # 수신자 유형
└── config/
    └── NotificationConfig.java            # Redis 구독 설정

주요 플로우:

SSE 연결 수립 → 알림 발생 → Redis 발행 → 구독자 수신 → DB 저장 → 실시간 전송 → 클라이언트 수신

주요기능

실시간 알림 전송

  • SSE(Server-Sent Events)를 통한 실시간 통신
  • Redis Pub/Sub을 활용한 분산 환경 지원
  • 사용자별 다중 연결 지원

알림 지속성

  • 데이터베이스 저장을 통한 알림 히스토리 관리
  • 오프라인 사용자를 위한 Redis 임시 저장
  • 연결 끊김 시 미전송 알림 복구

다양한 알림 타입

  • YATA, REVIEW, CHAT, RESERVATION, MEMBERSHIP

이중 수신자 지원

  • User와 Trainer 모두 지원
  • 수신자 타입별 독립적인 알림 처리

알림 타입 및 수신자

알림 타입 (NotificationType)

타입 설명 주요 용도
YATA 야타 알림 특별 이벤트, 공지 사항 등
REVIEW 리뷰 알림 리뷰 작성 요청, 리뷰 응답 알림 등
CHAT 채팅 알림 새 메시지 수신 알림
RESERVATION 예약 알림 예약 확정, 취소, 변경 알림
MEMBERSHIP 멤버십 알림 구독 시작, 만료, 갱신 관련 알림

수신자 타입 (ReceiverType)

타입 설명 대상 설명
USER 일반 사용자 피트니스 이용자
TRAINER 트레이너 헬스장 소속 트레이너

실시간 알림 처리 흐름

SSE 연결 수립

GET /notify/subscribe

처리 과정 :

  1. Controller(NotifyController.subscribe)
  • 사용자 인증 확인
  • Last-Event-ID 헤더 확인 (연결 복구용)
  • NotifyService 호출
  1. Service (NotifyService.subscribe)
  • SseEmitter 생성 (타임아웃: 60분)
  • 고유 키 생성: {userId}:{timestamp}
  • EmitterRepository에 연결 저장
  • 연결 완료/타임아웃 시 자동 정리
  • 초기 연결 메시지 전송
  • 끊어진 연결 복구 처리

알림 발송 (로컬 방식)

notifyService.send(user, NotificationType.REVIEW, "새 리뷰가 작성되었습니다", "/reviews/123")

처리 과정 :

  1. 데이터베이스 저장: Notify 엔티티 생성 및 저장
  2. 실시간 전송: 해당 사용자의 모든 SSE 연결에 전송
  3. 오프라인 처리: 연결이 없으면 Redis에 임시 저장

알림 발송 (분산 방식 - Redis Pub/Sub)

notificationPublisher.publishNotificationToUser(userId, NotificationType.CHAT, "새 메시지", "/chat/456")

처리 과정 :

  1. Publisher (NotificationPublisher)
  • NotificationEvent 생성
  • Redis 채널에 메시지 발행
  • 채널명: notification:USER:{userId} 또는 notification:TRAINER:{trainerId}
  1. Subscriber (NotificationSubscriber)
  • Redis 메시지 수신
  • JSON 파싱 후 NotificationEvent 추출
  • 데이터베이스에 Notify 엔티티 저장
  • 해당 수신자의 SSE 연결에 실시간 전송

데이터 저장 구조

Notify 엔티티

@Entity
public class Notify {
    private Long id;                    // 알림 ID
    private String content;             // 알림 내용
    private String url;                 // 관련 URL
    private Boolean isRead;             // 읽음 여부
    private User receiver;              // 사용자 수신자
    private Trainer trainer;            // 트레이너 수신자
    private NotificationType type;      // 알림 타입
    private ReceiverType receiverType;  // 수신자 타입
    private LocalDateTime createdAt;    // 생성일시
}

수신자 구분 로직

// 생성자 오버로딩으로 수신자 타입 자동 설정
public Notify(User receiver, ...) {
    this.receiver = receiver;
    this.receiverType = ReceiverType.USER;
}

public Notify(Trainer trainer, ...) {
    this.trainer = trainer;
    this.receiverType = ReceiverType.TRAINER;
}

// 헬퍼 메소드로 통합 접근
public Long getReceiverId() {
    return receiverType == ReceiverType.USER ? 
        receiver.getId() : trainer.getId();
}

메모리 관리 (EmitterRepository)

SSE 연결 관리

// 연결 저장
Map<String, SseEmitter> emitters = new ConcurrentHashMap<>();
String key = userId + ":" + timestamp;
emitters.put(key, sseEmitter);

// 사용자별 모든 연결 조회
public Map<String, SseEmitter> findAllEmittersById(Long userId) {
    String keyPrefix = userId + ":";
    return emitters.stream()
        .filter(key -> key.startsWith(keyPrefix))
        .collect(toMap());
}

이벤트 캐시 관리

  • 연결 끊김 시 복구를 위한 이벤트 임시 저장
  • Last-Event-ID 기반 미전송 이벤트 재선송

Redis 활용

1. Pub/Sub 메시징

// 발행
String channel = "notification:USER:" + userId;
redisTemplate.convertAndSend(channel, notificationEvent);

// 구독 (ApplicationReadyEvent 시 자동 설정)
redisMessageListenerContainer.addMessageListener(
    notificationSubscriber, 
    new PatternTopic("notification:*")
);

2. 오프라인 알림 저장

// Redis에 임시 저장
public void saveNotifyToRedis(Long userId, Notify notify) {
    String key = "notify:" + userId;
    List<Notify> notifications = getValuesForNotification(key);
    notifications.add(notify);
    redisTemplate.opsForValue().set(key, notifications);
}

클라이언트 연결 예시

SSE 구독

const eventSource = new EventSource('/notify/subscribe', {
    headers: {
        'Authorization': 'Bearer ' + token,
        'Last-Event-ID': lastEventId  // 연결 복구용
    }
});

eventSource.onmessage = function(event) {
    const notification = JSON.parse(event.data);
    displayNotification(notification);
};

eventSource.onerror = function(event) {
    // 재연결 로직
    setTimeout(() => {
        eventSource = new EventSource('/notify/subscribe');
    }, 3000);
};

알림 생명주기

알림 발생 ─→ Publisher 또는 직접 전송
    │
    ▼
Redis Pub/Sub ─→ Subscriber 수신
    │                │
    ▼                ▼
데이터베이스 저장 ─→ SSE 실시간 전송
    │                │
    ▼                ▼
영구 보존      ─→ 클라이언트 수신
    │                │
    ▼                ▼
읽음 상태 관리 ─→ UI 업데이트

에러 처리 및 복구

연결 관리

  • 타임아웃: 60분 후 자동 연결 해제
  • 완료/에러: 자동 정리 및 메모리에서 제거
  • 재연결: Last-Event-ID 기반 이벤트 복구

실패 처리

  • 전송 실패: IOException 시 연결 자동 제거
  • 오프라인: Redis 임시 저장 후 재연결 시 전송
  • 메시지 손실: 이벤트 캐시 활용한 복구

설정 및 구성

Redis 구독 설정

@Configuration
@ConditionalOnProperty(name = "spring.cache.type", havingValue = "redis")
public class NotificationConfig implements ApplicationListener<ApplicationReadyEvent> {
    
    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        // 애플리케이션 완전 준비 후 구독 설정
        redisMessageListenerContainer.addMessageListener(
            notificationSubscriber,
            new PatternTopic("notification:*")
        );
    }
}

Redis Template 설정

  • customStringRedisTemplate: 일반 문자열 저장용
  • notifyRedisTemplate: Notify 객체 리스트 저장용

API 명세

SSE 구독

Method Endpoint 설명 권한
GET /notify/subscribe 실시간 알림 구독 USER+ / TRAINER+

요청 헤더

Authorization: Bearer {token}
Last-Event-ID: {lastEventId}  // 선택사항, 연결 복구용

응답 형식

// SSE 스트림
data: {
    "id": "123",
    "name": "사용자명",
    "content": "알림 내용",
    "type": "REVIEW",
    "createdAt": "2024-06-01T10:30:00"
}

주요 특징 요약

실시간성

  • SSE 기반 즉시 알림 전송
  • 다중 디바이스 동시 지원

안정성

  • Redis를 통한 분산 환경 지원
  • 연결 끊김 시 자동 복구
  • 오프라인 알림 보장

확장성

  • 새로운 알림 타입 쉽게 추가
  • 수신자 타입별 독립적 처리
  • Pub/Sub 패턴으로 느슨한 결합

지속성

  • 데이터베이스 영구 저장
  • 알림 히스토리 관리
  • 읽음 상태 추적
⚠️ **GitHub.com Fallback** ⚠️