์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿš€ ์Šคํ”„๋ง WebSocket์œผ๋กœ ์‹ค์‹œ๊ฐ„ ํ†ต์‹  ๊ตฌํ˜„ํ•˜๊ธฐ

๐Ÿš€ ์Šคํ”„๋ง WebSocket์œผ๋กœ ์‹ค์‹œ๊ฐ„ ํ†ต์‹  ๊ตฌํ˜„ํ•˜๊ธฐ

์ฑ„ํŒ…, ์•Œ๋ฆผ, ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ๋ฅผ ์œ„ํ•œ ์™„๋ฒฝ ๊ฐ€์ด๋“œ

๐Ÿ’ฌ WebSocket, ์™œ ํ•„์š”ํ• ๊นŒ?

์—ฌ๋Ÿฌ๋ถ„, ์นด์นด์˜คํ†ก์ด๋‚˜ ์Šฌ๋ž™ ๊ฐ™์€ ๋ฉ”์‹ ์ € ์“ฐ๋ฉด์„œ "์™€, ๋ฉ”์‹œ์ง€๊ฐ€ ์ง„์งœ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์˜ค๋„ค!"๋ผ๊ณ  ์ƒ๊ฐํ•ด๋ณธ ์  ์žˆ์ฃ ? ๐Ÿ˜Š
๊ทธ๊ฒŒ ๋ฐ”๋กœ WebSocket์˜ ๋งˆ๋ฒ•์ด์—์š”. ์ „ํ†ต์ ์ธ HTTP ํ†ต์‹ ์€ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์š”์ฒญํ•ด์•ผ๋งŒ ์„œ๋ฒ„๊ฐ€ ์‘๋‹ตํ•˜๋Š” ๊ตฌ์กฐ์˜€์–ด์š”.
๋งˆ์น˜ ์šฐ์ฒด๋ถ€๊ฐ€ ํŽธ์ง€๋ฅผ ๋ฐฐ๋‹ฌํ•˜๋Ÿฌ ์˜ฌ ๋•Œ๋งˆ๋‹ค "ํ˜น์‹œ ์ œ ํŽธ์ง€ ์™”๋‚˜์š”?"๋ผ๊ณ  ๋ฌผ์–ด๋ด์•ผ ํ•˜๋Š” ๊ฒƒ์ฒ˜๋Ÿผ์š”.

ํ•˜์ง€๋งŒ WebSocket์€ ๋‹ค๋ฆ…๋‹ˆ๋‹ค! ํ•œ ๋ฒˆ ์—ฐ๊ฒฐ์„ ๋งบ์œผ๋ฉด ์„œ๋ฒ„์™€ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ž์œ ๋กญ๊ฒŒ ์–‘๋ฐฉํ–ฅ ํ†ต์‹ ์„ ํ•  ์ˆ˜ ์žˆ์–ด์š”. ๐Ÿ“ก
์ „ํ™” ํ†ตํ™”์ฒ˜๋Ÿผ ์„œ๋กœ ์–ธ์ œ๋“  ๋งํ•  ์ˆ˜ ์žˆ๋Š” ๊ฑฐ์ฃ . ์ด๊ฒŒ ๋ฐ”๋กœ ์‹ค์‹œ๊ฐ„ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ํ•ต์‹ฌ์ด์—์š”.

๐ŸŽฏ WebSocket์ด ๋น›๋‚˜๋Š” ์ˆœ๊ฐ„๋“ค โ€ข ์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ… ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๐Ÿ’ฌ
โ€ข ์ฃผ์‹/์•”ํ˜ธํ™”ํ ์‹ค์‹œ๊ฐ„ ์‹œ์„ธ ๐Ÿ“ˆ
โ€ข ์˜จ๋ผ์ธ ๊ฒŒ์ž„์˜ ๋ฉ€ํ‹ฐํ”Œ๋ ˆ์ด์–ด ๊ธฐ๋Šฅ ๐ŸŽฎ
โ€ข ํ˜‘์—… ๋„๊ตฌ์˜ ์‹ค์‹œ๊ฐ„ ๋™๊ธฐํ™” (๊ตฌ๊ธ€ ๋…์Šค์ฒ˜๋Ÿผ!) ๐Ÿ“
โ€ข ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ ์‹œ์Šคํ…œ ๐Ÿ””
โ€ข IoT ๋””๋ฐ”์ด์Šค ๋ชจ๋‹ˆํ„ฐ๋ง ๐ŸŒก๏ธ
HTTP vs WebSocket ํ†ต์‹  ๋น„๊ต HTTP ํ†ต์‹  ํด๋ผ์ด์–ธํŠธ ์„œ๋ฒ„ ์š”์ฒญ ์‘๋‹ต ๋งค๋ฒˆ ์—ฐ๊ฒฐ ์ƒ์„ฑ/์ข…๋ฃŒ ๋‹จ๋ฐฉํ–ฅ ํ†ต์‹  ์˜ค๋ฒ„ํ—ค๋“œ ๋ฐœ์ƒ WebSocket ํ†ต์‹  ํด๋ผ์ด์–ธํŠธ ์„œ๋ฒ„ ์‹ค์‹œ๊ฐ„ ์–‘๋ฐฉํ–ฅ ์ง€์†์  ์—ฐ๊ฒฐ ์œ ์ง€ ์–‘๋ฐฉํ–ฅ ํ†ต์‹  ๋‚ฎ์€ ์ง€์—ฐ์‹œ๊ฐ„

๐Ÿ› ๏ธ ์Šคํ”„๋ง์—์„œ WebSocket ์‹œ์ž‘ํ•˜๊ธฐ

์ž, ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ ์Šคํ”„๋ง์—์„œ WebSocket์„ ๊ตฌํ˜„ํ•ด๋ณผ๊นŒ์š”? ๐ŸŽจ
์Šคํ”„๋ง ํ”„๋ ˆ์ž„์›Œํฌ๋Š” WebSocket์„ ์ •๋ง ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ค˜์š”.
STOMP(Simple Text Oriented Messaging Protocol)๋ผ๋Š” ํ”„๋กœํ† ์ฝœ์„ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋ฉด ๋”์šฑ ๊ฐ•๋ ฅํ•ด์ง€์ฃ !

1
์˜์กด์„ฑ ์ถ”๊ฐ€ํ•˜๊ธฐ

๋จผ์ € Maven์ด๋‚˜ Gradle์— ํ•„์š”ํ•œ ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•ด์•ผ ํ•ด์š”. ์ด๊ฑด ๋งˆ์น˜ ์š”๋ฆฌํ•˜๊ธฐ ์ „์— ์žฌ๋ฃŒ๋ฅผ ์ค€๋น„ํ•˜๋Š” ๊ฒƒ๊ณผ ๊ฐ™์•„์š”! ๐Ÿฅ˜
<!-- Maven ์˜์กด์„ฑ -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

