๐Ÿ”ฅ TIL - Day 64 SSE๋ฅผ ์ด์šฉํ•œ ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ

Kim Dae Hyunยท2021๋…„ 11์›” 24์ผ
5

TIL

๋ชฉ๋ก ๋ณด๊ธฐ
75/93

์ „์ฒด์ฝ”๋“œ Github

๐Ÿ“Œ SSE (Server-Sent Event)

Websocket๊ณผ ๊ฐ™์ด ์„œ๋ฒ„์—์„œ ํด๋ผ์ด์–ธํŠธ๋กœ ๋‹จ๋ฐฉํ–ฅ ํ†ต์‹ ์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜๋Š” ์†”๋ฃจ์…˜ ์ค‘ ํ•˜๋‚˜์ด๋‹ค.
๊ฐ€์žฅ ํฐ ์ฐจ์ด์ ์œผ๋กœ๋Š” WebSocket์˜ ๊ฒฝ์šฐ ์™„์ „ํžˆ ์–‘๋ฐฉํ–ฅ ํ†ต์‹ ์„ ์ง€์›ํ•œ๋‹ค. ํด๋ผ์ด์–ธํŠธ์—์„œ ์„œ๋ฒ„๋กœ, ์„œ๋ฒ„์—์„œ ํด๋ผ์ด์–ธํŠธ๋กœ ํ†ต์‹ ์ด ๊ฐ€๋Šฅํ•˜๋‹ค. ๋ฐ˜๋ฉด SSE์˜ ๊ฒฝ์šฐ ์„œ๋ฒ„์—์„œ ํด๋ผ์ด์–ธํŠธ๋กœ ๋ณด๋‚ด๋Š” ๊ฒƒ๋งŒ ๊ฐ€๋Šฅํ•˜๋‹ค.

์ถ”๊ฐ€๋กœ Websocket๋ณด๋‹ค๋Š” ๊ฐ€๋ณ๊ณ  Springboot ๊ธฐ์ค€ ๊ตฌํ˜„์ด ์‰ฝ๋‹ค๋Š” ์žฅ์ ์„ ๊ฐ–๊ณ ์žˆ๋‹ค.

์ด๋ฒˆ ํ”„๋กœ์ ํŠธ์— ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ๊ธฐ๋Šฅ์„ ์ ์šฉํ•ด๋ณด๊ธฐ ์œ„ํ•ด ๊ฐ€๋ณ๊ฒŒ ๋‹ค๋ค„๋ดค๋‹ค.


๐Ÿ“Œ ์„ค์ •

Springboot ์˜ ๊ฒฝ์šฐ ๋”ฐ๋กœ ์˜์กด์„ฑ ์ถ”๊ฐ€๋Š” ํ•„์š”์—†๋‹ค.

์ผ๋‹จ ๊ตฌํ˜„์— ์•ž์„œ์„œ ์š”๊ตฌ์‚ฌํ•ญ์„ ๋ง๋กœ ํ’€์–ด๋ดค๋‹ค. ( ๋ง๋กœ ํ•˜๋‹ˆ๊นŒ ์ข€ ์ด์ƒํ•˜๊ธดํ•˜๋‹ค..)

  • ๋กœ๊ทธ์ธ ๋œ ์‚ฌ์šฉ์ž๋งŒ ์ด๋ฒคํŠธ๋ฅผ ๋ฐ›์œผ๋ฉด ๋˜๋ฏ€๋กœ ๋กœ๊ทธ์ธ ๋œ ์‚ฌ์šฉ์ž๋งŒ ์„œ๋ฒ„์™€ ์—ฐ๊ฒฐ๋˜๋„๋ก ํ•œ๋‹ค.
    (์—ฐ๊ฒฐ == ๊ตฌ๋…)
  • ๋กœ๊ทธ์ธ ๋œ ์‚ฌ์šฉ์ž๋Š” pk, username, ๊ถŒํ•œ ๋“ฑ์ด ํฌํ•จ๋œ ํ† ํฐ์„ sessionStorage์— ๊ฐ–๋Š”๋‹ค. (์‹ค์ œ ๋ฐ์ดํ„ฐ์™€ ๊ด€๊ณ„์—†๋Š” pk์ •๋„๋Š” ํ† ํฐ์—์„œ ๊ด€๋ฆฌํ•ด๋„ ๋˜์ง€ ์•Š์„๊นŒ ?)
  • ํŽ˜์ด์ง€๊ฐ€ ๋กœ๋“œ๋˜์—ˆ์„ ๋•Œ sessionStorage์— ํ† ํฐ์ด ์žˆ๋‹ค๋ฉด ํ† ํฐ์„ ์ฟผ๋ฆฌ์ŠคํŠธ๋ง์œผ๋กœ ์ „๋‹ฌํ•˜๋ฉด์„œ ์„œ๋ฒ„์™€ ์—ฐ๊ฒฐํ•œ๋‹ค.
  • ์–ด๋–ค ๊ฒŒ์‹œ๊ธ€์— ๋Œ“๊ธ€์ด ๋‹ฌ๋ ธ์„ ๋•Œ ์„œ๋ฒ„๋Š” ํ•ด๋‹น ๊ฒŒ์‹œ๊ธ€์˜ pk๋กœ ํ•ด๋‹น ๊ฒŒ์‹œ๊ธ€์˜ ์ฃผ์ธ(user)๋ฅผ ์ฐพ๊ณ  ์ฃผ์ธ(user)์˜ pk์™€ ๋งคํ•‘๋œ emitter๋กœ ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆผ์„ ๋ณด๋‚ธ๋‹ค.

