T_era
Chapter 1: WebSocket과 STOMP 프로토콜 본문
핵심 내용:
- 웹소켓 프로토콜의 동작 원리 이해
- STOMP의 역할과 필요성 인지
양방향 통신 vs 지속적 연결
양방향 통신 (Full-Duplex)
정의
- 동시에 양쪽 방향으로 데이터 전송 가능
- 서버 ↔ 클라이언트가 동시에 일어날 수 있음
Java WebSocket 예시
// 서버 측 WebSocket 핸들러
@Component
public class ChatWebSocketHandler extends TextWebSocketHandler {
private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) {
sessions.put(session.getId(), session);
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
// 클라이언트 → 서버 메시지 수신 (동시에 가능)
String clientMessage = message.getPayload();
log.info("클라이언트 메시지 수신: {}", clientMessage);
// 서버 → 클라이언트 메시지 전송 (동시에 가능)
try {
session.sendMessage(new TextMessage("서버 응답: " + clientMessage));
} catch (IOException e) {
log.error("메시지 전송 실패", e);
}
}
}
// 클라이언트 측 (JavaScript)
const socket = new WebSocket('ws://localhost:8080/chat');
// 클라이언트 → 서버 (동시에 가능)
socket.send('안녕하세요!');
// 서버 → 클라이언트 (동시에 가능)
socket.onmessage = function(event) {
console.log('서버 응답:', event.data);
};
장점
- 실시간 양방향 대화 가능
- 채팅, 게임, 협업 도구 등에 적합
- 즉시 응답 가능
단점
- 복잡한 상태 관리 필요
- 동시성 문제 발생 가능
🔗 지속적 연결 (Persistent Connection)
정의
- 연결을 유지하여 재연결 오버헤드 제거
- 연결이 끊어지지 않고 계속 유지됨
Java HTTP Keep-Alive 예시
// 일반 HTTP (연결 매번 생성/해제)
@Service
public class HttpService {
private final RestTemplate restTemplate;
public HttpService() {
// 연결 풀 설정 없음 - 매번 새로운 연결
this.restTemplate = new RestTemplate();
}
public String fetchData(String url) {
// 매번 새로운 HTTP 연결 생성
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
return response.getBody();
}
}
// HTTP Keep-Alive (연결 유지)
@Configuration
public class HttpConfig {
@Bean
public RestTemplate restTemplate() {
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
// 연결 유지 설정
factory.setConnectionRequestTimeout(5000);
factory.setConnectTimeout(5000);
factory.setReadTimeout(5000);
// Keep-Alive 설정
HttpClient httpClient = HttpClients.custom()
.setConnectionManager(createConnectionManager())
.build();
factory.setHttpClient(httpClient);
return new RestTemplate(factory);
}
private PoolingHttpClientConnectionManager createConnectionManager() {
PoolingHttpClientConnectionManager manager = new PoolingHttpClientConnectionManager();
manager.setMaxTotal(100); // 최대 연결 수
manager.setDefaultMaxPerRoute(20); // 라우트당 최대 연결 수
return manager;
}
}
장점
- 연결 설정 오버헤드 감소
- 빠른 응답 시간
- 리소스 효율성
단점
- 서버 리소스 사용 (연결 유지)
- 연결 수 제한 (메모리, 파일 디스크립터)
비교표
| 구분 | 양방향 통신 | 지속적 연결 |
|---|---|---|
| 목적 | 동시 양방향 데이터 전송 | 연결 오버헤드 제거 |
| 방향성 | 양방향 동시 전송 | 단방향도 가능 |
| 사용 사례 | 채팅, 게임, 실시간 협업 | API 호출, 파일 전송 |
| 복잡도 | 높음 | 낮음 |
| 리소스 | 높음 | 중간 |
WebSocket의 경우 (Java)
WebSocket은 둘 다 제공
// 서버 측 WebSocket 설정
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(chatHandler(), "/chat")
.setAllowedOrigins("*");
}
@Bean
public WebSocketHandler chatHandler() {
return new ChatWebSocketHandler();
}
}
@Component
public class ChatWebSocketHandler extends TextWebSocketHandler {
private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) {
// 1. 지속적 연결 (Persistent) - 연결 유지
sessions.put(session.getId(), session);
log.info("연결 유지 중... 세션 ID: {}", session.getId());
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
// 2. 양방향 통신 (Full-Duplex) - 동시 양방향 전송
String clientMessage = message.getPayload();
// 클라이언트 → 서버
log.info("클라이언트 메시지: {}", clientMessage);
// 서버 → 클라이언트 (동시에 가능)
try {
session.sendMessage(new TextMessage("서버 응답: " + clientMessage));
} catch (IOException e) {
log.error("메시지 전송 실패", e);
}
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
// 연결 끊김 처리
sessions.remove(session.getId());
log.info("연결 종료: {}", session.getId());
}
}
실제 사용 예시
양방향 통신이 필요한 경우
// 채팅 애플리케이션
@Component
public class ChatService {
private final SimpMessagingTemplate messagingTemplate;
public ChatService(SimpMessagingTemplate messagingTemplate) {
this.messagingTemplate = messagingTemplate;
}
// 클라이언트에서 메시지 수신
@MessageMapping("/chat")
@SendTo("/topic/messages")
public ChatMessage handleChatMessage(ChatMessage message) {
log.info("클라이언트 메시지 수신: {}", message);
// 서버에서 즉시 응답 (양방향 통신)
return new ChatMessage("서버", "메시지 수신됨: " + message.getContent());
}
}
지속적 연결이 중요한 경우
// 실시간 알림 시스템
@Service
public class NotificationService {
private final SimpMessagingTemplate messagingTemplate;
public NotificationService(SimpMessagingTemplate messagingTemplate) {
this.messagingTemplate = messagingTemplate;
}
// 서버에서 알림 전송 (단방향이지만 연결 유지)
public void sendNotification(String userId, String message) {
messagingTemplate.convertAndSendToUser(
userId,
"/topic/notifications",
new NotificationMessage(message)
);
}
// 연결 상태 모니터링
@EventListener
public void handleSessionConnected(SessionConnectedEvent event) {
log.info("사용자 연결됨: {}", event.getUser().getName());
}
@EventListener
public void handleSessionDisconnected(SessionDisconnectEvent event) {
log.info("사용자 연결 끊김: {}", event.getUser().getName());
}
}
성능 관점
양방향 통신
// 동시성 처리가 중요
@Component
public class ConcurrentMessageHandler {
private final ExecutorService executorService = Executors.newFixedThreadPool(10);
public void handleConcurrentMessages(List<Message> messages) {
// 동시에 여러 메시지 처리
List<CompletableFuture<Void>> futures = messages.stream()
.map(message -> CompletableFuture.runAsync(() -> {
processMessage(message);
}, executorService))
.collect(Collectors.toList());
// 모든 메시지 처리 완료 대기
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
}
}
지속적 연결
// 연결 풀 관리가 중요
@Configuration
public class WebSocketConnectionConfig {
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
// 연결 유지를 위한 설정
container.setMaxTextMessageBufferSize(8192);
container.setMaxBinaryMessageBufferSize(8192);
container.setMaxSessions(10000); // 최대 연결 수 제한
container.setAsyncSendTimeout(5000);
return container;
}
}
결론
- 양방향 통신: 동시성에 초점 (채팅, 게임)
- 지속적 연결: 효율성에 초점 (API, 알림)
STOMP의 역할과 필요성
순수 WebSocket의 한계
WebSocket은 단순한 데이터 전송 통로
// 순수 WebSocket - 메시지 구조나 형식에 대한 규칙 없음
@Component
public class RawWebSocketHandler extends TextWebSocketHandler {
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
String rawMessage = message.getPayload();
// 개발자가 직접 메시지 파싱 및 라우팅 로직 구현 필요
if (rawMessage.startsWith("CHAT:")) {
handleChatMessage(session, rawMessage);
} else if (rawMessage.startsWith("NOTIFICATION:")) {
handleNotificationMessage(session, rawMessage);
} else if (rawMessage.startsWith("GAME:")) {
handleGameMessage(session, rawMessage);
}
// ... 복잡한 메시지 라우팅 로직
}
private void handleChatMessage(WebSocketSession session, String message) {
// 채팅 메시지 처리 로직
}
private void handleNotificationMessage(WebSocketSession session, String message) {
// 알림 메시지 처리 로직
}
private void handleGameMessage(WebSocketSession session, String message) {
// 게임 메시지 처리 로직
}
}
문제점
- ❌ 메시지 형식 규칙 없음: 개발자가 직접 정의해야 함
- ❌ 복잡한 라우팅 로직: 메시지 타입별 분기 처리 필요
- ❌ 구독/발행 모델 구현 어려움: 직접 구현해야 함
- ❌ 비즈니스 로직과 인프라 로직 혼재
STOMP의 역할
STOMP = WebSocket 위의 하위 프로토콜
// STOMP 사용 시 - 구조화된 메시지와 명확한 라우팅
@Controller
public class StompController {
// 채팅 메시지 처리
@MessageMapping("/chat") // 목적지: /app/chat
@SendTo("/topic/chat") // 구독자들에게 브로드캐스트
public ChatMessage handleChatMessage(ChatMessage message) {
log.info("채팅 메시지 수신: {}", message);
return message;
}
// 개인 알림 처리
@MessageMapping("/notification")
public void handleNotification(NotificationMessage message) {
// 특정 사용자에게 전송
messagingTemplate.convertAndSendToUser(
message.getUserId(),
"/topic/notifications",
message
);
}
// 게임 이벤트 처리
@MessageMapping("/game")
@SendTo("/topic/game")
public GameEvent handleGameEvent(GameEvent event) {
log.info("게임 이벤트 수신: {}", event);
return event;
}
}
STOMP 메시지 구조
// STOMP 메시지 예시
{
"command": "SEND",
"destination": "/app/chat",
"headers": {
"content-type": "application/json",
"user": "user123"
},
"body": "{\"content\":\"안녕하세요!\",\"sender\":\"user123\"}"
}
STOMP의 핵심 기능
1. Destination 기반 라우팅
// 명확한 목적지 정의
@MessageMapping("/chat") // 클라이언트 → 서버: /app/chat
@SendTo("/topic/chat") // 서버 → 클라이언트: /topic/chat
@SendToUser("/topic/notifications") // 특정 사용자에게: /user/topic/notifications
2. 구독/발행(Pub/Sub) 모델
// 클라이언트 측 구독
const stompClient = new StompJs.Client({
webSocketFactory: () => new WebSocket('ws://localhost:8080/ws')
});
// 채팅 메시지 구독
stompClient.subscribe('/topic/chat', function(message) {
const chatMessage = JSON.parse(message.body);
displayChatMessage(chatMessage);
});
// 개인 알림 구독
stompClient.subscribe('/user/topic/notifications', function(message) {
const notification = JSON.parse(message.body);
showNotification(notification);
});
// 게임 이벤트 구독
stompClient.subscribe('/topic/game', function(message) {
const gameEvent = JSON.parse(message.body);
handleGameEvent(gameEvent);
});
3. 메시지 헤더 관리
// 헤더 정보 활용
@MessageMapping("/chat")
@SendTo("/topic/chat")
public ChatMessage handleChatMessage(
@Payload ChatMessage message,
@Header("user") String user,
@Header("timestamp") String timestamp
) {
log.info("사용자 {}의 메시지: {}", user, message);
message.setTimestamp(timestamp);
return message;
}
STOMP의 필요성
1. 개발 생산성 향상
// STOMP 없이 구현 시 (복잡함)
@Component
public class ComplexWebSocketHandler extends TextWebSocketHandler {
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
String rawMessage = message.getPayload();
JsonNode jsonNode = objectMapper.readTree(rawMessage);
String type = jsonNode.get("type").asText();
String destination = jsonNode.get("destination").asText();
String payload = jsonNode.get("payload").asText();
switch (type) {
case "CHAT":
handleChatMessage(session, destination, payload);
break;
case "NOTIFICATION":
handleNotificationMessage(session, destination, payload);
break;
// ... 수많은 분기 처리
}
}
}
// STOMP 사용 시 (간단함)
@MessageMapping("/chat")
@SendTo("/topic/chat")
public ChatMessage handleChatMessage(ChatMessage message) {
return message; // 비즈니스 로직에만 집중
}
2. 표준화된 메시지 구조
// STOMP 메시지 구조
public class StompMessage {
private String command; // SEND, SUBSCRIBE, MESSAGE 등
private String destination; // /app/chat, /topic/notifications 등
private Map<String, String> headers; // 메타데이터
private String body; // 실제 데이터
}
3. 자동 라우팅
// STOMP가 자동으로 처리하는 것들
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// /app으로 시작하는 메시지는 @MessageMapping으로 라우팅
config.setApplicationDestinationPrefixes("/app");
// /topic으로 시작하는 메시지는 구독자들에게 브로드캐스트
config.enableSimpleBroker("/topic");
// /user로 시작하는 메시지는 특정 사용자에게 전송
config.setUserDestinationPrefix("/user");
}
}
�� 비교표
| 항목 | 순수 WebSocket | STOMP |
|---|---|---|
| 메시지 구조 | 개발자 정의 | 표준화된 구조 |
| 라우팅 | 직접 구현 | 자동 라우팅 |
| 구독/발행 | 직접 구현 | 내장 지원 |
| 개발 복잡도 | 높음 | 낮음 |
| 유지보수 | 어려움 | 쉬움 |
| 확장성 | 제한적 | 높음 |
실제 사용 예시
채팅 시스템
@Controller
public class ChatController {
@MessageMapping("/chat")
@SendTo("/topic/chat")
public ChatMessage handleChat(ChatMessage message) {
// 비즈니스 로직에만 집중
message.setTimestamp(LocalDateTime.now());
return message;
}
@MessageMapping("/private-chat")
public void handlePrivateChat(PrivateChatMessage message) {
// 특정 사용자에게만 전송
messagingTemplate.convertAndSendToUser(
message.getRecipientId(),
"/topic/private-chat",
message
);
}
}
알림 시스템
@Controller
public class NotificationController {
@MessageMapping("/subscribe")
public void handleSubscription(SubscriptionRequest request) {
// 구독 처리
notificationService.addSubscription(request.getUserId(), request.getProductId());
}
// 서버에서 알림 전송
@EventListener
public void handleProductEvent(ProductDiscountEvent event) {
messagingTemplate.convertAndSend("/topic/product-events", event);
}
}
결론
STOMP는 WebSocket의 한계를 보완하여:
- ✅ 구조화된 메시지 제공
- ✅ 자동 라우팅 지원
- ✅ 구독/발행 모델 내장
- ✅ 개발 생산성 향상
- ✅ 비즈니스 로직 집중 가능
'Programing > Spring' 카테고리의 다른 글
| 설문 프로젝트 모니터링 및 부하 테스트 + 성능 개선 (2) | 2025.08.07 |
|---|---|
| Spring Boot 대량 데이터 처리 성능 튜닝 (0) | 2025.07.15 |
| Spring WebSocket 알림 기능 구현 및 테스트 경험 정리 (1) | 2025.07.09 |
| RestTemplate 직접 생성 vs 빌더 생성 차이와 동작 원리 (0) | 2025.07.08 |
| 실시간 데이터 전송 방식 정리 (1) | 2025.07.01 |