<!-- Gradle ์˜์กด์„ฑ -->
implementation 'org.springframework.boot:spring-boot-starter-websocket'
2
WebSocket ์„ค์ • ํด๋ž˜์Šค ๋งŒ๋“ค๊ธฐ

์ด์ œ WebSocket์„ ์„ค์ •ํ•  ์ฐจ๋ก€์˜ˆ์š”. ์ด ์„ค์ • ํด๋ž˜์Šค๋Š” WebSocket์˜ ์—”๋“œํฌ์ธํŠธ์™€ ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ์ปค๋ฅผ ์ •์˜ํ•ด์ค˜์š”. ๐Ÿ“‹
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        // ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ์ปค ํ™œ์„ฑํ™” - ํด๋ผ์ด์–ธํŠธ๋กœ ๋ฉ”์‹œ์ง€ ์ „๋‹ฌ
        config.enableSimpleBroker("/topic", "/queue");
        
        // ํด๋ผ์ด์–ธํŠธ์—์„œ ์„œ๋ฒ„๋กœ ๋ฉ”์‹œ์ง€ ๋ณด๋‚ผ ๋•Œ prefix
        config.setApplicationDestinationPrefixes("/app");
    }
    
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // WebSocket ์—ฐ๊ฒฐ ์—”๋“œํฌ์ธํŠธ
        registry.addEndpoint("/ws")
                .setAllowedOriginPatterns("*")
                .withSockJS(); // SockJS ํด๋ฐฑ ์˜ต์…˜
    }
}
๐Ÿ’ก ์„ค์ • ํฌ์ธํŠธ ์„ค๋ช… /topic: 1:N ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ์šฉ (์ฑ„ํŒ…๋ฐฉ ์ „์ฒด ๋ฉ”์‹œ์ง€)
/queue: 1:1 ๊ฐœ์ธ ๋ฉ”์‹œ์ง€์šฉ
/app: ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์„œ๋ฒ„๋กœ ๋ฉ”์‹œ์ง€ ๋ณด๋‚ผ ๋•Œ ์‚ฌ์šฉํ•˜๋Š” prefix
withSockJS(): WebSocket์„ ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์œ„ํ•œ ํด๋ฐฑ ์˜ต์…˜์ด์—์š”! ๐ŸŒ

๐Ÿ“จ ๋ฉ”์‹œ์ง€ ์ปจํŠธ๋กค๋Ÿฌ ๊ตฌํ˜„ํ•˜๊ธฐ

์ด์ œ ์‹ค์ œ๋กœ ๋ฉ”์‹œ์ง€๋ฅผ ์ฃผ๊ณ ๋ฐ›์„ ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”. ์ผ๋ฐ˜ REST ์ปจํŠธ๋กค๋Ÿฌ์™€ ๋น„์Šทํ•˜์ง€๋งŒ,
@MessageMapping๊ณผ @SendTo ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•œ๋‹ค๋Š” ์ ์ด ๋‹ค๋ฅด์ฃ ! โœจ

@Controller
public class ChatController {
    
    // ๋ฉ”์‹œ์ง€ ์ˆ˜์‹  ๋ฐ ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ
    @MessageMapping("/chat.sendMessage")
    @SendTo("/topic/public")
    public ChatMessage sendMessage(@Payload ChatMessage chatMessage) {
        return chatMessage;
    }
    
    // ์‚ฌ์šฉ์ž ์ž…์žฅ ์•Œ๋ฆผ
    @MessageMapping("/chat.addUser")
    @SendTo("/topic/public")
    public ChatMessage addUser(@Payload ChatMessage chatMessage,
                                SimpMessageHeaderAccessor headerAccessor) {
        // WebSocket ์„ธ์…˜์— ์‚ฌ์šฉ์ž๋ช… ์ถ”๊ฐ€
        headerAccessor.getSessionAttributes()
                     .put("username", chatMessage.getSender());
        return chatMessage;
    }
}
// ๋ฉ”์‹œ์ง€ DTO ํด๋ž˜์Šค
public class ChatMessage {
    private MessageType type;
    private String content;
    private String sender;
    private LocalDateTime timestamp;
    
    public enum MessageType {
        CHAT,    // ์ผ๋ฐ˜ ์ฑ„ํŒ… ๋ฉ”์‹œ์ง€
        JOIN,    // ์ž…์žฅ ์•Œ๋ฆผ
        LEAVE    // ํ‡ด์žฅ ์•Œ๋ฆผ
    }
    
    // Getter, Setter, Constructor ์ƒ๋žต
}

๐ŸŽฏ ํŠน์ • ์‚ฌ์šฉ์ž์—๊ฒŒ๋งŒ ๋ฉ”์‹œ์ง€ ๋ณด๋‚ด๊ธฐ

๋•Œ๋กœ๋Š” ์ „์ฒด๊ฐ€ ์•„๋‹Œ ํŠน์ • ์‚ฌ์šฉ์ž์—๊ฒŒ๋งŒ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ด์•ผ ํ•  ๋•Œ๊ฐ€ ์žˆ์–ด์š”.
์˜ˆ๋ฅผ ๋“ค์–ด ๊ฐœ์ธ ์•Œ๋ฆผ์ด๋‚˜ 1:1 ์ฑ„ํŒ… ๊ฐ™์€ ๊ฒฝ์šฐ์ฃ . ์ด๋Ÿด ๋• SimpMessagingTemplate์„ ์‚ฌ์šฉํ•ด์š”! ๐ŸŽฏ

@Service
public class NotificationService {
    
    @Autowired
    private SimpMessagingTemplate messagingTemplate;
    
    // ํŠน์ • ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋ฆผ ์ „์†ก
    public void sendNotificationToUser(String username, String message) {
        messagingTemplate.convertAndSendToUser(
            username,
            "/queue/notifications",
            new Notification(message, LocalDateTime.now())
        );
    }
    
    // ์ „์ฒด ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ
    public void broadcastMessage(String message) {
        messagingTemplate.convertAndSend(
            "/topic/public",
            new ChatMessage(MessageType.CHAT, message, "System")
        );
    }
}
WebSocket ๋ฉ”์‹œ์ง€ ํ๋ฆ„๋„ ํด๋ผ์ด์–ธํŠธ A ์›น ๋ธŒ๋ผ์šฐ์ € SockJS Client ํด๋ผ์ด์–ธํŠธ B ์›น ๋ธŒ๋ผ์šฐ์ € SockJS Client ์Šคํ”„๋ง ์„œ๋ฒ„ WebSocket Handler Message Broker Controller ๋ฉ”์‹œ์ง€ ํ topic/public queue/user1 queue/user2 ๋ฉ”์‹œ์ง€ ์ „์†ก ๋ฉ”์‹œ์ง€ ์ˆ˜์‹  ๋ธŒ๋กœ์ปค ํ†ต์‹  1 2 3 ์—ฐ๊ฒฐ ์ˆ˜๋ฆฝ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ

