Spring๐Ÿƒ

Spring messging, Stomp๋ฅผ ํ™œ์šฉํ•œ ๋ฉ”์„ธ์ง€ ๊ด€๋ฆฌ + ๋‹ค์ค‘ ์„ธ์…˜ ๊ด€๋ฆฌ ๊ตฌํ˜„

wannaDevelopIt 2025. 4. 20. 00:44
728x90

0. ํ•ด๊ฒฐํ•ด์•ผ ํ•˜๋Š”(๊ตฌํ˜„ํ•ด์•ผ ํ•˜๋Š”) ๋ฌธ์ œ(์š”๊ตฌ ์‚ฌํ•ญ)

- ์‚ฌ์ดํŠธ์— ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ ‘์†ํ•œ ์‚ฌ๋žŒ์˜ ๋ชฉ๋ก๊ณผ ํ•ด๋‹น ์‚ฌ์šฉ์ž๋“ค์ด ํ˜„์žฌ ์–ด๋–ค ํŽ˜์ด์ง€, ํƒญ์— ์ ‘์†ํ•ด์žˆ๋Š”์ง€๋ฅผ ํ™•์ธํ•ด๋ณด๊ณ ์ž ํ–ˆ๋‹ค

 

- http, rest API๋ฅผ ํ†ตํ•œ ๊ตฌํ˜„๋„ ๊ฐ€๋Šฅํ•˜๊ฒ ์ง€๋งŒ, ์ด๋Š” ์ง„์ •ํ•œ "์‹ค์‹œ๊ฐ„"์„ ์˜๋ฏธํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ฉฐ, ์กฐ๊ธˆ์ด๋ผ๋„ ์—ฐ๊ฒฐ์ด ๋Š์–ด์ง€๊ธฐ ๋•Œ๋ฌธ์— ์›น์†Œ์ผ“(WebSocket) ํ”„๋กœํ† ์ฝœ์„ ํ†ตํ•œ ์ ‘๊ทผ์„ ์‹œ์ž‘ํ–ˆ๋‹ค

 

- ํ˜„์žฌ ํ”„๋กœ์ ํŠธ ํ™˜๊ฒฝ์—์„œ๋Š” ์Šคํ”„๋ง์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๊ณ , ์Šคํ”„๋ง์—์„œ ์›น์†Œ์ผ“ ํ”„๋กœํ† ์ฝœ์„ ํ™œ์šฉํ•  ๋•Œ ์ˆœ์ˆ˜ ์›น์†Œ์ผ“์„ ์‚ฌ์šฉํ•ด๋„ ๋˜์ง€๋งŒ, Stomp ํ”„๋กœํ† ์ฝœ์„ ์‚ฌ์šฉํ•ด ๋”์šฑ ๊ฐ„ํŽธํ•œ ๊ตฌํ˜„์ด ๊ฐ€๋Šฅํ•˜๋ฏ€๋กœ, ์ด ๊ฐœ๋ฐœ์„ ์ง„ํ–‰ํ–ˆ๋‹ค

-> ์‚ฌ์šฉํ•œ ๊ธฐ์Šฌ ์ŠคํŽ™

1) stomp

2) spring messing

3) event Listener

4) ConcurrentHashMap

1. ๊ตฌํ˜„ ๊ฐ„ trade - off

1) ์ˆœ์ˆ˜ ์›น์†Œ์ผ“ vs stomp

- ์›น์†Œ์ผ“์€ ๊ธฐ์ดˆ์ ์ธ ํ”„๋กœํ† ์ฝœ์ด๊ธฐ ๋•Œ๋ฌธ์—, ์‹ค์‹œ๊ฐ„ ํ†ต์‹ ์— ํ•„์š”ํ•œ ๋ฉ”์„ธ์ง€ ๋ผ์šฐํŒ…, ๋ฉ”์„ธ์ง€์˜ ์„ธ์…˜ ๊ด€๋ฆฌ, ๋ธŒ๋กœ๋“œ ์บ์ŠคํŒ…์— ๋Œ€ํ•œ ๊ธฐ๋Šฅ ๊ตฌํ˜„์„ ๋ชจ๋‘ ํ•ด์•ผํ•œ๋‹ค.

stomp์—์„œ๋Š” ํ•ด๋‹น ๊ธฐ๋Šฅ์„ ๋ณด๋‹ค ๊ฐ„ํŽธํ•˜๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ฉ”์„ธ์ง€ ๋ธŒ๋กœ์ปค๋ฅผ ์ง€์›ํ•˜๋ฉฐ,