ํด๋ผ์ด์–ธํŠธ ์ธก ๋ณด๋‹ค๋Š” ์„œ๋ฒ„ ์ธก ์ฒ˜๋ฆฌ๊ฐ€ ์ชผ๋” ๋ณต์žกํ•˜๋‹ˆ ํด๋ผ์ด์–ธํŠธ ์ธก ์ฒ˜๋ฆฌ๋ถ€ํ„ฐ ๋ณด์ž.
ํด๋ผ์ด์–ธํŠธ๋Š” ํ† ํฐ์„ ๋ณด์œ ํ•˜๊ณ  ์žˆ์„ ๋•Œ๋งŒ ์„œ๋ฒ„๋ฅผ ๊ตฌ๋…ํ•˜๋ฉด ๋œ๋‹ค.

let subscribeUrl = "http://localhost:8080/sub";

$(document).ready(function() {

    if (sessionStorage.getItem("mytoken") != null) {
        let token = sessionStorage.getItem("mytoken");
        let eventSource = new EventSource(subscribeUrl + "?token=" + token);

        eventSource.addEventListener("addComment", function(event) {
            let message = event.data;
            alert(message);
        })

        eventSource.addEventListener("error", function(event) {
            eventSource.close()
        })
    }
})

์•„์ง ์†Œ๊ฐœํ•˜์ง„ ์•Š์•˜์ง€๋งŒ ์„œ๋ฒ„์ธก์—์„œ ๊ตฌ๋…(์—ฐ๊ฒฐ)์„ ์œ„ํ•ด ์—ด์–ด๋†“์€ ์—”๋“œํฌ์ธํŠธ๋Š” /sub์ด๋‹ค.
์ด ์—”๋“œํฌ์ธํŠธ์— ์ฟผ๋ฆฌ์ŠคํŠธ๋ง์œผ๋กœ ํ† ํฐ๊ฐ’๊ณผ ํ•จ๊ป˜ ์š”์ฒญํ•˜๊ณ  ์žˆ๋‹ค.

์—ฐ๊ฒฐ์„ ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋์ด๋‹ค. (์˜ ๊ฐ„๋‹จ)

๋‹ค์Œ์œผ๋กœ ์„œ๋ฒ„์—์„œ ๋ฐœํ—น๋œ ์ด๋ฒคํŠธ์— ๋Œ€ํ•œ ์ฒ˜๋ฆฌ๋ฅผ ํ•œ๋‹ค.
addComment๋ผ๋Š” ์ด๋ฆ„์œผ๋กœ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค๋ฉด ํ•จ๊ป˜ ์ „๋‹ฌ๋œ data๋ฅผ alertํ•˜๋Š” ๊ฐ„๋‹จํ•œ ์ฒ˜๋ฆฌ๋ฅผ ํ•˜๊ณ  ์žˆ๋‹ค.


์ด์ œ ์„œ๋ฒ„์ธก ์ฒ˜๋ฆฌ์ด๋‹ค.

@RequiredArgsConstructor
@Slf4j
@RestController
public class SseController {

    public static Map<Long, SseEmitter> sseEmitters = new ConcurrentHashMap<>();
    private final JwtUtils jwtUtils;

