Notice
Recent Posts
Recent Comments
Link
«   2026/08   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31
Tags
more
Archives
Today
Total
관리 메뉴

나의 개발 일상 기록

SockJs을 이용한 1:1 채팅 구현하기 본문

ETC

SockJs을 이용한 1:1 채팅 구현하기

느린 거북이 2021. 7. 7. 23:07

SockJS

 

1. WebSocket란?

 

WebSocket은 기존의 단방향 HTTP 프로토콜과 호환되어 양방향 통신을 제공하기 위해 개발된 프로토콜입니다. 일반 Socket통신과 달리 HTTP 80 Port를 이용하므로 방화벽에 제약이 없으며 통상 WebScoket으로 불립니다.

 

2. SockJs란?

 

SockJs는 WebSocket과 유사한 객체를 제공하는 브라우저 JavaScript 라이브러리입니다. SockJs는 브라우저 웹 서버 사이에 짧은 지연 시간, 도메인 간 통신 채널을 생성하는 일관된 브라우저 간 JavaScript API를 제공합니다.

 

 

Sping Boot WebSocket 발송된 메시지 처리

 

1. pom.xml 설정

 

pom.xml에 spring-boot-starter-websocket을 의존성 주입을 해줍니다.

<!-- websocket -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

2. Stomp

 

Stomp는 메시징 전송을 효율적으로 하기 위해 나온 프로토콜이며 기본적으로 pub/sub 구조로 메시지를 발송하고, 메시지를 받아 처리하는 부분이 확실히 정해져 있습니다.

 

pub/sub란 메시지를 공급하는 주체와 소비하는 주체를 분리하여 제공하는 메시징 방법입니다. 예를 들어 우체통(topic)이 있으면 집배원(publicher)이 신문을 우체통에 배달하는 액션이 있고 우체통에 신문이 배달되는 것을 기다렸다가 빼서 보는 구독자(subscriber)의 액션이 있습니다.

 

- 채팅방을 생성한다 : pub/sub 구현을 위한 Topic이 하나 생성됩니다.

- 채팅방에 입장한다 : Topic을 구독합니다.

- 채팅방에서 메시지를 보내고 받는다 : 해당 Topic으로 메시지를 발송하거나(pub) 메시지를 받는다(sub)

 

@Configuration
@RequiredArgsConstructor
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer{

	private final StompHandler stompHandler;
	
	
	@Override
	public void configureMessageBroker(MessageBrokerRegistry config) {
		config.enableSimpleBroker("/topic"); // sub 역할
		config.setApplicationDestinationPrefixes("/app"); // pub 역할
	}
	
	@Override
	public void registerStompEndpoints(StompEndpointRegistry registry) {
		// sockJs 사용 선언
		registry.addEndpoint("/ws").setAllowedOriginPatterns("*").withSockJS();
	}
	
	// 발송된 메시지 인터셉터
	@Override
	public void configureClientInboundChannel(ChannelRegistration registration) {
		registration.interceptors(stompHandler);
	}
	
}

 

3. StompHandler

 

 모든 메시지에 대한 처리를 StompHandler에서 인터셉터하여 다양한 상황에 맞게 로직을 구성할 수 있습니다. 

StompCommand에서는 CONNECTED, SEND, DISCONNECT 등 메시지 상태를 확인 할 수 있습니다.

 

@Log4j2
@Component
@RequiredArgsConstructor
public class StompHandler implements ChannelInterceptor{

	private final ChatDao chatDao;
	private final ChatMapper chatMapper;
	
	/**
	 * 발송된 메시지를 전처리를 진행한다.
	 */
	@Override
	public Message<?> preSend(Message<?> message, MessageChannel channel){
		StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
		
		// 연결이 끊어졌을 경우 Redis에 저장 된 message 내용 저장
		if(StompCommand.DISCONNECT == accessor.getCommand()) {
			
			String sessionId = (String) message.getHeaders().get("simpSessionId");
			List<ChatMessageDTO> messageList = chatDao.getChatMessage(sessionId);
			
			if(messageList.size() > 0) {
				chatDao.delChatMessage(sessionId);
				chatMapper.addMessageList(messageList);
			}
		}
		
		return message;
	}
}

특정 클라이언트 sessionId를 이용하여 Redis서버에 저장된 메시지 내용을 조회 후 DB에 저장합니다.

 

4. ChatController

 