๐ŸŒ ํด๋ผ์ด์–ธํŠธ ์ธก ๊ตฌํ˜„ (JavaScript)

์„œ๋ฒ„ ๊ตฌํ˜„์ด ๋๋‚ฌ์œผ๋‹ˆ ์ด์ œ ํด๋ผ์ด์–ธํŠธ ์ชฝ์„ ๋งŒ๋“ค์–ด๋ณผ๊นŒ์š”? ๐Ÿ˜Š
SockJS์™€ STOMP.js ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ •๋ง ๊ฐ„๋‹จํ•ด์š”!

<!-- ํ•„์š”ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ CDN -->
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script>
// WebSocket ์—ฐ๊ฒฐ ๋ฐ ๊ตฌ๋…
let stompClient = null;

function connect() {
    const socket = new SockJS('/ws');
    stompClient = Stomp.over(socket);
    
    stompClient.connect({}, function(frame) {
        console.log('Connected: ' + frame);
        
        // ๊ณต๊ฐœ ์ฑ„๋„ ๊ตฌ๋…
        stompClient.subscribe('/topic/public', function(message) {
            showMessage(JSON.parse(message.body));
        });
        
        // ๊ฐœ์ธ ์•Œ๋ฆผ ๊ตฌ๋…
        stompClient.subscribe('/user/queue/notifications', function(notification) {
            showNotification(JSON.parse(notification.body));
        });
        
        // ์ž…์žฅ ๋ฉ”์‹œ์ง€ ์ „์†ก
        sendJoinMessage();
    });
}

// ๋ฉ”์‹œ์ง€ ์ „์†ก
function sendMessage() {
    const messageContent = document.getElementById('message').value;
    
    if(messageContent && stompClient) {
        const chatMessage = {
            sender: username,
            content: messageContent,
            type: 'CHAT',
            timestamp: new Date()
        };
        
        stompClient.send("/app/chat.sendMessage", 
                        {}, 
                        JSON.stringify(chatMessage));
        
        document.getElementById('message').value = '';
    }
}

// ์—ฐ๊ฒฐ ํ•ด์ œ
function disconnect() {
    if(stompClient !== null) {
        const leaveMessage = {
            sender: username,
            type: 'LEAVE'
        };
        
        stompClient.send("/app/chat.addUser", 
                        {}, 
                        JSON.stringify(leaveMessage));
        
        stompClient.disconnect();
    }
    console.log("Disconnected");
}
๐Ÿ” ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ ํ•ต์‹ฌ ํฌ์ธํŠธ SockJS: WebSocket์„ ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๋ธŒ๋ผ์šฐ์ €์—์„œ๋„ ์ž‘๋™ํ•˜๋„๋ก ํด๋ฐฑ ์ œ๊ณต
STOMP: ๋ฉ”์‹œ์ง€ ํ”„๋กœํ† ์ฝœ๋กœ, pub/sub ํŒจํ„ด์„ ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค˜์š”
subscribe(): ํŠน์ • ํ† ํ”ฝ์„ ๊ตฌ๋…ํ•˜์—ฌ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ›์•„์š”
send(): ์„œ๋ฒ„๋กœ ๋ฉ”์‹œ์ง€๋ฅผ ์ „์†กํ•ด์š”

๐Ÿ” ๋ณด์•ˆ ์„ค์ •ํ•˜๊ธฐ

์‹ค์‹œ๊ฐ„ ํ†ต์‹ ์—์„œ ๋ณด์•ˆ์€ ์ •๋ง ์ค‘์š”ํ•ด์š”! ๐Ÿ›ก๏ธ
์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ์™€ ํ•จ๊ป˜ WebSocket์„ ์•ˆ์ „ํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณผ๊ฒŒ์š”.

@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
    
    @Override
    protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
        messages
            // ์—ฐ๊ฒฐ ์—”๋“œํฌ์ธํŠธ๋Š” ์ธ์ฆ ํ•„์š”
            .simpDestMatchers("/app/**").authenticated()
            // ๊ตฌ๋…์€ ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž๋งŒ
            .simpSubscribeDestMatchers("/user/queue/**").authenticated()
            .simpSubscribeDestMatchers("/topic/**").authenticated()
            // ๊ธฐํƒ€ ๋ชจ๋“  ๋ฉ”์‹œ์ง€๋Š” ๊ฑฐ๋ถ€
            .anyMessage().denyAll();
    }
    
    @Override
    protected boolean sameOriginDisabled() {
        // CSRF ๋ณดํ˜ธ ๋น„ํ™œ์„ฑํ™” (๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋งŒ)
        return true;
    }
}
โš ๏ธ ๋ณด์•ˆ ์ฃผ์˜์‚ฌํ•ญ โ€ข CORS ์„ค์ •์„ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ๋ฐ˜๋“œ์‹œ ์ œํ•œํ•˜์„ธ์š”
โ€ข ๋ฏผ๊ฐํ•œ ์ •๋ณด๋Š” ์•”ํ˜ธํ™”ํ•˜์—ฌ ์ „์†กํ•˜์„ธ์š”
โ€ข ์„ธ์…˜ ํ•˜์ด์žฌํ‚น ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด HTTPS๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”
โ€ข Rate Limiting์„ ๊ตฌํ˜„ํ•˜์—ฌ DoS ๊ณต๊ฒฉ์„ ๋ฐฉ์–ดํ•˜์„ธ์š”
โ€ข ์‚ฌ์šฉ์ž ์ธ์ฆ/์ธ๊ฐ€๋ฅผ ์ฒ ์ €ํžˆ ๊ฒ€์ฆํ•˜์„ธ์š”

โšก ์„ฑ๋Šฅ ์ตœ์ ํ™” ํŒ

WebSocket์„ ์‚ฌ์šฉํ•  ๋•Œ ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”ํ•˜๋Š” ๊ฒƒ๋„ ์ค‘์š”ํ•ด์š”. ํŠนํžˆ ์‚ฌ์šฉ์ž๊ฐ€ ๋งŽ์•„์งˆ์ˆ˜๋ก ๋”์šฑ ๊ทธ๋ ‡์ฃ ! ๐Ÿš€

