본문 바로가기
카테고리 없음

24.10.17 SSE(Server-Sent Events) 알림보내고 받기

by 융기융 2024. 10. 17.
반응형
/** ***** SSE(Server-Sent Events) *****
 * 
 *  서버(요청) -> 클라이언트 (응답)
 * 
 * - 서버가 클라이언트에 실시간으로 데이터를 전송할 수 있는 기술
 * 
 * - HTTP 프로토콜 기반으로 동작
 * 
 * - 단방향 통신 (ex : 무전기)
 * 
 * 1) 클라이언트가 서버에 연결 요청
 *   -> 클라이언트가 서버로부터 데이터 받기위한
 *      대기상태에 돌입
 *     (EventSource 객체이용)
 * 
 * 2) 서버가 연결된 클라이언트에게 데이터를 전달
 *   (서버 -> 클라이언트 데이터 전달하라는
 *    요청을 또 AJAX를 이용해 비동기 요청)
 */

/* SSE 연결하는 함수 
  -> 연결을 요청한 클라이언트가
     서버로부터 데이터가 전달될 때까지 대기상태
    (비동기)
  ex)음식점가서 음식나오길 기다리면서 다른행동 할 수 있는 것과 같다.
*/

 

* connectSse -> SSE연결하는 함수

  서버로부터 데이터 전달 대기받는상태