    @CrossOrigin
    @GetMapping(value = "/sub", consumes = MediaType.ALL_VALUE)
    public SseEmitter subscribe(@RequestParam String token) {
			
        // ํ† ํฐ์—์„œ user์˜ pk๊ฐ’ ํŒŒ์‹ฑ
        Long userId = jwtUtils.getUserIdFromToken(token);
		
        // ํ˜„์žฌ ํด๋ผ์ด์–ธํŠธ๋ฅผ ์œ„ํ•œ SseEmitter ์ƒ์„ฑ
        SseEmitter sseEmitter = new SseEmitter(Long.MAX_VALUE);
        try {
            // ์—ฐ๊ฒฐ!!
            sseEmitter.send(SseEmitter.event().name("connect"));
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        // user์˜ pk๊ฐ’์„ key๊ฐ’์œผ๋กœ ํ•ด์„œ SseEmitter๋ฅผ ์ €์žฅ
        sseEmitters.put(userId, sseEmitter);

        sseEmitter.onCompletion(() -> sseEmitters.remove(userId));
        sseEmitter.onTimeout(() -> sseEmitters.remove(userId));
        sseEmitter.onError((e) -> sseEmitters.remove(userId));

        return sseEmitter;
    }
}

๊ฐ€์žฅ ์ค‘์š”ํ•œ ๋ถ€๋ถ„์€ ์ „๋‹ฌ๋ฐ›์€ ํ† ํฐ์—์„œ user์˜ pk๊ฐ’์„ ํŒŒ์‹ฑํ•˜๊ณ  ํŒŒ์‹ฑ๋œ pk๊ฐ’์„ ํ‚ค ๊ฐ’์œผ๋กœ ํ•˜์—ฌ SseEmitter๋ฅผ ์ €์žฅํ•˜๋Š” ๋ถ€๋ถ„์ด๋‹ค.

์ด๋กœ์จ ์‚ฌ์šฉ์ž๋ณ„๋กœ SseEmitter๋ฅผ ์‹๋ณ„ํ•˜์—ฌ ์ด๋ฒคํŠธ๋ฅผ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ๋‹ค.


๋งˆ์ง€๋ง‰์œผ๋กœ ์•Œ๋ฆผ(์ด๋ฒคํŠธ)์„ ๋ณด๋‚ด๊ธฐ ์œ„ํ•œ ์ฒ˜๋ฆฌ๊ฐ€ ๋‚จ์•˜๋‹ค.

@RequiredArgsConstructor
@Service
public class NotificationService {

    private final MemoRepository memoRepository;

    public void notifyAddCommentEvent(Long memoId) {
        // ๋Œ“๊ธ€์— ๋Œ€ํ•œ ์ฒ˜๋ฆฌ ํ›„ ํ•ด๋‹น ๋Œ“๊ธ€์ด ๋‹ฌ๋ฆฐ ๊ฒŒ์‹œ๊ธ€์˜ pk๊ฐ’์œผ๋กœ ๊ฒŒ์‹œ๊ธ€์„ ์กฐํšŒ
        Memo memo = memoRepository.findById(memoId).orElseThrow(
                () -> new IllegalArgumentException("์ฐพ์„ ์ˆ˜ ์—†๋Š” ๋ฉ”๋ชจ์ž…๋‹ˆ๋‹ค.")
        );
        Long userId = memo.getUser().getId();

        if (sseEmitters.containsKey(userId)) {
            SseEmitter sseEmitter = sseEmitters.get(userId);
            try {
                sseEmitter.send(SseEmitter.event().name("addComment").data("๋Œ“๊ธ€์ด ๋‹ฌ๋ ธ์Šต๋‹ˆ๋‹ค!!!!!"));
            } catch (Exception e) {
                sseEmitters.remove(userId);
            }
        }
    }
}

์œ„ ๋ฉ”์„œ๋“œ๋Š” ๋Œ“๊ธ€์— ๋Œ€ํ•œ ์ฒ˜๋ฆฌ๋ฅผ ๋งˆ์นœ ํ›„์— ํ˜ธ์ถœ๋œ๋‹ค.
ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฐ›์€ ๊ฒŒ์‹œ๊ธ€์˜ ์ฃผ์ธ(user)์˜ pk๋ฅผ ์กฐํšŒํ•œ๋‹ค.

์šฐ๋ฆฌ๋Š” user์˜ pk์™€ ์—ฐ๊ฒฐ๋œ SseEmitter ์ €์žฅ์†Œ๋ฅผ ๊ฐ–๊ณ  ์žˆ๋‹ค.
SseEmitter ์ €์žฅ์†Œ์— ํ˜„์žฌ user์˜ pk๊ฐ€ ์กด์žฌํ•œ๋‹ค๋ฉด (๊ฒŒ์‹œ๊ธ€์˜ ์ฃผ์ธ์ด ํ˜„์žฌ ์„œ๋ฒ„์™€ ์—ฐ๊ฒฐ๋˜์–ด ์žˆ๋Š” ์ƒํƒœ๋ผ๋ฉด) ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•˜๋ฉด ๋œ๋‹ค.


์•Œ๋ฆผ์„ ์œ„ํ•œ ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๋ถ€๋ถ„