๋˜ํ•œ ๋ฉ”์„ธ์ง€์— ๋Œ€ํ•ด ํ—ค๋”์™€ ๋ฐ”๋””๋ฅผ ๋ช…ํ™•ํ•˜๊ฒŒ ๊ตฌ๋ถ„ํ•˜๊ณ  ์žˆ์–ด ๋ฉ”์„ธ์ง€ ์†ก์ˆ˜์‹ ์ด ๋ณด๋‹ค ๊ฐ„ํŽธํ•˜๊ณ  ๋ช…ํ™•ํ•˜๊ฒŒ ์ง„ํ–‰์ด ๊ฐ€๋Šฅํ•˜๋‹ค

 

2) ๋ฉ”์„ธ์ง€ ๋ธŒ๋กœ์ปค : spring messging, rabbitMQ, kafka

- spring messging์€ ๋…๋ฆฝ์ ์ธ ๋ฉ”์„ธ์ง€ ๋ธŒ๋กœ์ปค์˜ ๊ฐœ๋…์ด ์•„๋‹ˆ๋ผ, ์Šคํ”„๋ง์—์„œ ์ œ๊ณตํ•˜๋Š” ๋ฉ”์„ธ์ง€ ๋ธŒ๋กœ์ปค ์ถ”์ƒํ™” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ ๋‹ค์–‘ํ•œ ๋ฉ”์„ธ์ง€ ๋ธŒ๋กœ์ปค๋ฅผ ์ฝ”๋“œ์˜ ๋ณ€๊ฒฝ์ด ๊ฑฐ์˜ ์—†์ด ์ž์œ ๋กญ๊ฒŒ ๋ณ€๊ฒฝ์ด ๊ฐ€๋Šฅํ•˜๋‹ค๋Š” ์žฅ์ ์„ ์ œ๊ณตํ•œ๋‹ค. ๋ฉ”์„ธ์ง€ ํ…œํ”Œ๋ฆฟ์œผ๋กœ messagingTemplate ์ธ์Šคํ„ด์Šค๋ฅผ ์ œ๊ณตํ•˜๋ฉฐ, ์ด ๋Œ€์‹  spring - rabbit ๋˜๋Š” spring - kafka ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

+ ํ˜„์žฌ ํ”„๋กœ์ ํŠธ์—์„œ rabbitMQ์™€ kafka๋ฅผ ํ†ตํ•œ ๋ฉ”์„ธ์ง• ๊ตฌํ˜„์€ ์˜ค๋ฒ„์ŠคํŽ™์ด์–ด์„œ,(ํŠธ๋ž˜ํ”ฝ์ด ๋†’์ง€ ์•Š๋‹ค) ํ•ด๋‹น ๊ธฐ์ˆ ์„ ํ™œ์šฉํ•œ ๊ฐœ๋ฐœ์€ ์•„์‰ฝ์ง€๋งŒ, ๋Ÿฌ๋‹์ปค๋ธŒ๋ฅผ ๊ณ ๋ คํ•ด ๋‹ค์Œ ๊ธฐํšŒ๋กœ ๋ฏธ๋ค˜๋‹ค

 

3) ๋™์‹œ์„ฑ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ concurrentHashMap ์ž๋ฃŒ๊ตฌ์กฐ ์‚ฌ์šฉ

- spring messging ํ…œํ”Œ๋ฆฟ๋งŒ์„ ์ด์šฉํ•ด ๋ฉ”์„ธ์ง• ๊ด€๋ฆฌ๋ฅผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ์‚ฌ์šฉ์ž ์ถ”์  ๋ฐฉ์‹์„

๊ตฌํ˜„ํ•  ๋•Œ ์–ด๋–ค ๋ฐฉ์‹์œผ๋กœ ํ•ด์•ผํ•˜๋Š” ์ง€ ๊ณ ๋ฏผ์„ ๋งŽ์ด ํ–ˆ๋‹ค.

 

๊ธฐ๋ณธ์ ์œผ๋กœ ์‚ฌ์šฉ์ž๋ฅผ ํŠน์ •ํ•  ๋•Œ ๋งŽ์ด ์‚ฌ์šฉํ•˜๋Š” ์„ธ์…˜์œผ๋กœ ์‚ฌ์šฉ์ž๋ฅผ ํŠน์ •ํ•˜๋ ค๊ณ  ํ•˜๋ฉด ๋ฉ”์„ธ์ง€ ๋ถ€๋ถ„์—์„œ ์„ธ์…˜์ด ๋ฉ”์„ธ์ง€๋งˆ๋‹ค ๊ฐ๊ฐ ์ƒˆ๋กœ ์ƒ์„ฑ๋˜๋Š” ๊ฒƒ์„ ์•Œ๊ฒŒ ๋˜์—ˆ๋‹ค. ๋”ฐ๋ผ์„œ ๊ฐ™์€ ๋ธŒ๋ผ์šฐ์ € ๋‚ด๋ผ๋„ ๋‹ค๋ฅธ ํƒญ ๋“ฑ์œผ๋กœ ๋ฉ”์„ธ์ง€๋ฅผ ํด๋ผ์ด์–ธํŠธ์—์„œ ์†ก์‹ ํ•˜๋Š” ๊ฒฝ์šฐ ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž๋กœ ์ธ์‹ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํ•ด๋‹น ๋ฌธ์ œ ํ•ด๊ฒฐ์ด ํ•„์š”ํ–ˆ๋‹ค