const connectSse = () => {

  /** 로그인이 되어있지 않은 경우 함수종료 */
  if(notificationLoginCheck === false) return;

  console.log("connectSse() 호출");

  // 서버의 "/sse/connect" 주소로 연결요청
  const eventSource = new EventSource("/sse/connect");

  // ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ

  /** 서버로부터 메시지가 왔을경우(전달받은 경우)*/
  eventSource.addEventListener("message", e => {
    console.log(e.data); // e.data == 전달받은 메시지
                         // -> Spring HttpMessageConverter가
                         //    JSON으로 변환해서 응답해줌

    const obj = JSON.parse(e.data);
    console.log(obj);
  })

 

 

/** 알림메시지 전송 함수
 *  - 알림을 받을 특정 클라이언트의 id 필요
 *   (memberNo 또는 memberNo를 알아낼 수 있는 값)
 * 
 * [동작 원리]
 * 1) AJAX를 이용해 SseController에 요청
 * 
 * 2) 연결된 클라이언트 대기명단 (emmiters)에서
 *    클라이언트 id가 일치하는 회원을 찾아
 *    메시지 전달하는 send() 메서드를 수행
 * 
 * 3) 서버로부터 메시지를 전달받은 클라이언트의
 *    eventSource.addEventListener()가 수행됨
 */

 

const sendNotification = (type, url, pkNo, content) => {

  // type : 댓글, 답글, 게시글 좋아요 등을 구분하는 값
  // url  : 알림 클릭 시 이동할 페이지 주소
  // pkNo : 알림 받는 회원번호 또는
  //        회원번호를 찾을 수 있는 pk값
  // content : 알림 내용


  /** 로그인이 되어있지 않은 경우 함수종료 */
  if(notificationLoginCheck === false) return;

  /** 서버로 제출할 데이터를 JS객체 형태로 저장 */
const notification = {
  "notificationType"    : type,
  "notificationUrl"     : url,
  "pkNo"                : pkNo,
  "notificationContent" : content
}

 

*비동기 함수

fetch("/sse/send",{
    method : "POST",
    headers : {"Content-Type" : "application/json"},
    body : JSON.stringify(notification)
  })
  .then(response => {
    if(!response.ok) { // 비동기 통신 실패한 경우
      throw new Error("알림전송 실패");
    }
    console.log("알림전송 성공");
  })
  .catch(err => console.error(err));

}

 

SseController

@Slf4j

@RestController // @Controller + @ResponseBody 비동기통신

public class SseController {

 

@Autowired

private SseService service;

 

// SseEmitter : 서버로부터 메시지를 전달받을

// 클라이언트 정보를 저장한 객체 == 연결된 클라이언트

 

// ConcurrentHashMap : 멀티스레드 환경에서 동기화를 보장하는 Map

// -> 한번에 많은 요청이 있어도 차례대로 처리

private final Map<String, SseEmitter> emitters

= new ConcurrentHashMap<>();

 

/* 클라이언트 연결 요청처리 */

@GetMapping("sse/connect")

public SseEmitter sseConnect(

@SessionAttribute("loginMember") Member loginMember) {

 

// Map에 저장될 Key 값으로 회원번호 얻어오기

String clientId = loginMember.getMemberNo() + "";

 

// SseEmitter 객체 생성

// -> 연결 대기시간 10분 설정(ms 단위)

SseEmitter emitter = new SseEmitter(10 * 60 * 1000L);

 

// 클라이언트 정보를 Map에 추가

emitters.put(clientId, emitter);

 

// 클라이언트 연결 종료 시 Map에서 제거

emitter.onCompletion(() -> emitters.remove(clientId));

 

// 클라이언트 타임아웃 시 Map 에서 제거

emitter.onTimeout(() -> emitters.remove(clientId));

 

return emitter;

 

}

 

/** 알림메시지 전송 */

@PostMapping("/sse/send")

public void sendNotification(

@RequestBody Notification notification,

@SessionAttribute("loginMember")Member loginMember){

 

// 알림 보낸회원(현재 로그인한 회원) 번호추가

notification.setSendMemberNo(loginMember.getMemberNo());

 

// 전달받은 알림데이터를 DB에 저장하고

// 알림 받을 회원의 번호

// + 해당 회원이 읽지 않은 알림 개수 반환 받는 서비스 호출

Map<String, Object> map

= service.insertNotification(notification);

 

 

 

// 알림을 받을 클라이언트 id(xml 까지 다쓰고 수정했음 10.17 12:39)

String clientId = map.get("receiveMemberNo").toString();

 

// 연결된 클리어인트 대기 명단(emitters) 에서

// clinet 가 일치하는 클라이언트 찾기

SseEmitter emitter = emitters.get(clientId);

 

// clientId가 일치하는 클라이언트가 있을 경우

if(emitter != null) {

try{

emitter.send(map);

}catch(Exception e) {

emitters.remove(clientId);

}

 

SseServiceImpl

@Service

@RequiredArgsConstructor

@Transactional

public class SseServiceImpl implements SseService{

 

private final SseMapper mapper;

 

// 알림 삽입 후 알림받을 회원번호 + 알림개수반환

@Override

public Map<String, Object> insertNotification(Notification notification) {

 

// 매개변수 notification에 저장된 값

// -> type, url, content, pkNo, sendMemberNo

 

// 결과 저장용 map

Map<String, Object> map = null;

 

// 알림 삽입

int result = mapper.insertNotification(notification);

 

if(result > 0) { // 알림 삽입 성공시

// 알림을 받아야하는 회원의 번호 + 안읽은 알람 개수 조회

map = mapper.selectReceiveMember(notification.getNotificationNo());

}

 

 

return map;

}

 

 

Sse-mapper.xml

<!--

useGeneratedKeys="true"

- SQL에서 생성된 key 값 (시퀀스) 을 자바에서도

사용할 수 있게 하는 속성

 

- 원리 : 전달받은 파라미터에(얕은 복사로 인해 주소만 복사됨)

생성된 key값을 세팅해서 java에서도 사용 가능하게함

-->

 

<insert id="insertNotification"

parameterType="Notification"

useGeneratedKeys="true">

 

<selectKey order="BEFORE" resultType="_int"

keyProperty="notificationNo">

 

SELECT SEQ_NOTI_NO.NEXTVAL FROM DUAL

</selectKey>

 

INSERT INTO "NOTIFICATION"(

NOTIFICATION_NO,

NOTIFICATION_CONTENT,

NOTIFICATION_URL,

SEND_MEMBER_NO,

RECEIVE_MEMBER_NO)

VALUES(

#{notificationNo},

#{notificationContent},

#{notificationUrl},

#{sendMemberNo},

 

<choose>

<!-- 알림 종류가 댓글 작성 또는 좋아요 -->

<when test="notificationType == 'insertComment'

or notificationType == 'boardLike'">

(SELECT MEMBER_NO

FROM "BOARD"

WHERE BOARD_NO = #{pkNo})

</when>

 

<!-- 알림 종류가 답글인 경우 -->

<when test="notificationType == 'insertChildComment'">

(SELECT MEMBER_NO

FROM "COMMENT"

WHERE COMMENT_NO = #{pkNo})

</when>

 

</choose>

 

)

 

</insert>

 

<!-- 알림을 받아야하는 회원의 번호 + 안읽은 알람 개수조회 -->

<select id="selectReceiveMember" resultType="map">

SELECT

RECEIVE_MEMBER_NO "receiveMemberNo",

(SELECT COUNT(*)

FROM "NOTIFICATION" SUB

WHERE SUB.RECEIVE_MEMBER_NO = MAIN.RECEIVE_MEMBER_NO

AND SUB.NOTIFICATION_CHECK = 'N')

"notiCount"

FROM (

SELECT RECEIVE_MEMBER_NO

FROM "NOTIFICATION"

WHERE NOTIFICATION_NO = #{notificationNo}

) MAIN

 

</select>

 

 

 

 

반응형