[프로젝트] 주식 체결 알림 기능 추가 - 2

2025. 1. 23. 17:47·project

SSE 구독 상태란?

클라이언트가 서버에 요청을 보내고, 서버는 클라이언트와 지속적인 연결을 유지하는데 이 상태를 구독 상태라고 한다. 

클라이언트는 HTTP 요청을 보내고, 서버는 그 요청을 통해 실시간으로 데이터를 푸시한다.

 

작동 방식

1. 클라이언트가 SSE 엔드포인트에 요청을 보낸다.

2. 서버는 요청에 대한 응답을 지속적으로 유지하면서 데이터를 스트리밍 한다.

3. 데이터는 HTTP 의 Content-Type: text/event - stream 포맷으로 전송된다.

4. 클라이언트는 연결이 유지된 상태에서 서버가 전송하는 데이터를 실시간으로 수신한다. 

 

서버가 데이터를 푸시하기 위해서는 클라이언트와의 연결이 끊어지지 않아야 한다. 

 

 

@Service
@RequiredArgsConstructor
public class NotificationService {

    private final Map<Long, SseEmitter> emitters = new ConcurrentHashMap<>();

    public SseEmitter subscribe(Long memberId) {

        SseEmitter sseEmitter = new SseEmitter(Long.MAX_VALUE);
        emitters.put(memberId, sseEmitter); //로그인 된 사용자 아이디 emitter 객체 등록

        sseEmitter.onCompletion(() -> emitters.remove(memberId)); 
        sseEmitter.onTimeout(() -> emitters.remove(memberId));
        sseEmitter.onError((e) -> emitters.remove(memberId));

        try { 
            sseEmitter.send(SseEmitter.event().name("connect")
                    .data("연결 성공"));  //연결 성공 할 경우 메세지
        } catch (IOException e) {
            e.printStackTrace();
        }
        return sseEmitter;
    }
}

 

사용자의 memberId를 키로 하여 SseEmitter 객체를 emitters Map에 저장한다.

연결 완료, 시간 초과 에러 발생 시 구독을 삭제한다.

 

 

package com.example.investment_api.virtual.alarm.domain;

import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Notification {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "notification_id")
    private Long id;
    private Long memberId;
    private String message;
    private LocalDateTime createdAt;
    private String url;

    @Builder
    public Notification(Long memberId, String message, String url, LocalDateTime createdAt) {
        this.memberId = memberId;
        this.message = message;
        this.url = url;
        this.createdAt = LocalDateTime.now();
    }
}

알람 객체 생성

 

 

public NotificationDTO toDTO(Notification notification) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss");
        return NotificationDTO.builder()
                .id(notification.getId())
                .message(notification.getMessage())
                .createdAT(notification.getCreatedAt().format(formatter))
                .url(notification.getUrl())
                .build();
    }

LocalDateTime 타입의 데이터를 JSON 으로 직렬화 하면서 문제가 생김

String 값으로 변환해주었다.

 

 

@RestController
@RequiredArgsConstructor
public class NotificationController {

    private final NotificationService notificationService;

    @GetMapping("/notification/subscribe")
    public SseEmitter subscribe(@Member Long memberId) {
        return notificationService.subscribe(memberId);
    }

SSE 를 통한 알림 기능을 받을 사용자들을 등록

 

 

연결 !

 

 

 

notification service 작성

 public void sendOrderNotification(Long memberId, NotificationType type, String stockName, int quantity) {
        String message = generateNotificationMessage(type, stockName, quantity);
        String url = "https://growfolio-nu.vercel.app/login";

        Notification notification = createNotification(memberId, message, url);
        notificationRepository.save(notification);

        SseEmitter emitter = emitters.get(memberId);
        if (emitter != null) {
            NotificationDTO notificationDTO = notificationMapper.toDTO(notification);
            sendToClient(emitter, memberId.toString(), notificationDTO);
        }
    }

    private Notification createNotification(Long memberId, String message, String url) {
        return Notification.builder()
                .memberId(memberId)
                .message(message)
                .url(url)
                .createdAt(LocalDateTime.now())
                .build();
    }

    private void sendToClient(SseEmitter emitter, String id, Object data) {
        try {
            emitter.send(SseEmitter.event()
                    .id(id)
                    .name("order-notification")
                    .data(data));
        } catch (IOException exception) {
            emitters.remove(Long.valueOf(id));
            throw new RuntimeException("알림 전송 실패!", exception);
        }
    }


    private String generateNotificationMessage(NotificationType type, String stockName, int quantity) {
        switch (type) {
            case BUY_SUCCESS:
                return "매수 성공: " + stockName + " " + quantity + "주가 체결되었습니다.";
            case SELL_SUCCESS:
                return "매도 성공: " + stockName + " " + quantity + "주가 체결되었습니다.";
            default:
                return "알림: 주문 상태가 업데이트되었습니다.";
        }
    }

주문 유형 (BUY_SUCCESS, SELL_SUCCESS)에 따라 알림 메세지를 생성 후

createNotification 메서드를 통해 알림 객체를 생성 한 후, 해당 알림을 DB에 저장한 뒤 실시간 전송을 시도한다.

sendToClient 메서드는 SseEmitter 객체를 통해 클라이언트로 데이터를 전송한다.

 

 

 

 

 

 

확인을 위해 특정 회원의 알람목록을 최신순으로 반환하도록 하였다.

 

 

 

 

https://velog.io/@max9106/Spring-SSE-Server-Sent-Events%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%95%8C%EB%A6%BC

 

[Spring + SSE] Server-Sent Events를 이용한 실시간 알림

코드리뷰 매칭 플랫폼 개발 중 알림 기능이 필요했다. 리뷰어 입장에서는 새로운 리뷰 요청이 생겼을 때 모든 리뷰가 끝나고 리뷰이의 피드백이 도착했을 때 리뷰이 입장에서는 리뷰 요청이 거

velog.io

 

'project' 카테고리의 다른 글

[프로젝트] 댓글 조회시 N+1 문제 해결, 성능 비교  (0) 2025.11.26
[프로젝트] 주식 체결 알람 기능 추가 - 1  (2) 2025.01.15
'project' 카테고리의 다른 글
  • [프로젝트] 댓글 조회시 N+1 문제 해결, 성능 비교
  • [프로젝트] 주식 체결 알람 기능 추가 - 1
zioni
zioni
  • zioni
    jiwon's dev.log
    zioni
  • 전체
    오늘
    어제
    • 분류 전체보기 (76)
      • spring & java (13)
      • Algorithm (14)
      • PS (37)
      • project (3)
      • experience (1)
      • etc (6)
      • study (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • Github
  • 공지사항

  • 인기 글

  • 태그

    java
    백준
    자바
    백준2525
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
zioni
[프로젝트] 주식 체결 알림 기능 추가 - 2
상단으로

티스토리툴바