/** ***** SSE(Server-Sent Events) *****
*
* 서버(요청) -> 클라이언트 (응답)
*
* - 서버가 클라이언트에 실시간으로 데이터를 전송할 수 있는 기술
*
* - HTTP 프로토콜 기반으로 동작
*
* - 단방향 통신 (ex : 무전기)
*
* 1) 클라이언트가 서버에 연결 요청
* -> 클라이언트가 서버로부터 데이터 받기위한
* 대기상태에 돌입
* (EventSource 객체이용)
*
* 2) 서버가 연결된 클라이언트에게 데이터를 전달
* (서버 -> 클라이언트 데이터 전달하라는
* 요청을 또 AJAX를 이용해 비동기 요청)
*/
/* SSE 연결하는 함수
-> 연결을 요청한 클라이언트가
서버로부터 데이터가 전달될 때까지 대기상태
(비동기)
ex)음식점가서 음식나오길 기다리면서 다른행동 할 수 있는 것과 같다.
*/
* connectSse -> SSE연결하는 함수
서버로부터 데이터 전달 대기받는상태
/** 알림메시지 전송 함수
* - 알림을 받을 특정 클라이언트의 id 필요
* (memberNo 또는 memberNo를 알아낼 수 있는 값)
*
* [동작 원리]
* 1) AJAX를 이용해 SseController에 요청
*
* 2) 연결된 클라이언트 대기명단 (emmiters)에서
* 클라이언트 id가 일치하는 회원을 찾아
* 메시지 전달하는 send() 메서드를 수행
*
* 3) 서버로부터 메시지를 전달받은 클라이언트의
* eventSource.addEventListener()가 수행됨
*/
*비동기 함수
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>