1๏ธโƒฃ ๋ฉ”์‹œ์ง€ ํฌ๊ธฐ ์ตœ์†Œํ™” ๋ถˆํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋Š” ์ œ๊ฑฐํ•˜๊ณ , JSON ๋Œ€์‹  MessagePack ๊ฐ™์€ ๋ฐ”์ด๋„ˆ๋ฆฌ ํฌ๋งท์„ ๊ณ ๋ คํ•ด๋ณด์„ธ์š”.
ํŠนํžˆ ์‹ค์‹œ๊ฐ„ ๊ฒŒ์ž„์ด๋‚˜ ๊ณ ๋นˆ๋„ ๋ฐ์ดํ„ฐ ์ „์†ก ์‹œ ํšจ๊ณผ์ ์ด์—์š”!
// ๋ฉ”์‹œ์ง€ ์••์ถ• ์„ค์ •
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
    config.enableSimpleBroker("/topic", "/queue")
          .setTaskScheduler(taskScheduler())
          .setHeartbeatValue(new long[] {10000, 10000}); // ํ•˜ํŠธ๋น„ํŠธ ์„ค์ •
    
    config.setApplicationDestinationPrefixes("/app");
    config.setCacheLimit(4096); // ์บ์‹œ ํฌ๊ธฐ ์„ค์ •
}
2๏ธโƒฃ ์—ฐ๊ฒฐ ํ’€ ๊ด€๋ฆฌ ๋™์‹œ ์—ฐ๊ฒฐ ์ˆ˜๋ฅผ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๊ณ  ์ ์ ˆํžˆ ์ œํ•œํ•˜์„ธ์š”. ๋ฌด์ œํ•œ ์—ฐ๊ฒฐ์€ ์„œ๋ฒ„ ๋ฆฌ์†Œ์Šค๋ฅผ ๊ณ ๊ฐˆ์‹œํ‚ฌ ์ˆ˜ ์žˆ์–ด์š”!
@Configuration
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    
    @Override
    public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
        registration
            .setMessageSizeLimit(128 * 1024)      // ๋ฉ”์‹œ์ง€ ํฌ๊ธฐ ์ œํ•œ: 128KB
            .setSendBufferSizeLimit(512 * 1024)   // ์ „์†ก ๋ฒ„ํผ: 512KB
            .setSendTimeLimit(20 * 1000)          // ์ „์†ก ํƒ€์ž„์•„์›ƒ: 20์ดˆ
            .setTimeToFirstMessage(30 * 1000);    // ์ฒซ ๋ฉ”์‹œ์ง€ ํƒ€์ž„์•„์›ƒ: 30์ดˆ
    }
}
3๏ธโƒฃ ์™ธ๋ถ€ ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ์ปค ์‚ฌ์šฉ ์‚ฌ์šฉ์ž๊ฐ€ ๋งŽ์•„์ง€๋ฉด RabbitMQ๋‚˜ Redis ๊ฐ™์€ ์™ธ๋ถ€ ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ์ปค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ์ข‹์•„์š”.
์Šคํ”„๋ง์˜ ๋‚ด์žฅ ๋ธŒ๋กœ์ปค๋Š” ๋‹จ์ผ ์„œ๋ฒ„ ํ™˜๊ฒฝ์— ์ ํ•ฉํ•˜๊ฑฐ๋“ ์š”! ๐Ÿฐ
// RabbitMQ ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ์ปค ์„ค์ •
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
    config.enableStompBrokerRelay("/topic", "/queue")
          .setRelayHost("localhost")
          .setRelayPort(61613)
          .setClientLogin("guest")
          .setClientPasscode("guest")
          .setSystemLogin("guest")
          .setSystemPasscode("guest");
    
    config.setApplicationDestinationPrefixes("/app");
}

๐Ÿ› ๋””๋ฒ„๊น…๊ณผ ๋ชจ๋‹ˆํ„ฐ๋ง

์‹ค์‹œ๊ฐ„ ํ†ต์‹ ์€ ๋””๋ฒ„๊น…์ด ๊นŒ๋‹ค๋กœ์šธ ์ˆ˜ ์žˆ์–ด์š”. ํ•˜์ง€๋งŒ ๊ฑฑ์ • ๋งˆ์„ธ์š”! ๋ช‡ ๊ฐ€์ง€ ์œ ์šฉํ•œ ํŒ์„ ์•Œ๋ ค๋“œ๋ฆด๊ฒŒ์š”. ๐Ÿ”

// ๋กœ๊น… ์„ค์ • (application.yml)
logging:
  level:
    org.springframework.messaging: DEBUG
    org.springframework.web.socket: DEBUG
    org.springframework.messaging.simp: TRACE
// WebSocket ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
@Component
public class WebSocketEventListener {
    
    private static final Logger logger = LoggerFactory.getLogger(WebSocketEventListener.class);
    
    @Autowired
    private SimpMessageSendingOperations messagingTemplate;
    
    @EventListener
    public void handleWebSocketConnectListener(SessionConnectedEvent event) {
        logger.info("์ƒˆ๋กœ์šด WebSocket ์—ฐ๊ฒฐ ์ˆ˜๋ฆฝ");
    }
    
    @EventListener
    public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
        StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
        
        String username = (String) headerAccessor.getSessionAttributes().get("username");
        if(username != null) {
            logger.info("์‚ฌ์šฉ์ž ์—ฐ๊ฒฐ ํ•ด์ œ: " + username);
            
            ChatMessage chatMessage = new ChatMessage();
            chatMessage.setType(MessageType.LEAVE);
            chatMessage.setSender(username);
            
            messagingTemplate.convertAndSend("/topic/public", chatMessage);
        }
    }
    
    @EventListener
    public void handleSessionSubscribeEvent(SessionSubscribeEvent event) {
        StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
        logger.info("์ƒˆ๋กœ์šด ๊ตฌ๋…: " + headerAccessor.getDestination());
    }
}
๐Ÿ’ก ๋ชจ๋‹ˆํ„ฐ๋ง ์ฒดํฌ๋ฆฌ์ŠคํŠธ
  • โœ… ํ™œ์„ฑ WebSocket ์—ฐ๊ฒฐ ์ˆ˜
  • โœ… ๋ฉ”์‹œ์ง€ ์ „์†ก/์ˆ˜์‹  ์†๋„
  • โœ… ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„
  • โœ… ์—๋Ÿฌ ๋ฐœ์ƒ๋ฅ 
  • โœ… ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰
  • โœ… CPU ์‚ฌ์šฉ๋ฅ 