(์ถ”ํ›„ IP๋ฅผ ํ†ตํ•œ ๊ด€๋ฆฌ๋กœ ๋ฆฌํŒฉํ† ๋ง์ด ๊ฐ€๋Šฅํ• ์ง€ ๋ถ„์„ํ•ด์•ผํ•œ๋‹ค)

 

๊ทธ๋ž˜์„œ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋‹ด์€ Dto๋ฅผ ์ƒ์„ฑํ•˜๊ณ , ์ด๋ฅผ concurentHashMap์œผ๋กœ ๊ฐ์‹ผ ์ ‘์†์ž ๋ชฉ๋ก, ํ•ด๋‹น ์‚ฌ์šฉ์ž์˜ ์„ธ์…˜์„ ๊ด€๋ฆฌํ•˜๋Š” ๋ชฉ๋ก์„ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ concurrentHashMap์œผ๋กœ ๊ตฌํ˜„ํ–ˆ๋‹ค. ํ•ด๋‹น ์‚ฌ์šฉ์ž ๋ชฉ๋ก์— ๋Œ€ํ•ด์„œ๋Š” ์—ฌ๋Ÿฌ ์Šค๋ ˆ๋“œ์—์„œ ๋™์‹œ์— ์ ‘๊ทผํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ์Šค๋ ˆ๋“œ ์•ˆ์ „ ๋ฐ ๋™์‹œ์„ฑ ๋ณด์žฅ์ด ๋งค์šฐ ์ค‘์š”ํ–ˆ๋‹ค.

 

concurrentHashMap์€, ๋จผ์ € ๋‚ด๋ถ€ ๋ฐ์ดํ„ฐ์— ๊ฐ๊ฐ ๋…๋ฆฝ์ ์ธ ๋ฝ์„ ์‚ฌ์šฉํ•œ๋‹ค. ๋”ฐ๋ผ์„œ ์—ฌ๋Ÿฌ ์Šค๋ ˆ๋“œ๊ฐ€ ๋™์‹œ์— ์ ‘๊ทผํ•ด๋„ ๋ฝ ๊ฒฝ์Ÿ์ด ๋ฐœ์ƒํ•  ๊ฐ€๋Šฅ์„ฑ์ด ๋‚ฎ์•„์ง„๋‹ค. ๋˜ํ•œ ์Šค๋ ˆ๋“œ๊ฐ€ ์ฆ๊ฐ€ํ•ด๋„ ์„ฑ๋Šฅ ์ €ํ•˜์— ์˜ํ–ฅ์ด ์ตœ์†Œํ™”๋˜๋„๋ก ์„ค๊ณ„๋œ ์ž๋ฃŒ๊ตฌ์กฐ๊ธฐ ๋•Œ๋ฌธ์— ์ผ๋ฐ˜์ ์ธ hashMap ๋ณด๋‹ค ํ›จ์”ฌ ๋‚˜์€ ์ ์ด ๋งŽ์•˜๋‹ค

(์ถ”ํ›„ ํด๋Ÿฌ์Šคํ„ฐ ํ™˜๊ฒฝ์ด ๋„์ž…๋œ๋‹ค๋ฉด, ์ด ๋ถ€๋ถ„์— ๋Œ€ํ•ด redis ๋“ฑ์˜ ์ˆ˜๋‹จ์„ ํ†ตํ•ด ์‚ฌ์šฉ์ž ๋ชฉ๋ก ๊ด€๋ฆฌ๋ฅผ ๊ณ ๋„ํ™”ํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ์ด๋‹ค)

 

2. ์ฝ”๋“œ

1) ์—ฐ๊ฒฐ ํ•ด์ œ ์‹œ,

