기술 의사 결정 < Redis를 이용한 Pub/Sub 알림 서비스 >

김규현·2025년 6월 30일
0

개요

Fitpass 웹서비스에서 실시간 알림 시스템 구현을 위해 Redis Pub/Sub 패턴을 도입한 기술적 의사결정에 대한 분석 현재 구현된 시스템들과 다른 대안들을 비교하여 Redis Pub/Sub 선택의 타당성을 검증

알림 시스템 요구사항

핵심 요구 사항

User와 Trainer 구분 : 사용자와 트레이너 각각에 대한 별도 알림 채널 필요

즉시 전달 : SSE(Server-Sent Events)를 통한 즉시 알림 전송

오프라인 처리 : 사용자가 오프라인일 때 알림 저장 및 후속 전송

다중 서버 지원 : 서버 확장 시에도 일관된 알림 전달

알림 타입 관리 다양한 Notification Type에 따른 알림 분류

현재 구현된 알림 타입

public enum NotificationType {
	YATA, REVIEW, CHAT, RESERVATION, MEMBERSHIP
}
  • 예약 생성/수정/취소 알림
  • 예약 승인/거부 알림
  • 예약 리마인더 알림 (스케줄러)
  • 간단한 알림 분류

로 수정 가능성 있음

현재 구현된 Redis Pub/Sub 아키텍쳐 분석

시스템 구조

┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 비즈니스 │ │ NotificationPub │ │ Redis Pub/Sub │
│ 로직 │───▶│ lisher │───▶│ Channel │
│ (Service) │ │ │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘


┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ SSE Emitter │◀───│ NotificationSub │◀───│ Message │
│ (실시간 전송) │ │ scriber │ │ Listener │
└─────────────────┘ └─────────────────┘ └─────────────────┘

구현된 코드 구조

Publisher (알림 발송)


@Service
@RequiredArgsConstructor
public class NotificationPublisher {
    private final RedisTemplate<String, Object> redisTemplate;
    private static final String NOTIFICATION_CHANNEL = "notification:";

    // User 알림: notification:USER:{userId}
    public void publishNotificationToUser(Long userId, NotificationType type, 
                                        String content, String url) {
        NotificationEvent event = new NotificationEvent(
            userId, "USER", type, content, url, LocalDateTime.now()
        );
        String channel = NOTIFICATION_CHANNEL + "USER:" + userId;
        redisTemplate.convertAndSend(channel, event);
    }

    // Trainer 알림: notification:TRAINER:{trainerId}  
    public void publishNotificationToTrainer(Long trainerId, NotificationType type,
                                           String content, String url) {
        NotificationEvent event = new NotificationEvent(
            trainerId, "TRAINER", type, content, url, LocalDateTime.now()
        );
        String channel = NOTIFICATION_CHANNEL + "TRAINER:" + trainerId;
        redisTemplate.convertAndSend(channel, event);
    }
}

Subscriber (알림 수신 및 처리)


@Component
@RequiredArgsConstructor
public class NotificationSubscriber implements MessageListener {
    
    @Override
    public void onMessage(Message message, byte[] pattern) {
        try {
            // Redis 메시지를 NotificationEvent로 역직렬화
            NotificationEvent event = mapper.readValue(message.getBody(), 
                                                     NotificationEvent.class);
            
            // 1. DB에 알림 저장 (영속성)
            Notify notification = createAndSaveNotification(event);
            
            // 2. SSE를 통한 실시간 전송
            sendToSseEmitters(event.getReceiverId(), 
                            event.getReceiverType(), notification);
            
        } catch (Exception e) {
            // 에러 처리
        }
    }
}

채널 구독 설정


@Configuration
public class NotificationConfig implements ApplicationListener<ApplicationReadyEvent> {
    
    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        // 패턴 매칭으로 모든 알림 채널 구독: notification:*
        redisMessageListenerContainer.addMessageListener(notificationSubscriber,
                new PatternTopic("notification:*"));
    }
}

Redis Pub//Sub vs 다른 대안 비교

Redis 없는 기본 구현 vs 현재 Redis 구현

구분기본 구현현재 Redis 구현
다중 서버 지원불가능완벽 지원
메시지 전파서버 내부만모든 서버에 전파
오프라인 처리별도 구현 필요RedisDao로 저장
채널 관리복잡한 구현패턴 매칭 지원
구현 복잡도높음낮음

Apache Kafka와의 비교

Apache Kafka

장점 :

  • 메시지 영속성과 순서 보장
  • 높은 처리량과 확장성
  • 복제를 통한 고가용성

단점 :

  • 현재 프로젝트에는 과도한 복잡성
  • 높은 운영 비용과 관리 오버헤드
  • 실시간 알림에는 상대적으로 높은 지연시간
  • Zookeeper 의존성 (구버전 기준)

현재 Reids 구현 우위 :

// 간단한 알림 발송 - Redis
notificationPublisher.publishNotificationToUser(userId, RESERVATION_COMPLETE, 
                                               "예약이 완료되었습니다", "/reservations");

// vs Kafka의 경우 필요한 설정
// - Producer 설정
// - Topic 관리  
// - Partition 설정
// - Consumer Group 관리
// - Offset 관리

RabbitMQ와의 비교

RabbitMQ

장점 :

  • 메시지 영속성과 전달 보장
  • 다양한 라우팅 패턴
  • 관리 UI 제공

단점 :

  • Redis보다 높은 지연시간
  • 더 복잡한 설정과 관리
  • Fitpass의 단순한 알림 요구사항에는 부합하지 않음

현재 구현의 장점


// 현재 Redis 패턴 매칭 - 매우 간단
new PatternTopic("notification:*")  // 모든 알림 구독

