채팅 서비스 기술 분석
HTTP 와 WebSocket 통신
(1) HTTP는 단방향 통신 (Request/Response)
- 단방향 구조
- Client가 요청해야만 Server가 응답
- Polling 방식으로 구현 가능하지만 → 지속적인 요청으로 서버 과부하 발생 ( Polling 방식: 클라이언트가 서버에 주기적으로 요청을 보내 업데이트를 확인하는 방식)
- 지속적인 연결 불가
- Clietn가 요청 보낼 때마다 새로운 TCP 연결 시도 및 요청 처리 후 연결을 끊음
- 매번 요청을 통해 연결을 맺고 끊어야 하므로 지속적인 통신시 성능 저하 발생
- http 메시지 무거움
- HTTP 요청은 전송 필요한 부가 정보가 담긴 헤더를 포함
- 요청마다 헤더가 매번 포함된 http 메시지 보내는 것은 비효율적
(2) WebSocket은 양방향 통신
- 양방향 통신
- Client 요청 없이도 Server → Client로 실시간 메시지 전송 가능
- 연결형 프로토콜
- 한 번 연결로 지속적인 통신 유지 가능
- 초기 1회 연결 이후엔 TCP 연결 유지하므로 매번 추가 handshake 불필요
- HTTP 대비 네트워크 오버헤드 적음
- 매 요청마다 불필요한 헤더 정보 없어 전송 단위가 작음
- 매 요청마다 인증 토큰이나 쿠키등 재전송 불필요 → 최초 한번의 연결시 HTTP 헤더를 통해 token과 같은 인증정보를 통해 인증 처리 가능
Stopm 메시지 전송 프로토콜
(1) STOMP란?
-
WebSocket 기반 위에서 동작하는 메시징 프로토콜
-
Simple Text Oriented Messaging Protocol
- 메시지를 주고받기 위한 통신 규칙이 정해져있음
- HTTP처럼 텍스트 기반의 메시지 형식을 사용해 이해하기 쉽고 구조가 명확함
-
특징
- 웹소켓 통신에서는 ws://의 url을 가진 웹소켓 프로토콜을 사용 Stomp는 명시적으로 ws:// 프로토콜 사용하는 대신 http:// 사용 (내부적으로 ws://프로토콜 사용)
- 메시지를 구분하고 특정 목적지(예: 채팅방)로 효율적으로 라우팅/분배하는 규칙을 제공
- 메시지 라우팅 및 그룹화를 통한 효율적인 메시지 분배를 돕는 역할
- STOMP는
Broker를 중간에 두어, 클라이언트가 구독한 목적지로만 메시지 전달되도록 관리 → 채팅방 등 그룹 통신 구현에 유용함
-
전체 흐름
Client 채팅방 입장 → SUBSCRIBE(/topic/roomKey}) ClientA 메시지 송신 → SEND(/topic/{roomKey}, msg) Broker 메시지 전파 → 해당 Topic 구독자(B 포함)에게 전송 ClientB 메시지 수신 ← MESSAGE(/topic/{roomKey}, msg) -
STOMP의 **표준 메시지 프레임 형식 (**Spring에서는 자동으로 처리 해줌)
프레임 설명 CONNECT 클라이언트가 서버에 연결 요청 시 사용 CONNECTED 서버가 연결 요청을 수락할 때 응답 SEND 클라이언트가 메시지를 보낼 때 사용 SUBSCRIBE 클라이언트가 특정 Topic을 구독할 때 사용 MESSAGE 서버가 구독 중인 클라이언트에게 메시지를 보낼 때 사용 DISCONNECT 연결 종료 요청
(2) WebSocket 대신 Stomp 선택 이유
- WebSocket은 저수준 API
- 메시지 송수신, 세션 관리 등을 직접 구현해야 함
- Topic, Broadcast 같은 기능은 수동 구현 필요
- STOMP는 고수준 메시징 프로토콜
- 브로커 기반 publish/subscribe 구조 제공 → 실시간 브로드캐스팅에 용이함
- 메시지 목적지(
/topic/{roomKey})만 지정하면 라우팅 자동 처리 - 프레임 구조 정의되어 있어 표준화된 메시지 송수신
- STOMP는 Spring WebSocket과 통합이 간편함
- Stomp의 표준 메시지 프레임 형식에 맞게 구현시, Spring에서 프레임 처리를 내부적으로 자동 지원하고 있음
- Spring Boot에서는 spring-websocket, spring-messaging 라이브러리를 사용하면 직접 프레임을 작성하거나 파싱할 필요 없이 추상화된 방식으로 STOMP 통신을 구현할 수 있음
(3) Stomp 예시 코드
-
StompWebSocketConfig
public class StompWebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.setApplicationDestinationPrefixes("/publish"); registry.enableSimpleBroker("/topic"); } } -
StompController- client에서 특정 publish/roomKey 형태로 메시지 발행시 MessageMapping 수신
`@SendTo("/topic/room123")` 대신 messageTemplate.convertAndSend 사용 이유
- 동적 라우팅 가능
- 경로(/topic/{roomKey})를 런타임에서 자유롭게 설정 가능
- 예: 여러 채팅방, 알림 채널, 사용자별 큐 등을 코드로 처리 가능
- 복잡한 메시징 구조(동적 토픽, 개인 전송, 조건 분기 등)에 훨씬 적합함
- SendTo는 무조건 모든 구독자에게 전송하지만, messageTemplate는 특정 사용자에게 메시지 전송 가능
- messageTemplate.convertAndSendToUser()로 1:1 메시징 구현 가능
```java
public class StompController {
private final SimpMessageSendingOperations messageTemplate;
private final ChatMsgFacadeService chatMsgFacadeService;
@MessageMapping("/{roomKey}")
public void sendMessage(@DestinationVariable String roomKey, SendChatMsgReq chatMessageDto) {
log.info(chatMessageDto.getMessage());
ChatMsgSimpleResp response = chatMsgFacadeService.createMsg(roomKey, chatMessageDto);
messageTemplate.convertAndSend("/topic/" + roomKey, response);
}
}
```
-
Client 코드
stompClient.connect({}, function(frame) { stompClient.subscribe(`/topic/${this.roomKey}`, function(messageOutput) { // 메시지 수신 처리 }); stompClient.send(`/publish/${this.roomKey}`, JSON.stringify(message)); });
다중 서버 설계
1) 서버가 1대인 경우

2) Redis pub/sub 방식

[ 메시지 흐름 ]
- 1) ClientA → 서버1 → Redis
- ClientA가 room123에 메시지 전송
- 서버1이 Redis에 room123 채널로 메시지 발행 (publish)
- 2) Redis → 모든 구독 서버(서버1, 서버2)
- Redis는 room123 채널을 구독 중인 모든 서버에 메시지를 전파 (broadcast)
room123에 메시지 전파해라!메시지를 서버들에게 보냄- 구독중인 모든 서버들은 해당 메시지를 받음**(Subscribe)**
- Redis는 room123 채널을 구독 중인 모든 서버에 메시지를 전파 (broadcast)
- 4) 서버1 → ClientA
- 서버1은 Redis로부터 다시 수신한 메시지를 ClientA에게 전송
- ClientA도 room123 구독중이므로 메시지 전파 받음 → 즉, 본인이 보낸 메시지를 화면에서 확인 가능
- 5) 서버2 → ClientB
- 서버2도 Redis로부터 같은 메시지를 수신
- room123을 구독중인 ClientB에게 메시지 전파 → ClientB는 다른 유저(ClientA)가 보낸 메시지를 수신 가능함
3) Redis Queue 방식