@EventListener
    public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
        StompHeaderAccessor accessor = StompHeaderAccessor.wrap(event.getMessage());
        String sessionId = accessor.getSessionId(); // ์—ฐ๊ฒฐ ํ•ด์ œ๋œ ์„ธ์…˜ ID ์ถ”์ถœ

        if (sessionId != null) {
            String email = userSessions.get(sessionId) != null ? userSessions.get(sessionId).get("email") : null;

            if (email == null) {
                logger.warn("No email found for sessionId: {}", sessionId);
                return;
            }

            removeUserSession(sessionId, email);

            sendClientListOnline();
        }
    }

- ๋ฉ”์„ธ์ง€ ํ•ด์ œ ์ด๋ฒคํŠธ ๋ฐœ์ƒ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ํ†ตํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋กœ์ง ๊ตฌํ˜„

- userSessions๋Š” ๋™์ผ ์‚ฌ์šฉ์ž์˜ ๋‹ค์ค‘ ๋ฉ”์„ธ์ง€ ์„ธ์…˜ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ map์œผ๋กœ ์‚ฌ์šฉ์ž๋ฅผ ํŠน์ •ํ•˜๊ธฐ ์œ„ํ•ด ์—ฌ๊ธฐ์„œ๋Š” ์ด๋ฉ”์ผ์„ ์‚ฌ์šฉํ–ˆ๋‹ค

- ์ด๋ฒคํŠธ์˜ ์„ธ์…˜ ์•„์ด๋””๋ฅผ ๊ฐ€์ง„ ์ด๋ฉ”์ผ(์‚ฌ์šฉ์ž)๋ฅผ ์ฐพ๊ณ  ๊ทธ ์‚ฌ์šฉ์ž๋ฅผ ์ ‘์†์ž ๋ชฉ๋ก์—์„œ ์ œ๊ฑฐํ•œ ํ›„, ๋ธŒ๋กœ๋“œ ์บ์ŠคํŒ…์œผ๋กœ ํ•ด๋‹น ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ๋ฐ˜์˜ํ•œ๋‹ค

 

2) ๋‹ค์ค‘ ์„ธ์…˜์—์„œ ์ตœ์‹  ์„ธ์…˜์˜ ์ •๋ณด๋กœ ์—…๋ฐ์ดํŠธ ํ•˜๊ธฐ ์œ„ํ•œ ์ตœ์‹  ์„ธ์…˜ ์ฐพ๊ธฐ

 private ConcurrentHashMap<String, String> findLatestUserInfo(String targetEmail) {
        return userSessions.entrySet().stream()
                .filter(entry -> targetEmail.equals(entry.getValue().get("email")))
                .max(Comparator.comparing(entry -> {
                    String timeStamp = entry.getValue().get("timestamp");
                    return timeStamp != null ? timeStamp : "";
                }))
                .map(Map.Entry::getValue)
                .orElse(null);
    }

-๋‹ค์ค‘ ์„ธ์…˜์„ ๋‹จ์ˆœํžˆ ์ €์žฅํ•˜๊ธฐ๋งŒ ํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, ์„ธ์…˜์—์„œ ๋ฉ”์„ธ์ง€๋ฅผ ๋ณด๋‚ธ ์‹œ๊ฐ„์„ timestamp๋กœ ๊ธฐ๋กํ•ด ๊ฐ€์žฅ ์ตœ์‹ ์˜ ์„ธ์…˜์œผ๋กœ ํ˜„์žฌ ์‚ฌ์šฉ์ž๊ฐ€ ์–ด๋А ํŽ˜์ด์ง€์— ์žˆ๋Š”์ง€๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋Š” ๋กœ์ง์ด๋‹ค

- ๋˜ํ•œ ์‚ฌ์šฉ์ž๊ฐ€ ์„ธ์…˜์„ ์ข…๋ฃŒํ•  ๊ฒฝ์šฐ, ๋™์ผ ์‚ฌ์šฉ์ž์˜ ๊ฐ€์žฅ ์ตœ์‹  ์„ธ์…˜์„ ์ฐพ๊ธฐ์œ„ํ•ด์„œ๋„ ํ™œ์šฉ๋œ๋‹ค

 

3) ์‚ฌ์šฉ์ž ์ถ”์  ๋กœ์ง

public void trackingUserInfo(ConcurrentHashMap<String, String> userInfo) {
        String email = userInfo.get("email");

        if (email == null) {
            logger.warn("Email is null, skipping user tracking");
            return;
        }

        if (!connectedUsers.containsKey(email)) {
           logger.debug("Adding user with email: {}", email);
        }

        makeUserSession(userInfo);

        ConcurrentHashMap<String, String> latestUserInfo = findLatestUserInfo(email);

        if (latestUserInfo != null) {
            connectedUsers.put(email, userInfo); // ์‚ฌ์šฉ์ž ์ •๋ณด ์—…๋ฐ์ดํŠธ
        }

        sendClientListOnline();
    }

 