StompHandler에서 리턴값으로 message를 받으므로 매개변수 값으로 사용할 수 있습니다. message의 simpSessionId값을 받아옵니다.

@RestController
@RequiredArgsConstructor
public class ChatController {

	@Autowired
	private ChatServiceImpl chatService;
	
	
	/**
	* 발송된 message를 처리한다.
	* 
	* @param chatMessage
	* @param message
	*/
	@MessageMapping("/chat/message")
	public void message(ChatMessageDTO chatMessage, Message<?> message) {
		String simpSessionId = (String) message.getHeaders().get("simpSessionId");
		chatService.insertMessage(chatMessage, simpSessionId);
	}
}

 

5. ChatServiceImpl

 

simpSessionId의 값을 구분값으로 Redis서버에 message정보를 저장합니다.

@Service
@Log4j2
@RequiredArgsConstructor
public class ChatServiceImpl implements ChatService{
	
	@Autowired
	private ChatDao chatDao;
	
	private final SimpMessageSendingOperations messagingTemplate;

	/**
	 * 발송된 message를 Redis에 저장한다.
	 */
	@Override
	public void insertMessage(ChatMessageDTO message, String simpSessionId) {
		//현재시간 set
		SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		Date time = new Date();
		message.setSentDttm(format.format(time));
		
		chatDao.addChatMessage(message, simpSessionId);
		messagingTemplate.convertAndSend("/topic/chatting" , message);
	}
}

messagingTemplate.convertAndSend("/topic/chatting" , message) 

- 클라이언트에 message정보를 전달합니다.

 

6. ChatDao

 

발송된 message정보를 simpSessionId값을 기준으로 Redis서버에서 저장, 조회 및 삭제를 합니다.

@Repository
@Log4j2
public class ChatDao {

	@Autowired
	private RedisTemplate<String, Object> redisTemplate;
	
	@Autowired
	private ObjectMapper objectMapper;
	
	@Value("${expire.chat}")
	private Long chatExpireSecond;
	
	/**
	 * 발송된 message를 Redis에 저장한다.
	 * 
	 * @param chatRoom
	 * @param key
	 * @param userId
	 */
	public void addChatMessage(ChatMessageDTO message, String simpSessionId) {
		String key = RedisKeyFactory.generateChatUserKey(simpSessionId);

		redisTemplate.watch(key);
		
		try {
			redisTemplate.multi();
			redisTemplate.opsForList().rightPush(key, message);
			redisTemplate.expire(key, chatExpireSecond, TimeUnit.SECONDS);
			
			redisTemplate.exec();
			
		}catch (Exception e) {
			redisTemplate.discard(); // 트랜잭션 종료시 unwatch()가 호출된다
		    System.out.println(e.getMessage());
		    throw e;
		}
	}
	
	/**
	 * Redis에서 message 내용을 가져온다.
	 * 
	 * @param userId
	 * @return
	 */
	public List<ChatMessageDTO> getChatMessage(String simpSessionId){
		return redisTemplate.opsForList().range(RedisKeyFactory.generateChatUserKey(simpSessionId), 0, -1)
				  .stream()
				  .map(e -> objectMapper.convertValue(e, ChatMessageDTO.class))
				  .collect(Collectors.toList());
	}
	
	/**
	 * message 내용을 Redis에서 삭제한다.
	 * 
	 * @param userId
	 */
	public void delChatMessage(String simpSessionId) {
		redisTemplate.delete(RedisKeyFactory.generateChatUserKey(simpSessionId));
	}
	
	
}

 

* 클라이언트 구성

  • sock open 및 메시지 수신
function sockOpen(){
   //웹소켓 전송시 현재 방의 번호를 넘겨서 보낸다.
   sock = new SockJS("/ws");
   ws = Stomp.over(sock);
   
   ws.connect({}, function(frame) {
       console.log('Connected: ' + frame);
       ws.subscribe('/topic/chatting', function(ChatMessageDTO) {
     	  showMessageOutput(JSON.parse(ChatMessageDTO.body));
       });
   }); 
}
  • 메시지 송신
function send() {
   var option ={
      type: "TALK",
      roomId: $("#roomId").val(),
      sender : $("#userName").val(),
      message : $("#chatting").val()
   }
   ws.send("/app/chat/message", {}, JSON.stringify(option));
   $('#chatting').val("");
}

 

Comments