Ver3.0 ๐ ์คํ๋ง WebSocket์ผ๋ก ์ค์๊ฐ ํต์ ๊ตฌํํ๊ธฐ

๐ ์คํ๋ง WebSocket์ผ๋ก ์ค์๊ฐ ํต์ ๊ตฌํํ๊ธฐ
์ฑํ , ์๋ฆผ, ์ค์๊ฐ ๋ฐ์ดํฐ ์ ๋ฐ์ดํธ๋ฅผ ์ํ ์๋ฒฝ ๊ฐ์ด๋
๐ฌ WebSocket, ์ ํ์ํ ๊น?
์ฌ๋ฌ๋ถ, ์นด์นด์คํก์ด๋ ์ฌ๋ ๊ฐ์ ๋ฉ์ ์ ์ฐ๋ฉด์ "์, ๋ฉ์์ง๊ฐ ์ง์ง ์ค์๊ฐ์ผ๋ก ์ค๋ค!"๋ผ๊ณ ์๊ฐํด๋ณธ ์ ์์ฃ ? ๐
๊ทธ๊ฒ ๋ฐ๋ก WebSocket์ ๋ง๋ฒ์ด์์. ์ ํต์ ์ธ HTTP ํต์ ์ ํด๋ผ์ด์ธํธ๊ฐ ์์ฒญํด์ผ๋ง ์๋ฒ๊ฐ ์๋ตํ๋ ๊ตฌ์กฐ์์ด์.
๋ง์น ์ฐ์ฒด๋ถ๊ฐ ํธ์ง๋ฅผ ๋ฐฐ๋ฌํ๋ฌ ์ฌ ๋๋ง๋ค "ํน์ ์ ํธ์ง ์๋์?"๋ผ๊ณ ๋ฌผ์ด๋ด์ผ ํ๋ ๊ฒ์ฒ๋ผ์.
ํ์ง๋ง WebSocket์ ๋ค๋ฆ
๋๋ค! ํ ๋ฒ ์ฐ๊ฒฐ์ ๋งบ์ผ๋ฉด ์๋ฒ์ ํด๋ผ์ด์ธํธ๊ฐ ์์ ๋กญ๊ฒ ์๋ฐฉํฅ ํต์ ์ ํ ์ ์์ด์. ๐ก
์ ํ ํตํ์ฒ๋ผ ์๋ก ์ธ์ ๋ ๋งํ ์ ์๋ ๊ฑฐ์ฃ . ์ด๊ฒ ๋ฐ๋ก ์ค์๊ฐ ์น ์ ํ๋ฆฌ์ผ์ด์
์ ํต์ฌ์ด์์.
โข ์ฃผ์/์ํธํํ ์ค์๊ฐ ์์ธ ๐
โข ์จ๋ผ์ธ ๊ฒ์์ ๋ฉํฐํ๋ ์ด์ด ๊ธฐ๋ฅ ๐ฎ
โข ํ์ ๋๊ตฌ์ ์ค์๊ฐ ๋๊ธฐํ (๊ตฌ๊ธ ๋ ์ค์ฒ๋ผ!) ๐
โข ์ค์๊ฐ ์๋ฆผ ์์คํ ๐
โข IoT ๋๋ฐ์ด์ค ๋ชจ๋ํฐ๋ง ๐ก๏ธ
๐ ๏ธ ์คํ๋ง์์ WebSocket ์์ํ๊ธฐ
์, ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ์คํ๋ง์์ WebSocket์ ๊ตฌํํด๋ณผ๊น์? ๐จ
์คํ๋ง ํ๋ ์์ํฌ๋ WebSocket์ ์ ๋ง ์ฝ๊ฒ ์ฌ์ฉํ ์ ์๋๋ก ๋์์ค์.
STOMP(Simple Text Oriented Messaging Protocol)๋ผ๋ ํ๋กํ ์ฝ์ ํจ๊ป ์ฌ์ฉํ๋ฉด ๋์ฑ ๊ฐ๋ ฅํด์ง์ฃ !
๋จผ์ 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'
์ด์ 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 ํด๋ฐฑ ์ต์
}
}
/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")
);
}
}
๐ ํด๋ผ์ด์ธํธ ์ธก ๊ตฌํ (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");
}
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;
}
}
โข ๋ฏผ๊ฐํ ์ ๋ณด๋ ์ํธํํ์ฌ ์ ์กํ์ธ์
โข ์ธ์ ํ์ด์ฌํน ๋ฐฉ์ง๋ฅผ ์ํด HTTPS๋ฅผ ์ฌ์ฉํ์ธ์
โข Rate Limiting์ ๊ตฌํํ์ฌ DoS ๊ณต๊ฒฉ์ ๋ฐฉ์ดํ์ธ์
โข ์ฌ์ฉ์ ์ธ์ฆ/์ธ๊ฐ๋ฅผ ์ฒ ์ ํ ๊ฒ์ฆํ์ธ์
โก ์ฑ๋ฅ ์ต์ ํ ํ
WebSocket์ ์ฌ์ฉํ ๋ ์ฑ๋ฅ์ ์ต์ ํํ๋ ๊ฒ๋ ์ค์ํด์. ํนํ ์ฌ์ฉ์๊ฐ ๋ง์์ง์๋ก ๋์ฑ ๊ทธ๋ ์ฃ ! ๐
ํนํ ์ค์๊ฐ ๊ฒ์์ด๋ ๊ณ ๋น๋ ๋ฐ์ดํฐ ์ ์ก ์ ํจ๊ณผ์ ์ด์์!
// ๋ฉ์์ง ์์ถ ์ค์
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic", "/queue")
.setTaskScheduler(taskScheduler())
.setHeartbeatValue(new long[] {10000, 10000}); // ํํธ๋นํธ ์ค์
config.setApplicationDestinationPrefixes("/app");
config.setCacheLimit(4096); // ์บ์ ํฌ๊ธฐ ์ค์
}
@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์ด
}
}
์คํ๋ง์ ๋ด์ฅ ๋ธ๋ก์ปค๋ ๋จ์ผ ์๋ฒ ํ๊ฒฝ์ ์ ํฉํ๊ฑฐ๋ ์! ๐ฐ
// 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 ์ฌ์ฉ๋ฅ
๐จ ์ค์ ์์ : ๊ฐ๋จํ ์ฑํ ๋ฐฉ ๊ตฌํ
์ด์ ๋ฐฐ์ด ๋ด์ฉ์ ๋ชจ๋ ํ์ฉํด์ ์ค์ ๋ก ์๋ํ๋ ์ฑํ
๋ฐฉ์ ๋ง๋ค์ด๋ณผ๊ฒ์! ๐
์ด ์์ ๋ ์ฌ๋ฌ ์ฌ์ฉ์๊ฐ ๋์์ ์ฑํ
ํ ์ ์๋ ๊ณต๊ฐ ์ฑํ
๋ฐฉ์ด์์.
// ์ฑํ
๋ฐฉ ์๋น์ค
@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);
}
}
โข ์ฌ์ฉ์ ๊ด๋ฆฌ: Set์ ์ฌ์ฉํ์ฌ ์ค๋ณต ์์ด ์ฌ์ฉ์ ๊ด๋ฆฌ
โข ์ ์ฅ/ํด์ฅ ์๋ฆผ: ์์คํ ๋ฉ์์ง๋ก ๋ค๋ฅธ ์ฌ์ฉ์๋ค์๊ฒ ์๋ฆผ
โข ์ค์๊ฐ ์ธ์ ์: ๋ณ๋ ํ ํฝ์ผ๋ก ํ์ฌ ์ธ์ ์ ์ ๋ฐ์ดํธ
๐ง ํธ๋ฌ๋ธ์ํ ๊ฐ์ด๋
WebSocket ๊ฐ๋ฐํ๋ค ๋ณด๋ฉด ์ฌ๋ฌ ๋ฌธ์ ๋ฅผ ๋ง๋ ์ ์์ด์. ์์ฃผ ๋ฐ์ํ๋ ๋ฌธ์ ๋ค๊ณผ ํด๊ฒฐ ๋ฐฉ๋ฒ์ ์ ๋ฆฌํด๋ดค์ด์! ๐ ๏ธ
| ๋ฌธ์ ์ํฉ | ์์ธ | ํด๊ฒฐ ๋ฐฉ๋ฒ |
|---|---|---|
| ์ฐ๊ฒฐ์ด ์์ฃผ ๋๊น | ๋คํธ์ํฌ ๋ถ์์ , ํ์์์ | ํํธ๋นํธ ์ค์ , ์ฌ์ฐ๊ฒฐ ๋ก์ง ๊ตฌํ |
| ๋ฉ์์ง๊ฐ ์ ๋ฌ๋์ง ์์ | ํ ํฝ ๊ฒฝ๋ก ์ค๋ฅ, ๊ตฌ๋ ์คํจ | ๊ฒฝ๋ก ํ์ธ, ๋ก๊ทธ๋ก ๊ตฌ๋ ์ํ ์ฒดํฌ |
| CORS ์๋ฌ | Origin ์ค์ ๋๋ฝ | setAllowedOriginPatterns ์ค์ |
| ๋ฉ๋ชจ๋ฆฌ ๋์ | ์ฐ๊ฒฐ ํด์ ์ ๋ฆฌ์์ค ๋ฏธ์ ๋ฆฌ | disconnect ์ด๋ฒคํธ์์ ์ ๋ฆฌ ๋ก์ง |
| ๋์ ์ ์์ ์ฆ๊ฐ ์ ๋๋ ค์ง | ๋จ์ผ ์๋ฒ ํ๊ณ | ์ธ๋ถ ๋ธ๋ก์ปค ์ฌ์ฉ, ๋ก๋ ๋ฐธ๋ฐ์ฑ |
์ฌ์ฉ์๊ฐ ๋ธ๋ผ์ฐ์ ๋ฅผ ๋ซ๊ฑฐ๋ ๋คํธ์ํฌ๊ฐ ๋๊ฒผ์ ๋ ์ฒ๋ฆฌ๋ฅผ ์ ํ๋ฉด ์ข๋น ์ธ์ ์ด ๋จ์์!
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)
}
โข ๋ฉ์์ง ์ฒ๋ฆฌ๋: ์ด๋น ์ฒ๋ฆฌ ๊ฐ๋ฅํ ๋ฉ์์ง ์
โข ์๋ต ์๊ฐ: ํ๊ท , ์ต๋, 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
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์ ๋ ๊น์ด ๊ณต๋ถํ๊ณ ์ถ๋ค๋ฉด ์ด๋ฐ ์ฃผ์ ๋ค์ ์ดํด๋ณด์ธ์! ๐
โข ๋ฉ์์ง ํ: Kafka, RabbitMQ์ ํตํฉํ๊ธฐ
โข ํ๋กํ ์ฝ ๋ฒํผ: JSON ๋์ ํจ์จ์ ์ธ ๋ฐ์ด๋๋ฆฌ ํฌ๋งท ์ฌ์ฉ
โข ๋ฐฑํ๋ ์ : ๋ฉ์์ง ๊ณผ๋ถํ ์ ํ๋ฆ ์ ์ด
โข ์ค๋ฉ: ์ฑํ ๋ฐฉ์ ์ฌ๋ฌ ์๋ฒ์ ๋ถ์ฐํ๊ธฐ
โข ๋ฉ์์ง ์์์ฑ: ์คํ๋ผ์ธ ๋ฉ์์ง ์ ์ฅ ๋ฐ ์ ๋ฌ
โข Netty: ๊ณ ์ฑ๋ฅ ๋คํธ์ํฌ ์ ํ๋ฆฌ์ผ์ด์ ํ๋ ์์ํฌ
โข Spring Cloud Stream: ๋ฉ์์ง ๊ธฐ๋ฐ ๋ง์ดํฌ๋ก์๋น์ค ๊ตฌ์ถ
โข Apache Kafka: ๋์ฉ๋ ๋ฉ์์ง ์คํธ๋ฆฌ๋ฐ ํ๋ซํผ
โข Redis Pub/Sub: ๊ฐ๋จํ ๋ฉ์์ง ๋ธ๋ก์ปค๋ก ํ์ฉ
๐ญ ๋ง์น๋ฉฐ
์, ์ฌ๊ธฐ๊น์ง ์คํ๋ง WebSocket์ผ๋ก ์ค์๊ฐ ํต์ ์ ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ํจ๊ป ์์๋ดค์ด์! ๐
์ฒ์์๋ ๋ณต์กํด ๋ณด์ผ ์ ์์ง๋ง, ํ๋์ฉ ๋ฐ๋ผํ๋ค ๋ณด๋ฉด ์๊ฐ๋ณด๋ค ์ด๋ ต์ง ์๋ค๋ ๊ฑธ ๋๋ผ์ค ๊ฑฐ์์.
WebSocket์ ํ๋ ์น ์ ํ๋ฆฌ์ผ์ด์
์์ ์ ๋ง ์ค์ํ ๊ธฐ์ ์ด์์. ์ฌ์ฉ์๋ค์ ์ ์ ๋ ๋น ๋ฅด๊ณ ์ค์๊ฐ์ผ๋ก ๋ฐ์ํ๋ ์๋น์ค๋ฅผ ์ํ๊ฑฐ๋ ์! โก
์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์๋ ์ค์๊ฐ ์ฑํ
์ด๋ ์๋ฆผ ๊ธฐ๋ฅ์ ์ถ๊ฐํ๋ฉด ์ฌ์ฉ์ ๊ฒฝํ์ด ํจ์ฌ ์ข์์ง ๊ฑฐ์์.
์ฌ๋ฌ๋ถ๋ ์ด ๊ธฐ์ ์ ํ์ฉํด์ ๋ฉ์ง ์ค์๊ฐ ์๋น์ค๋ฅผ ๋ง๋ค์ด๋ณด์ธ์! ๐ช
๊ธฐ์ตํ์ธ์! ์ข์ ๊ฐ๋ฐ์๋ ๋จ์ํ ์ฝ๋๋ฅผ ์์ฑํ๋ ์ฌ๋์ด ์๋๋ผ,
์ฌ์ฉ์์ ๊ฒฝํ์ ์๊ฐํ๊ณ ์์ ์ ์ธ ์๋น์ค๋ฅผ ๋ง๋๋ ์ฌ๋์ด์์. ๐
์ค์๊ฐ ํต์ ๊ตฌํํ๋ฉด์ ๋งํ๋ ๋ถ๋ถ์ด ์๋ค๋ฉด ๊ณต์ ๋ฌธ์๋ฅผ ์ฐธ๊ณ ํ๊ฑฐ๋,
์ปค๋ฎค๋ํฐ์ ์ง๋ฌธํด๋ณด์ธ์. ๋ง์ ๊ฐ๋ฐ์๋ค์ด ๊ธฐ๊บผ์ด ๋์์ค ๊ฑฐ์์! ๐ค
๊ทธ๋ผ ์ฌ๋ฌ๋ถ์ ํ๋ก์ ํธ์ WebSocket์ ์ฑ๊ณต์ ์ผ๋ก ์ ์ฉํ์๊ธธ ๋ฐ๋ผ๋ฉฐ,
์ฆ๊ฑฐ์ด ์ฝ๋ฉ ๋์ธ์! Happy Coding! ๐โจ
๐ฏ ์ค์๊ฐ ํต์ ์ผ๋ก ๋ ๋์ ์ฌ์ฉ์ ๊ฒฝํ์ ๋ง๋ค์ด๋ณด์ธ์!
WebSocket์ ์ฌ๋ฌ๋ถ์ ์ ํ๋ฆฌ์ผ์ด์ ์ ํ ๋จ๊ณ ์ ๊ทธ๋ ์ด๋์์ผ์ค ๊ฑฐ์์ ๐
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.