์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ… ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์•„ํ‚คํ…์ฒ˜ ํ”„๋ก ํŠธ์—”๋“œ React / Vue SockJS Client STOMP.js ๋ฉ”์‹œ์ง€ ํ•ธ๋“ค๋ง UI ์ปดํฌ๋„ŒํŠธ ์ฑ„ํŒ…์ฐฝ, ์•Œ๋ฆผ ์ƒํƒœ ๊ด€๋ฆฌ Redux / Vuex ์Šคํ”„๋ง ๋ฐฑ์—”๋“œ WebSocket Handler ์—ฐ๊ฒฐ ๊ด€๋ฆฌ ๋ฐ ์ธ์ฆ Message Controller ๋ฉ”์‹œ์ง€ ๋ผ์šฐํŒ… ๋ฐ ์ฒ˜๋ฆฌ Message Broker RabbitMQ / Redis Business Logic ์ฑ„ํŒ…๋ฐฉ ๊ด€๋ฆฌ, ์‚ฌ์šฉ์ž ๊ด€๋ฆฌ ๋ฐ์ดํ„ฐ ๋ ˆ์ด์–ด MySQL / PostgreSQL ์ฑ„ํŒ… ํžˆ์Šคํ† ๋ฆฌ Redis Cache ์„ธ์…˜ ๊ด€๋ฆฌ MongoDB ๋กœ๊ทธ ์ €์žฅ S3 / CDN ํŒŒ์ผ ์ €์žฅ WebSocket ๋ฐ์ดํ„ฐ ํ†ต์‹ 

๐ŸŽจ ์‹ค์ „ ์˜ˆ์ œ: ๊ฐ„๋‹จํ•œ ์ฑ„ํŒ…๋ฐฉ ๊ตฌํ˜„

์ด์ œ ๋ฐฐ์šด ๋‚ด์šฉ์„ ๋ชจ๋‘ ํ™œ์šฉํ•ด์„œ ์‹ค์ œ๋กœ ์ž‘๋™ํ•˜๋Š” ์ฑ„ํŒ…๋ฐฉ์„ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”! ๐ŸŽ‰
์ด ์˜ˆ์ œ๋Š” ์—ฌ๋Ÿฌ ์‚ฌ์šฉ์ž๊ฐ€ ๋™์‹œ์— ์ฑ„ํŒ…ํ•  ์ˆ˜ ์žˆ๋Š” ๊ณต๊ฐœ ์ฑ„ํŒ…๋ฐฉ์ด์—์š”.

// ์ฑ„ํŒ…๋ฐฉ ์„œ๋น„์Šค
@Service
public class ChatRoomService {
    
    private final Map<String, ChatRoom> chatRooms = new ConcurrentHashMap<>();
    
    @PostConstruct
    private void init() {
        // ๊ธฐ๋ณธ ์ฑ„ํŒ…๋ฐฉ ์ƒ์„ฑ
        ChatRoom defaultRoom = ChatRoom.create("์ผ๋ฐ˜ ์ฑ„ํŒ…๋ฐฉ");
        chatRooms.put(defaultRoom.getRoomId(), defaultRoom);
    }
    
    public List<ChatRoom> findAllRooms() {
        return new ArrayList<>(chatRooms.values());
    }
    
    public ChatRoom findRoomById(String roomId) {
        return chatRooms.get(roomId);
    }
    
    public ChatRoom createRoom(String name) {
        ChatRoom chatRoom = ChatRoom.create(name);
        chatRooms.put(chatRoom.getRoomId(), chatRoom);
        return chatRoom;
    }
}
// ์ฑ„ํŒ…๋ฐฉ ์—”ํ‹ฐํ‹ฐ
@Getter
@Setter
public class ChatRoom {
    private String roomId;
    private String name;
    private Set<String> users = new HashSet<>();
    private LocalDateTime createdAt;
    
    public static ChatRoom create(String name) {
        ChatRoom room = new ChatRoom();
        room.roomId = UUID.randomUUID().toString();
        room.name = name;
        room.createdAt = LocalDateTime.now();
        return room;
    }
    
    public void addUser(String username) {
        users.add(username);
    }
    
    public void removeUser(String username) {
        users.remove(username);
    }
    
    public int getUserCount() {
        return users.size();
    }
}
// ์ฑ„ํŒ…๋ฐฉ ์ปจํŠธ๋กค๋Ÿฌ
@Controller
@RequiredArgsConstructor
public class ChatRoomController {
    
    private final ChatRoomService chatRoomService;
    private final SimpMessagingTemplate messagingTemplate;
    
    // ์ฑ„ํŒ…๋ฐฉ ์ž…์žฅ
    @MessageMapping("/chat.enter/{roomId}")
    public void enterRoom(@DestinationVariable String roomId,
                         @Payload ChatMessage message,
                         SimpMessageHeaderAccessor headerAccessor) {
        
        ChatRoom room = chatRoomService.findRoomById(roomId);
        if (room != null) {
            room.addUser(message.getSender());
            
            // ์„ธ์…˜์— ์‚ฌ์šฉ์ž ์ •๋ณด ์ €์žฅ
            headerAccessor.getSessionAttributes().put("username", message.getSender());
            headerAccessor.getSessionAttributes().put("roomId", roomId);
            
            // ์ž…์žฅ ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ
            message.setType(MessageType.JOIN);
            message.setContent(message.getSender() + "๋‹˜์ด ์ž…์žฅํ•˜์…จ์Šต๋‹ˆ๋‹ค.");
            messagingTemplate.convertAndSend("/topic/room/" + roomId, message);
            
            // ํ˜„์žฌ ์ธ์› ์ˆ˜ ์—…๋ฐ์ดํŠธ
            updateRoomUserCount(roomId, room.getUserCount());
        }
    }
    
    // ์ฑ„ํŒ… ๋ฉ”์‹œ์ง€ ์ „์†ก
    @MessageMapping("/chat.message/{roomId}")
    public void sendMessage(@DestinationVariable String roomId,
                           @Payload ChatMessage message) {
        
        message.setTimestamp(LocalDateTime.now());
        messagingTemplate.convertAndSend("/topic/room/" + roomId, message);
    }
    
    // ์ฑ„ํŒ…๋ฐฉ ๋‚˜๊ฐ€๊ธฐ
    @MessageMapping("/chat.leave/{roomId}")
    public void leaveRoom(@DestinationVariable String roomId,
                         @Payload ChatMessage message) {
        
        ChatRoom room = chatRoomService.findRoomById(roomId);
        if (room != null) {
            room.removeUser(message.getSender());
            
            message.setType(MessageType.LEAVE);
            message.setContent(message.getSender() + "๋‹˜์ด ํ‡ด์žฅํ•˜์…จ์Šต๋‹ˆ๋‹ค.");
            messagingTemplate.convertAndSend("/topic/room/" + roomId, message);
            
            updateRoomUserCount(roomId, room.getUserCount());
        }
    }
    