[ 메시지 흐름 ]
- 1) ClientA → 서버1 → Redis
- ClientA가 room123에 메시지 전송
- 메시지 받은 서버는 room123의 참여자를 조회
- 참여자 수만큼 Message Queue에 전달할 메시지를 push함
- 2) Consumer
- Message Queue에서 메시지를 1개씩 꺼냄 (ex. userC의 메시지를 꺼냈다고 가정)
- 해당 메시지를 받을 유저를 파악하고, 해당 User가 연결된 ServerIP를 Key-Value Store에서 조회
- 해당 ServerIP로 메시지를 전송함
- 3) 서버2 → ClientC
- 서버2은 수신한 메시지를 ClientC에게 전송
4) 비교 분석
(1) 구조 요약
| Redis Pub/Sub 방식 | MQ + Consumer 방식 |
|---|---|
| • Client가 Server1에 메시지 전송 (publish/123) • Server1이 Redis에 publish("room123", message) 호출 • Redis는 해당 채널을 subscribe 중인 서버들에게 브로드캐스트 • 각 서버는 받은 메시지를 자신의 room123 참여자에게 전파 |
• Server1이 메시지 수신 • room123의 참여자 목록 조회 • 각 참여자별로 메시지를 Redis List(Queue)에 enqueue • Consumer(한 대 또는 분산)가 메시지를 pop • receiverUserId → serverId 매핑 조회 (Key-Value Store, Redis) • 해당 서버로 HTTP 전송 후 WebSocket 전달 |
| 소규모 실시간 채팅이 사용 | 대규모 분산 시스템, 푸시 보완 구조 필요 시 사용 |
| - 구성 난이도 단순함 - 서버 수 증가시 Redis 부담 (확장성 떨어짐) |
- 구성 복잡 - Worker 수평 확장 가능 |
브로드 캐스트 구조 / 1:1 direct 라우팅 방식
(1) 일반적인 채팅 구조: 브로드캐스트 방식
- 채팅방 단위 브로드캐스트 방식 흐름
- 채팅방 생성:
ROOM_KEY생성 및 DB 저장 - 저 A와 B가 특정 채팅방에 참여
- 해당
ROOM_KEY구독 (Subscribe) 요청 - Spring의 SimpleBroker가 구독자 세션을 메모리에 등록함
- 내부적으로 다음과 같은 형태로 구독 정보가 관리됨
→ ex.
"/topic/{ROOM_KEY}" → ["sessionA", "sessionB"]
- 해당
- 메시지 전송
- 서버에서
/topic/{ROOM_KEY}로 메시지 전송 messageTemplate.convertAndSend("``/topic/{ROOM_KEY}``", msg)호출
- 서버에서
- 메시지 수신
- Broker가 해당 토픽을 구독 중인 모든 클라이언트에게 메시지를 자동 전달
- 해당 topic을 구독 중인 모든 클라이언트가 동일한 메시지를 수신
- 채팅방 생성:
- 이 구조는 다수의 사용자가 동일한 대화를 공유하는 환경에서는 매우 효율적이고 직관적임
(2) 상담 시나리오의 특수성 한계
- Customer ↔ Agent 1:1 상담 흐름
- Customer가 채팅을 시작
- 상담 시나리오(자동 응답 등) 제공
- 상담 요청 발생
- 상담 생성 및 Agent 배정 (채팅 모듈이 아닌 코어 모듈에서 담당)
- Agent에게 채팅 시작 이벤트 전송
- Agent가 메시지를 전송 (외부 상담 애플리케이션 사용)
- 메시지가 이벤트 형태로 코어 모듈에 전달됨
- 코어 모듈이 채팅 모듈에게 해당 메시지를 Customer에게 전달
- 상담 시나리오의 특수성
- 고객(Customer): 비회원이 대부분이며, 상담 요청 시점에만 일시적으로 접속함
- 상담원(Agent): 우리 시스템의 채팅방에 직접 들어오는 것이 아니라, 타 사의 외부 상담 애플리케이션을 사용함
- 연동 방식: 상담원의 메시지는 외부 시스템에서 이벤트 형태로 우리 백엔드에 유입됨
브로드캐스트 방식이 적합하지 않은 이유
구독자가 1명(고객)
채팅방 생성 시점의 불일치
리소스 낭비
(3) 해결책: 1:1 Direct 라우팅
- 해결 방법
- 상담원 메시지가 백엔드 서버로 들어와 고객에게 전달되는 구조라면 채팅방(RoomKey) 단위 브로드 캐스트 방식을 사용할 필요가 없다고 판단
- 대신 고객의 고유 식별자 기준 1:1 Direct 라우팅 구조 선택함 → 본질적으로 1:1 대화에 적합하며, 채팅방 개념에 의존하지 않음
- 1:1 Direct 라우팅 방식 흐름
- 고객은 자신의 고유 키를 기반으로 경로를 구독
- 비회원: 고객 세션키, 고객: 고유 UserKey
- 구독 경로 예시:
/topic/{userKey}
- 서버는 특정 사용자에게만 다이렉트로 메시지 전송
- 서버는 “채팅방”이 아닌 사용자 자체를 목적지로 삼아 메시지를 전송
convertAndSendToUser(userId, "/private/message", msg)호출
- 고객은 자신의 고유 키를 기반으로 경로를 구독
- 이 방식의 장점
- 로직의 단순화
RoomKey존재 여부와 상관없이 고객은 본인의userKey만으로 메시지를 받을 준비가 됨- 상담 배정 전 시나리오 봇과의 대화부터 상담원 연결도 포함하여 관리 가능
- 불필요한 구독 관리 로직 필요 없음
- Room 생성/삭제, 채팅방 생명주기 관리 등의 복잡도가 감소
- 참가자 관리 및 상담 종료시 정리도 단순
- 기존 시스템 구조 유지 및 외부 시스템과의 결합이 쉬움
- 상담원이 메시지를 보내고자하는 고객 UserKey만 알면 보낼 수 있어 외부 시스템과 연동 쉬움
- RoomKey 관련 로직 수정 필요 없음
- 로직의 단순화