- ํด๋ผ์ด์–ธํŠธ์—์„œ ๋ณด๋‚ด์ค€ ์ •๋ณด์—์„œ ์ด๋ฉ”์ผ์„ ์ฐพ์•„ ํ•ด๋‹น ์ •๋ณด์™€ ํ˜„์žฌ ํŽ˜์ด์ง€ ๋“ฑ์„ ๋ฐ”ํƒ•์œผ๋กœ ํ•œ ์œ ์ € ์„ธ์…˜ ๋ฆฌ์ŠคํŠธ์— ๋“ฑ๋กํ•œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๊ทธ ์„ธ์…˜์„ ๋ฐ”ํƒ•์œผ๋กœ ์ ‘์†์ž ๋ชฉ๋ก์„ ์—…๋ฐ์ดํŠธํ•˜๊ณ  ๋ธŒ๋กœ๋“œ ์บ์ŠคํŒ…์„ ํ†ตํ•ด ํด๋ผ์ด์–ธํŠธ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค

 

4) ์„ธ์…˜ ์ œ๊ฑฐ ๋กœ์ง

- ์—ฐ๊ฒฐ ํ•ด์ œ ์ด๋ฒคํŠธ ๋ฐœ์ƒ ์‹œ, 2)์˜ ์ตœ์‹  ์ •๋ณด ์ฐพ๊ธฐ ๋กœ์ง์„ ํ†ตํ•ด ๋จผ์ € ๋‚จ์•„์žˆ๋Š” ์„ธ์…˜์ด ์žˆ๋Š”์ง€๋ฅผ ํŒŒ์•…ํ•œ๋‹ค. ์—†๋Š” ๊ฒฝ์šฐ์— ํ•ด๋‹น ์‚ฌ์šฉ์ž๋ฅผ ์‚ฌ์šฉ์ž ๋ชฉ๋ก์—์„œ ์ œ๊ฑฐํ•œ๋‹ค. ๊ทธ๋ ‡์ง€ ์•Š์€ ๊ฒฝ์šฐ์—๋Š” 3)์„ ์•ž์—์„œ ์ฐพ์€ ์ตœ์‹  ์œ ์ € ์„ธ์…˜์„ ๊ฐ€์ง€๊ณ  ์žฌ ํ˜ธ์ถœํ•˜์—ฌ ํ•ด๋‹น ์‚ฌ์šฉ์ž์˜ ์ •๋ณด๋ฅผ ์—…๋ฐ์ดํŠธํ•œ๋‹ค

3. ์†Œ๊ฐ

- ๋งŒ๋“ค๋ฉด์„œ ์ƒ๋‹นํžˆ ์žฌ๋ฐŒ์—ˆ๋‹ค. ์„ค๊ณ„ ๋ถ€๋ถ„์—์„œ ์˜ค๋ฒ„์ŠคํŽ™์ด ๋˜์ง€ ์•Š๊ธฐ ์œ„ํ•ด ๊ณ ๋ฏผ์„ ํ–ˆ๊ณ , ํ˜ผ์ž ๊ตฌํ˜„ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ผ์ •์—๋„ ๋ถ€๋‹ด์ด ์—†์–ด์•ผ ํ–ˆ๋‹ค. ๊ทธ๋Ÿฐ ์ ์—์„œ ์ƒ๋‹นํžˆ ๊น”๋”ํ•˜๊ณ  ๋…ผ๋ฆฌ์ ์œผ๋กœ๋„ ์™„์„ฑ๋„ ์žˆ๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•œ ๋“ฏ ํ•˜์—ฌ ๋ณด๋žŒ์žˆ์—ˆ๋‹ค

- ์ถ”ํ›„ ์„ค๊ณ„๋ฅผ ํ†ตํ•ด ์–ผ๋งˆ๋“ ์ง€ ํ™•์žฅํ•  ์ˆ˜ ์žˆ๋Š” ํ™•์žฅ์„ฑ์žˆ๋Š” ์ฝ”๋“œ์™€ ์žฌ์‚ฌ์šฉ์„ฑ์ด ์ถฉ๋ถ„ํ•œ ์ฝ”๋“œ๋ฅผ ๊ตฌํ˜„ํ•œ ์ ์ด ๋งค์šฐ ์ข‹์•˜๊ณ , ๋™์‹œ์„ฑ์„ ๊ณ ๋ คํ•œ ์ ๋„ ๋งค์šฐ ๋œป์žˆ๋Š” ์ฝ”๋“œ์˜€๋‹ค

728x90