    private void updateRoomUserCount(String roomId, int count) {
        Map<String, Object> payload = new HashMap<>();
        payload.put("roomId", roomId);
        payload.put("userCount", count);
        messagingTemplate.convertAndSend("/topic/room/" + roomId + "/count", payload);
    }
}
๐ŸŽฏ ์ฑ„ํŒ…๋ฐฉ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ํฌ์ธํŠธ โ€ข ๋ฐฉ ๋‹จ์œ„ ๋ฉ”์‹œ์ง€ ์ „์†ก: /topic/room/{roomId} ํ˜•์‹์œผ๋กœ ๊ฐ ๋ฐฉ๋งˆ๋‹ค ๋…๋ฆฝ์ ์ธ ํ† ํ”ฝ ์‚ฌ์šฉ
โ€ข ์‚ฌ์šฉ์ž ๊ด€๋ฆฌ: Set์„ ์‚ฌ์šฉํ•˜์—ฌ ์ค‘๋ณต ์—†์ด ์‚ฌ์šฉ์ž ๊ด€๋ฆฌ
โ€ข ์ž…์žฅ/ํ‡ด์žฅ ์•Œ๋ฆผ: ์‹œ์Šคํ…œ ๋ฉ”์‹œ์ง€๋กœ ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž๋“ค์—๊ฒŒ ์•Œ๋ฆผ
โ€ข ์‹ค์‹œ๊ฐ„ ์ธ์› ์ˆ˜: ๋ณ„๋„ ํ† ํ”ฝ์œผ๋กœ ํ˜„์žฌ ์ธ์› ์ˆ˜ ์—…๋ฐ์ดํŠธ

๐Ÿ”ง ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… ๊ฐ€์ด๋“œ

WebSocket ๊ฐœ๋ฐœํ•˜๋‹ค ๋ณด๋ฉด ์—ฌ๋Ÿฌ ๋ฌธ์ œ๋ฅผ ๋งŒ๋‚  ์ˆ˜ ์žˆ์–ด์š”. ์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ๋“ค๊ณผ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์„ ์ •๋ฆฌํ•ด๋ดค์–ด์š”! ๐Ÿ› ๏ธ

๋ฌธ์ œ ์ƒํ™ฉ ์›์ธ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•
์—ฐ๊ฒฐ์ด ์ž์ฃผ ๋Š๊น€ ๋„คํŠธ์›Œํฌ ๋ถˆ์•ˆ์ •, ํƒ€์ž„์•„์›ƒ ํ•˜ํŠธ๋น„ํŠธ ์„ค์ •, ์žฌ์—ฐ๊ฒฐ ๋กœ์ง ๊ตฌํ˜„
๋ฉ”์‹œ์ง€๊ฐ€ ์ „๋‹ฌ๋˜์ง€ ์•Š์Œ ํ† ํ”ฝ ๊ฒฝ๋กœ ์˜ค๋ฅ˜, ๊ตฌ๋… ์‹คํŒจ ๊ฒฝ๋กœ ํ™•์ธ, ๋กœ๊ทธ๋กœ ๊ตฌ๋… ์ƒํƒœ ์ฒดํฌ
CORS ์—๋Ÿฌ Origin ์„ค์ • ๋ˆ„๋ฝ setAllowedOriginPatterns ์„ค์ •
๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ์—ฐ๊ฒฐ ํ•ด์ œ ์‹œ ๋ฆฌ์†Œ์Šค ๋ฏธ์ •๋ฆฌ disconnect ์ด๋ฒคํŠธ์—์„œ ์ •๋ฆฌ ๋กœ์ง
๋™์‹œ ์ ‘์†์ž ์ฆ๊ฐ€ ์‹œ ๋А๋ ค์ง ๋‹จ์ผ ์„œ๋ฒ„ ํ•œ๊ณ„ ์™ธ๋ถ€ ๋ธŒ๋กœ์ปค ์‚ฌ์šฉ, ๋กœ๋“œ ๋ฐธ๋Ÿฐ์‹ฑ
๐Ÿšจ ์ž์ฃผ ํ•˜๋Š” ์‹ค์ˆ˜๋“ค 1. ์—ฐ๊ฒฐ ํ•ด์ œ ์ฒ˜๋ฆฌ ๋ˆ„๋ฝ
์‚ฌ์šฉ์ž๊ฐ€ ๋ธŒ๋ผ์šฐ์ €๋ฅผ ๋‹ซ๊ฑฐ๋‚˜ ๋„คํŠธ์›Œํฌ๊ฐ€ ๋Š๊ฒผ์„ ๋•Œ ์ฒ˜๋ฆฌ๋ฅผ ์•ˆ ํ•˜๋ฉด ์ข€๋น„ ์„ธ์…˜์ด ๋‚จ์•„์š”!

2. ๋ฉ”์‹œ์ง€ ํฌ๊ธฐ ์ œํ•œ ๋ฏธ์„ค์ •
์•…์˜์ ์ธ ์‚ฌ์šฉ์ž๊ฐ€ ํฐ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ด๋ฉด ์„œ๋ฒ„๊ฐ€ ๋‹ค์šด๋  ์ˆ˜ ์žˆ์–ด์š”.

3. ์—๋Ÿฌ ํ•ธ๋“ค๋ง ๋ถ€์กฑ
๋„คํŠธ์›Œํฌ ์—๋Ÿฌ, ํŒŒ์‹ฑ ์—๋Ÿฌ ๋“ฑ ๋‹ค์–‘ํ•œ ์˜ˆ์™ธ ์ƒํ™ฉ์„ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•ด์š”.

4. ํ…Œ์ŠคํŠธ ๋ถ€์กฑ
๋™์‹œ ์ ‘์†์ž ํ…Œ์ŠคํŠธ, ๋„คํŠธ์›Œํฌ ์ง€์—ฐ ํ…Œ์ŠคํŠธ ๋“ฑ์ด ํ•„์ˆ˜์˜ˆ์š”!

๐Ÿ“Š ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ์™€ ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ

์‹ค์ œ ์„œ๋น„์Šค์— ๋ฐฐํฌํ•˜๊ธฐ ์ „์— ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ๋Š” ํ•„์ˆ˜์˜ˆ์š”! JMeter๋‚˜ Gatling์„ ์‚ฌ์šฉํ•˜๋ฉด ์‰ฝ๊ฒŒ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์–ด์š”. ๐Ÿ“ˆ

// Gatling ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์˜ˆ์ œ
class WebSocketSimulation extends Simulation {
    
    val httpProtocol = http
        .baseUrl("http://localhost:8080")
        .wsBaseUrl("ws://localhost:8080")
    