// vs RabbitMQ Exchange/Routing 설정
// - Exchange 타입 선택
// - Binding Key 설정  
// - Queue 관리
// - Dead Letter Queue 설정

현재 Fitpass 구현의 핵심 장점

사용자/트레이너 구분 채널


// 명확한 채널 분리로 권한 관리 용이
"notification:USER:" + userId     // 사용자 전용 채널
"notification:TRAINER:" + trainerId  // 트레이너 전용 채널

기존 인프라 활용


// RedisConfig.java에서 이미 설정된 인프라 활용
- Cache 용도로 이미 Redis 사용 중
- 별도 메시지 브로커 불필요  
- 운영 복잡성 최소화

SSE와의 연동

// NotificationSubscriber에서 Redis → SSE 연결
private void sendToSseEmitters(Long receiverId, String receiverType, Notify notification) {
    Map<String, SseEmitter> emitters = emitterRepository.findAllEmittersById(receiverId);
    
    emitters.forEach((key, emitter) -> {
        emitter.send(SseEmitter.event()
                .id(key)
                .name("notification")  
                .data(NotifyDto.Response.createResponse(notification)));
    });
}

오프라인 사용자 처리


// NotifyService.java - 오프라인 사용자 대응
if (emitters.isEmpty()) {
    redisDao.saveNotifyToRedis(receiverId, notification); // Redis에 저장
    return;
}

실제 사용 사례 기반 분석

예약완료 알림 시나리오


// 예약 서비스에서 알림 발송
@Service
public class ReservationService {
    
    public void completeReservation(Long userId, ReservationData reservation) {
        // 1. 예약 처리 로직
        // ...
        
        // 2. 사용자에게 예약 완료 알림
        notificationPublisher.publishNotificationToUser(
            userId, 
            NotificationType.RESERVATION_COMPLETE,
            "예약이 완료되었습니다: " + reservation.getClassName(),
            "/reservations/" + reservation.getId()
        );
        
        // 3. 트레이너에게 새 예약 알림  
        notificationPublisher.publishNotificationToTrainer(
            reservation.getTrainerId(),
            NotificationType.NEW_RESERVATION,
            "새로운 예약이 접수되었습니다",
            "/trainer/reservations/" + reservation.getId()
        );
    }
}

결제 관련 알림


// 결제 서비스 연동
public void processPayment(PaymentData payment) {
    if (payment.isSuccess()) {
        notificationPublisher.publishNotificationToUser(
            payment.getUserId(),
            NotificationType.PAYMENT_SUCCESS, 
            "결제가 완료되었습니다: " + payment.getAmount() + "원",
            "/payments/" + payment.getId()
        );
    } else {
        notificationPublisher.publishNotificationToUser(
            payment.getUserId(),
            NotificationType.PAYMENT_FAILED,
            "결제에 실패했습니다. 다시 시도해주세요.",
            "/payments/retry"
        );
    }
}

성능과 확장성 분석

현재 구현의 성능 특성

  • 지연시간 : Redis 평균 1ms 미만으로 즉시 알림에 최적

  • 처리량 : Fitpass 규모에서 충분한 성능 (초당 수만 건 처리 가능)

  • 메모리 효율성 : 패턴 매칭으로 효율적인 구독 관리

확장성 고려사항


// 현재 구조의 확장 가능성
- Redis Cluster로 수평 확장 가능
- 서버 인스턴스 추가 시 자동으로 알림 분산
- 채널 패턴으로 새로운 알림 타입 쉽게 추가

운영상의 이점

모니터링과 디버깅


// Redis CLI로 실시간 모니터링 가능
redis-cli monitor  // 모든 명령어 실시간 확인
redis-cli psubscribe "notification:*"  // 알림 메시지 실시간 확인

개발 및 테스트 용이성


// 테스트 환경에서 쉬운 알림 확인
@Test
public void testNotification() {
    // Given
    Long userId = 1L;
    
    // When  
    notificationPublisher.publishNotificationToUser(userId, TEST_TYPE, "test", "/test");
    
    // Then
    // Redis 메시지 직접 확인 가능
}

제한사항과 보완 방안

메시지 영속성

현재 상화 :

  • Redis Pub/Sub는 fire-and-forget 방식
  • 구독자가 없으면 메시지 손실 가능

구현된 보완책 :

/ NotifyService.java에서 DB 영속성 보장
Notify notification = notifyRepository.save(createNotification(...));

// 오프라인 사용자를 위한 Redis 저장
redisDao.saveNotifyToRedis(receiverId, notification);

중요 알림의 전달 보장

// 향후 개선 방안 예시
public void publishCriticalNotification(Long userId, NotificationType type, 
                                       String content, String url) {
    // 1. 일반 Pub/Sub 발송
    publishNotificationToUser(userId, type, content, url);
    
    // 2. 중요 알림은 추가로 백업 저장
    redisDao.saveCriticalNotification(userId, type, content, url);
    
    // 3. 필요시 외부 알림 (SMS, 이메일) 연동
}

결론

Redis Pub/Sub 선택의 타당성

현재 우리 프로젝트에서 Redis Pub/Sub이 최적인 이유 :

  • 기존 인프라 활용 : 이미 캐시용으로 사용 중인 Redis 인프라 활용

  • 개발 생산성 : 간단한 API로 빠른 개발과 안정적인 운영

  • 완벽한 통합 : SSE, DB 저장, 오프라인 처리가 하나의 시스템으로 통합

  • 적정 기술 : Fitpass 규모와 요구사항에 적합한 기술 선택

현재 구현의 장점

  • Publisher/Subscriber 분리로 책임 명확
  • 패턴 매칭으로 유연한 채널 관리
  • SSE 연동으로 완벽한 실시간 처리
  • DB 영속성과 Redis 캐싱의 조화

0개의 댓글