    @PostMapping("/memo/{id}/comment")
    public ResponseEntity addComment(
            @AuthenticationPrincipal UserDetailsImpl userDetails,
            @PathVariable Long id,
            @RequestBody CommentDto commentDto) {
        Memo memo = memoService.addComment(id, userDetails.getUser(), commentDto);

	// ์•Œ๋ฆผ ์ด๋ฒคํŠธ ๋ฐœํ–‰ ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ
        notificationService.notifyAddCommentEvent(memo);
        
        return ResponseEntity.ok("ok");
    }

๊ตฌํ˜„ ๋~~



๐Ÿ“Œ ํ…Œ์ŠคํŠธ




๐Ÿ“Œ ์ฐธ๊ณ 

https://www.youtube.com/watch?v=HoxPgU4lFGE

https://www.youtube.com/watch?v=4HlNv1qpZFY

profile
์ข€ ๋” ์ฒœ์ฒœํžˆ ๊นŒ๋จน๊ธฐ ์œ„ํ•ด ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค. ๐Ÿง

1๊ฐœ์˜ ๋Œ“๊ธ€

comment-user-thumbnail
2022๋…„ 8์›” 8์ผ

์•ˆ๋…•ํ•˜์„ธ์š”. ๊ธ€ ๋ณด๋ฉด์„œ ๊ตฌํ˜„์˜ ์„ฑ๊ณตํ–ˆ์Šต๋‹ˆ๋‹ค.
๊ทธ๋Ÿฐ๋ฐ ์งˆ๋ฌธ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

  1. ํด๋ผ์ด์–ธํŠธ์˜ ๋ชจ๋“  ๊ฒฝ๋กœ์—์„œ ์•Œ๋žŒ์„ ๋ฐ›๊ณ  ์‹ถ์œผ๋ฉด ์–ด๋–ป๊ฒŒ ํ•ด์•ผํ•˜๋‚˜์š”?
    ํ˜„์žฌ ์ฝ”๋“œ๋กœ๋Š” eventSource.addEventListener๋ฅผ ํ•ด์ค€ html์˜ ์œ„์น˜๊ฐ€ ์•„๋‹Œ ๋‹ค๋ฅธ ๊ฒฝ๋กœ์—์„œ๋Š” ์•Œ๋ฆผ์„ ๋ณด๋‚ด๋„ ์•Œ๋ฆผ์ด ๊ฐ€์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์–ด๋–ค์‹์œผ๋กœ ํ•ด์•ผํ• ๊นŒ์š”. ๋ชจ๋“  htmlํŒŒ์ผ์—์„œ /sub๋ฅผ ํ˜ธ์ถœํ•˜๊ณ  ์ด๋ฒคํŠธ๋ฅผ ๊ธฐ๋‹ค๋ ค์•ผ ํ•˜๋‚˜์š”?

  2. Get "/sub" ํ˜ธ์ถœ ์‹œ๊ธฐ
    1๋ฒˆ ์งˆ๋ฌธ๊ณผ ๋น„์Šทํ•œ ์งˆ๋ฌธ์ผ์ˆ˜ ์žˆ๋Š”๋ฐ ํ•ด๋‹น url์š”์ฒญ์„ ํ•ด์•ผ sseEmitters (Map)์— ๋“ฑ๋ก๋˜๋Š”๋ฐ ๊ทธ๋Ÿฌ๋ฉด ๋กœ๊ทธ์ธ์ด ์„ฑ๊ณตํ•œ ์‹œ์ ์— ๋“ฑ๋ก์„ ํ•ด์•ผํ• ๊นŒ์š”?

๋‹ต๊ธ€ ๋‹ฌ๊ธฐ