    val scn = scenario("WebSocket Load Test")
        .exec(ws("Connect")
            .connect("/ws"))
        .pause(1)
        .exec(ws("Subscribe")
            .sendText("""{"type":"SUBSCRIBE","destination":"/topic/public"}"""))
        .pause(1)
        .repeat(100) {
            exec(ws("Send Message")
                .sendText("""{"type":"CHAT","content":"Test message","sender":"user"}"""))
            .pause(1)
        }
        .exec(ws("Close").close)
    
    setUp(
        scn.inject(
            rampUsers(1000) during (60 seconds)
        )
    ).protocols(httpProtocol)
}
๐Ÿ’ก ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ ์ฒดํฌํฌ์ธํŠธ โ€ข ๋™์‹œ ์ ‘์†์ž ์ˆ˜: ๋ชฉํ‘œ ์‚ฌ์šฉ์ž ์ˆ˜์˜ 2๋ฐฐ๋กœ ํ…Œ์ŠคํŠธ
โ€ข ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ๋Ÿ‰: ์ดˆ๋‹น ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅํ•œ ๋ฉ”์‹œ์ง€ ์ˆ˜
โ€ข ์‘๋‹ต ์‹œ๊ฐ„: ํ‰๊ท , ์ตœ๋Œ€, 95 percentile
โ€ข ๋ฆฌ์†Œ์Šค ์‚ฌ์šฉ๋Ÿ‰: CPU, ๋ฉ”๋ชจ๋ฆฌ, ๋„คํŠธ์›Œํฌ ๋Œ€์—ญํญ
โ€ข ์—๋Ÿฌ์œจ: ์—ฐ๊ฒฐ ์‹คํŒจ, ๋ฉ”์‹œ์ง€ ์†์‹ค๋ฅ 

๐ŸŒŸ ์‹ค๋ฌด ํ™œ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค

WebSocket์„ ์‹ค์ œ ํ”„๋กœ์ ํŠธ์— ์–ด๋–ป๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์„๊นŒ์š”? ๋‹ค์–‘ํ•œ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์‚ดํŽด๋ณผ๊ฒŒ์š”! โœจ

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

๐Ÿš€ ๋ฐฐํฌ์™€ ์šด์˜

๊ฐœ๋ฐœ์ด ๋๋‚ฌ๋‹ค๋ฉด ์ด์ œ ์‹ค์ œ ์„œ๋น„์Šค์— ๋ฐฐํฌํ•  ์ฐจ๋ก€์˜ˆ์š”! ์šด์˜ ํ™˜๊ฒฝ์—์„œ ์ฃผ์˜ํ•  ์ ๋“ค์„ ์•Œ์•„๋ณผ๊ฒŒ์š”. ๐ŸŽฏ

// application-prod.yml (ํ”„๋กœ๋•์…˜ ์„ค์ •)
spring:
  websocket:
    message-size-limit: 128KB
    send-buffer-size-limit: 512KB
    send-time-limit: 20000
    
server:
  tomcat:
    max-connections: 10000
    threads:
      max: 200
      min-spare: 10
      
management:
  endpoints:
    web:
      exposure:
        include: health,metrics,websocket
  metrics:
    export:
      prometheus:
        enabled: true
โ˜๏ธ ํด๋ผ์šฐ๋“œ ๋ฐฐํฌ ์‹œ ๊ณ ๋ ค์‚ฌํ•ญ AWS: Application Load Balancer๋Š” WebSocket์„ ์ง€์›ํ•ด์š”. Sticky Session ์„ค์ • ํ•„์š”!
Nginx: ๋ฆฌ๋ฒ„์Šค ํ”„๋ก์‹œ ์„ค์ • ์‹œ upgrade ํ—ค๋” ์ „๋‹ฌ ํ•„์ˆ˜
Docker: ์ปจํ…Œ์ด๋„ˆ ๊ฐ„ ํ†ต์‹ ์„ ์œ„ํ•ด Redis ๊ฐ™์€ ์™ธ๋ถ€ ๋ธŒ๋กœ์ปค ์‚ฌ์šฉ
Kubernetes: Service์˜ sessionAffinity๋ฅผ ClientIP๋กœ ์„ค์ •
// Nginx ์„ค์ • ์˜ˆ์ œ
upstream websocket {
    server localhost:8080;
}

server {
    listen 80;
    
    location /ws {
        proxy_pass http://websocket;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_connect_timeout 7d;
        proxy_send_timeout 7d;
        proxy_read_timeout 7d;
    }
}

๐Ÿ“š ๋” ์•Œ์•„๋ณด๊ธฐ

WebSocket์„ ๋” ๊นŠ์ด ๊ณต๋ถ€ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์ด๋Ÿฐ ์ฃผ์ œ๋“ค์„ ์‚ดํŽด๋ณด์„ธ์š”! ๐Ÿ“–

๐Ÿ” ๊ณ ๊ธ‰ ์ฃผ์ œ๋“ค โ€ข ํด๋Ÿฌ์Šคํ„ฐ๋ง: ์—ฌ๋Ÿฌ ์„œ๋ฒ„์—์„œ WebSocket ์„ธ์…˜ ๊ณต์œ ํ•˜๊ธฐ
โ€ข ๋ฉ”์‹œ์ง€ ํ: Kafka, RabbitMQ์™€ ํ†ตํ•ฉํ•˜๊ธฐ
โ€ข ํ”„๋กœํ† ์ฝœ ๋ฒ„ํผ: JSON ๋Œ€์‹  ํšจ์œจ์ ์ธ ๋ฐ”์ด๋„ˆ๋ฆฌ ํฌ๋งท ์‚ฌ์šฉ
โ€ข ๋ฐฑํ”„๋ ˆ์…”: ๋ฉ”์‹œ์ง€ ๊ณผ๋ถ€ํ•˜ ์‹œ ํ๋ฆ„ ์ œ์–ด
โ€ข ์ƒค๋”ฉ: ์ฑ„ํŒ…๋ฐฉ์„ ์—ฌ๋Ÿฌ ์„œ๋ฒ„์— ๋ถ„์‚ฐํ•˜๊ธฐ
โ€ข ๋ฉ”์‹œ์ง€ ์˜์†์„ฑ: ์˜คํ”„๋ผ์ธ ๋ฉ”์‹œ์ง€ ์ €์žฅ ๋ฐ ์ „๋‹ฌ
๐Ÿ› ๏ธ ์œ ์šฉํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์™€ ๋„๊ตฌ โ€ข Socket.IO: Node.js ํ™˜๊ฒฝ์—์„œ ์ธ๊ธฐ ์žˆ๋Š” ์‹ค์‹œ๊ฐ„ ํ†ต์‹  ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
โ€ข Netty: ๊ณ ์„ฑ๋Šฅ ๋„คํŠธ์›Œํฌ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํ”„๋ ˆ์ž„์›Œํฌ
โ€ข Spring Cloud Stream: ๋ฉ”์‹œ์ง€ ๊ธฐ๋ฐ˜ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ๊ตฌ์ถ•
โ€ข Apache Kafka: ๋Œ€์šฉ๋Ÿ‰ ๋ฉ”์‹œ์ง€ ์ŠคํŠธ๋ฆฌ๋ฐ ํ”Œ๋žซํผ
โ€ข Redis Pub/Sub: ๊ฐ„๋‹จํ•œ ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ์ปค๋กœ ํ™œ์šฉ
WebSocket ํ•™์Šต ๋กœ๋“œ๋งต ๊ธฐ์ดˆ WebSocket ๊ฐœ๋… ์ดํ•ด ์ดˆ๊ธ‰ ์Šคํ”„๋ง ์„ค์ • ๊ฐ„๋‹จํ•œ ์ฑ„ํŒ… ์ค‘๊ธ‰ ์ฑ„ํŒ…๋ฐฉ ๊ตฌํ˜„ ๋ณด์•ˆ ์„ค์ • ๊ณ ๊ธ‰ ํด๋Ÿฌ์Šคํ„ฐ๋ง ์„ฑ๋Šฅ ์ตœ์ ํ™” ํ•„์ˆ˜ ๊ฐœ๋… HTTP vs WebSocket STOMP ํ”„๋กœํ† ์ฝœ ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ์ปค Pub/Sub ํŒจํ„ด ์‹ค์Šต ํ”„๋กœ์ ํŠธ 1:1 ์ฑ„ํŒ… ๊ทธ๋ฃน ์ฑ„ํŒ… ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ ํ˜‘์—… ๋„๊ตฌ ์„ฑ๋Šฅ ๊ณ ๋ ค์‚ฌํ•ญ ๋ฉ”์‹œ์ง€ ์••์ถ• ์—ฐ๊ฒฐ ํ’€ ๊ด€๋ฆฌ ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ ๋ชจ๋‹ˆํ„ฐ๋ง ์šด์˜ ๋…ธํ•˜์šฐ ๋ฐฐํฌ ์ „๋žต ์žฅ์•  ๋Œ€์‘ ์Šค์ผ€์ผ๋ง ๋ณด์•ˆ ๊ฐ•ํ™”

๐Ÿ’ญ ๋งˆ์น˜๋ฉฐ

์ž, ์—ฌ๊ธฐ๊นŒ์ง€ ์Šคํ”„๋ง WebSocket์œผ๋กœ ์‹ค์‹œ๊ฐ„ ํ†ต์‹ ์„ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ํ•จ๊ป˜ ์•Œ์•„๋ดค์–ด์š”! ๐ŸŽ‰

์ฒ˜์Œ์—๋Š” ๋ณต์žกํ•ด ๋ณด์ผ ์ˆ˜ ์žˆ์ง€๋งŒ, ํ•˜๋‚˜์”ฉ ๋”ฐ๋ผํ•˜๋‹ค ๋ณด๋ฉด ์ƒ๊ฐ๋ณด๋‹ค ์–ด๋ ต์ง€ ์•Š๋‹ค๋Š” ๊ฑธ ๋А๋ผ์‹ค ๊ฑฐ์˜ˆ์š”.
WebSocket์€ ํ˜„๋Œ€ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์ •๋ง ์ค‘์š”ํ•œ ๊ธฐ์ˆ ์ด์—์š”. ์‚ฌ์šฉ์ž๋“ค์€ ์ ์  ๋” ๋น ๋ฅด๊ณ  ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ฐ˜์‘ํ•˜๋Š” ์„œ๋น„์Šค๋ฅผ ์›ํ•˜๊ฑฐ๋“ ์š”! โšก

์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ ์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ…์ด๋‚˜ ์•Œ๋ฆผ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜๋ฉด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ํ›จ์”ฌ ์ข‹์•„์งˆ ๊ฑฐ์˜ˆ์š”.
์—ฌ๋Ÿฌ๋ถ„๋„ ์ด ๊ธฐ์ˆ ์„ ํ™œ์šฉํ•ด์„œ ๋ฉ‹์ง„ ์‹ค์‹œ๊ฐ„ ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค์–ด๋ณด์„ธ์š”! ๐Ÿ’ช

๊ธฐ์–ตํ•˜์„ธ์š”! ์ข‹์€ ๊ฐœ๋ฐœ์ž๋Š” ๋‹จ์ˆœํžˆ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ์‚ฌ๋žŒ์ด ์•„๋‹ˆ๋ผ,
์‚ฌ์šฉ์ž์˜ ๊ฒฝํ—˜์„ ์ƒ๊ฐํ•˜๊ณ  ์•ˆ์ •์ ์ธ ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“œ๋Š” ์‚ฌ๋žŒ์ด์—์š”. ๐ŸŒŸ

์‹ค์‹œ๊ฐ„ ํ†ต์‹  ๊ตฌํ˜„ํ•˜๋ฉด์„œ ๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ๋‹ค๋ฉด ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ์ฐธ๊ณ ํ•˜๊ฑฐ๋‚˜,
์ปค๋ฎค๋‹ˆํ‹ฐ์— ์งˆ๋ฌธํ•ด๋ณด์„ธ์š”. ๋งŽ์€ ๊ฐœ๋ฐœ์ž๋“ค์ด ๊ธฐ๊บผ์ด ๋„์™€์ค„ ๊ฑฐ์˜ˆ์š”! ๐Ÿค

๊ทธ๋Ÿผ ์—ฌ๋Ÿฌ๋ถ„์˜ ํ”„๋กœ์ ํŠธ์— WebSocket์„ ์„ฑ๊ณต์ ์œผ๋กœ ์ ์šฉํ•˜์‹œ๊ธธ ๋ฐ”๋ผ๋ฉฐ,
์ฆ๊ฑฐ์šด ์ฝ”๋”ฉ ๋˜์„ธ์š”! Happy Coding! ๐Ÿš€โœจ

๐ŸŽฏ ์‹ค์‹œ๊ฐ„ ํ†ต์‹ ์œผ๋กœ ๋” ๋‚˜์€ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ๋งŒ๋“ค์–ด๋ณด์„ธ์š”!

WebSocket์€ ์—ฌ๋Ÿฌ๋ถ„์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ํ•œ ๋‹จ๊ณ„ ์—…๊ทธ๋ ˆ์ด๋“œ์‹œ์ผœ์ค„ ๊ฑฐ์˜ˆ์š” ๐Ÿš€

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.