Notice
Recent Posts
Recent Comments
Link
«   2024/07   »
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
Archives
Today
Total
관리 메뉴

KSI일기장

0515spring 웹소켓(WebSocket) 이용한 채팅(Chatting) 본문

Spring

0515spring 웹소켓(WebSocket) 이용한 채팅(Chatting)

MyDiaryYo 2023. 5. 15. 16:34

채팅 관련 테이블

 

-- 채팅

CREATE TABLE CHAT_ROOM (
	CHAT_ROOM_NO	NUMBER	PRIMARY KEY,
	TITLE	VARCHAR2(200)	NOT NULL,
	STATUS	CHAR(1)	DEFAULT 'Y' CHECK(STATUS IN('Y','N')),
	MEMBER_NO NUMBER REFERENCES MEMBER_S
);


COMMENT ON COLUMN CHAT_ROOM.CHAT_ROOM_NO IS '채팅방번호';
COMMENT ON COLUMN CHAT_ROOM.TITLE IS '채팅방제목';
COMMENT ON COLUMN CHAT_ROOM.STATUS IS '채팅방상태(정상:Y, 삭제:N)';
COMMENT ON COLUMN CHAT_ROOM.MEMBER_NO IS '회원번호(방 개설자)';


CREATE TABLE CHAT_MESSAGE (
	CM_NO	NUMBER	PRIMARY KEY,
	MESSAGE	VARCHAR2(4000)	NOT NULL,
	CREATE_DT	DATE DEFAULT SYSDATE,
	CHAT_ROOM_NO NUMBER	REFERENCES CHAT_ROOM,
	MEMBER_NO	NUMBER REFERENCES MEMBER_S
);

COMMENT ON COLUMN CHAT_MESSAGE.CM_NO IS '채팅메세지번호';
COMMENT ON COLUMN CHAT_MESSAGE.MESSAGE IS '작성한 채팅 메세지';
COMMENT ON COLUMN CHAT_MESSAGE.CREATE_DT IS '메세지 작성 시간';
COMMENT ON COLUMN CHAT_MESSAGE.CHAT_ROOM_NO IS '채팅방번호';
COMMENT ON COLUMN CHAT_MESSAGE.MEMBER_NO IS '회원번호';


CREATE TABLE CHAT_ROOM_JOIN (
	MEMBER_NO NUMBER REFERENCES MEMBER_S,
	CHAT_ROOM_NO NUMBER REFERENCES CHAT_ROOM,
    PRIMARY KEY(MEMBER_NO, CHAT_ROOM_NO)
);


COMMENT ON COLUMN CHAT_ROOM_JOIN.MEMBER_NO IS '회원번호';
COMMENT ON COLUMN CHAT_ROOM_JOIN.CHAT_ROOM_NO IS '채팅방번호';

CREATE SEQUENCE SEQ_CR_NO;
CREATE SEQUENCE SEQ_CM_NO;

COMMIT;

 

공유폴더 chatting-mapper.xml  comm_230512 다운

 

 

 

 

 

pom.xml  WebSocket 라이브러리, jackson bind 라이브러리  추가 ( <dependencies>안에 추가)

 

<dependencies>

	<!-- Spring WebSocket -->
      <!-- https://mvnrepository.com/artifact/org.springframework/spring-websocket -->
      <dependency>
         <groupId>org.springframework</groupId>
         <artifactId>spring-websocket</artifactId>
         <version>${org.springframework-version}</version>
      </dependency>


      <!-- jackson bind -->
      <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
      <dependency>
         <groupId>com.fasterxml.jackson.core</groupId>
         <artifactId>jackson-databind</artifactId>
         <version>2.9.8</version>
      </dependency>
      
  <dependencies>

 

mybatis-config.xml  채팅에 이용할 mybatis 변수명 추가

<!-- 별칭 작성 부분 -->
	<typeAliases>
		<typeAlias type="edu.kh.comm.chat.model.vo.ChatRoom" alias="chatRoom"/>
		<typeAlias type="edu.kh.comm.chat.model.vo.ChatRoomJoin" alias="chatRoomJoin"/>
		<typeAlias type="edu.kh.comm.chat.model.vo.ChatMessage" alias="chatMessage"/>
	</typeAliases>
	
	
	<!-- SQL이 작성되는 mapper파일 위치를 등록 -->
	<mappers>
		<mapper resource="/mappers/chatting-mapper.xml"/>
	</mappers>

 

 

 

 

 

 

 

servlet-context.xml WebSocket 요청시 bean등록을 위한 추가

<!-- WebSocket 요청 시 핸들러 클래스 (ChatWebsocketHandler클래스) bean등록, 연결하기 -->
	<beans:bean id="chatHandler" class="edu.kh.comm.chat.model.websocket.ChatWebsocketHandler"/>
	
	<!-- Websocket 요청(주소)를 처리할 bean 지정 -->
	<!-- <websocket:mapping handler="beans:bean의 id" path="chatRoom.jsp new SockJS(contextPath+"/chat")에서 chat"/> -->
	<websocket:handlers>
		
		<websocket:mapping handler="chatHandler" path="/chat"/>
		
		<websocket:handshake-interceptors>
		
		<beans:bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor"/>
	
		</websocket:handshake-interceptors>
		
		<!-- 
		interceptor :http통신에서 req, resp 가로채는 역할
		handshake-interceptors :요청관련 데이터 중 HttpSession(로그인정보, 채팅방번호)을 가로채서
		WebSocketSession에 넣어주는 역할
		 -->
		
		<!-- SockJs :라이브러리를 이용해 만들어진 웹소켓 객체임을 인식 -->
		<websocket:sockjs></websocket:sockjs>
	</websocket:handlers>

 

 

 

 

 

 

 

ChatMessage (VO)

package edu.kh.comm.chat.model.vo;

import java.sql.Date;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@NoArgsConstructor
@ToString
public class ChatMessage {
	private int cmNo;
	private String message;
	private Date createDate;
	private int chatRoomNo;
	private int memberNo;
	private String memberEmail;
	private String memberNickname;
}

ChatRoom (VO)

package edu.kh.comm.chat.model.vo;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@NoArgsConstructor
@ToString
public class ChatRoom {
	private int chatRoomNo;
	private String title;
	private String status;
	private int memberNo;
	private String memberNickname;
	private int cnt; // 참여자 수
}

 

ChatRoomJoin (VO)

package edu.kh.comm.chat.model.vo;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@NoArgsConstructor
@ToString
public class ChatRoomJoin {
	private int memberNo;
	private int chatRoomNo;
}

 

 

ChatWebsocketHandler

package edu.kh.comm.chat.model.websocket;

import java.sql.Date;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.Gson;

import edu.kh.comm.chat.model.service.ChatService;
import edu.kh.comm.chat.model.vo.ChatMessage;

public class ChatWebsocketHandler extends TextWebSocketHandler {
	
	@Autowired
	private ChatService service;
	
	
	/*WebSocketHandler 인터페이스 : 웹소켓을 위한 메소드를 지원하는 인터페이스
	-> WebSocketHandler 인터페이스를 상속받은 클래스를 이용해 웹소켓 기능을 구현

	WebSocketHandler 주요 메소드
        
    void handlerMessage(WebSocketSession session, WebSocketMessage message)
    - 클라이언트로부터 메세지가 도착하면 실행
    
    void afterConnectionEstablished(WebSocketSession session)
    - 클라이언트와 연결이 완료되고, 통신할 준비가 되면 실행

    void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus)
    - 클라이언트와 연결이 종료되면 실행

    void handleTransportError(WebSocketSession session, Throwable exception)
    - 메세지 전송중 에러가 발생하면 실행 

----------------------------------------------------------------------------

	TextWebSocketHandler :  WebSocketHandler 인터페이스를 상속받아 구현한 텍스트 메세지 전용 웹소켓 핸들러 클래스
 	
 	handlerTextMessage(WebSocketSession session, TextMessage message)
    - 클라이언트로부터 텍스트 메세지를 받았을때 실행
 */
	
	
	
	private Set<WebSocketSession> sessions
	= Collections.synchronizedSet(new HashSet<WebSocketSession>() );
	
	//synchronizedSet :동기화된 Set을 반환(동기 <-> 비동기(여러개 작업중 다른 작업을 기다리지 않고 수행))
	//->멀티스레드 환경에서 하나의 컬렉션 요소에 여러 스레드가 접근하면 충돌이 발생할 수 있으므로
	//동기화를 진행한다(충돌이 안나도록 줄세우는 것)
	
	
	/*Set<WebSocketSession> 을 만든 이유
	 * 
	 * -WebSocketSession == 웹소켓에 연결된 클라이언트의 세션
	 * ->세션을 통해서 누가 연결했는지 알 수 있다
	 * 
	 * WebSocketSession 을 모아두기 위해 Set을 사용해 만들었다 
	 * -> 현재 웹소켓에 연결되있는 모든 클라이언트를 알 수 있다
	 * ->Set을 분석해 원하는 클라이언트를 찾아서 메시지(채팅)을 전달할 수 있다
	 * 
	 */
	
	
	//클라이언트와 연결 완료되고, 통신할 준비가 되면 수행( new SockJS() )
	@Override
	public void afterConnectionEstablished(WebSocketSession session) throws Exception {
		
		//WebSocketSession :웹소켓에 접속/요청한 클라이언트 세션
		System.out.println(session.getId() + "연결됨"); 	//세션 아이디 확인
		
		sessions.add(session);	//WebSocketSession을 set에 추가
		
	}

	
	//클라이언트로 부터 텍스트메시지를 전달 받았을 때 수행
	@Override
	protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
		// -> TextMessage :웹소켓을 이용해 텍스트로 전달된 메세지가 담겨있는 객체
		
		//payLoad :전송되는 데이터
		//message.getPayload() : JSON 형태
		
		System.out.println( "전달된 메세지: " + message.getPayload() );
		
		//Jackson 라이브러리 : Java에서 JSON을 다루기 위한 라이브러리
		
		//Jackson-datebind 라이브러리 : ObjectMapper 객체를 이용해서 
		//								JSON데이터를 특정 VO필드에 맞게 자동 매핑되게 해준다
		
		ObjectMapper objectMapper = new ObjectMapper();
		
		/* 메세지 입력 시 필요한 데이터를 js객체로 생성
		const chatMessage = {
			"memberNo" : memberNo,
			"memberEmail" : memberEmail,
			"memberNickname" : memberNickname,
			"chatRoomNo" : chatRoomNo,
			"message" : inputChatting.value
		};
		 * 
		 */

		ChatMessage chatMessage = objectMapper.readValue(message.getPayload(), ChatMessage.class);
		
		//(new Date(System.currentTimeMillis()))
		
		//createDate (시간)세팅
		chatMessage.setCreateDate( new Date(System.currentTimeMillis()) );
		
		System.out.println(chatMessage);
		
		int result = service.insertMessage(chatMessage);
		
		if(result>0) {
			
			//같은 방에 접속중인 클라이언트에게만 메세지 보내지
			//-> Set<WebSocketSession>에서 같은 방 클라이언트만 골라내기
			
			for(WebSocketSession s : sessions) {
				
				//WebSocketSession == HttpSession(로그인저옵, 채팅방번호)를 가로챈 것
				int chatRoomNo = (Integer)s.getAttributes().get("chatRoomNo");
				
				//WebSocketSession에 담겨있는 채팅방 번호와
				//TextMessage message에 담겨있는 채팅방 번호가 같을경우 == 같은방 클라이언트라는 의미
				if(chatRoomNo == chatMessage.getChatRoomNo()) {
					//같은방 클라이언트에게 JSON형식의 메세지를 보낸다
					s.sendMessage(new TextMessage( new Gson().toJson(chatMessage) ) );
					//-> chat.js의 88번째 줄(에서 받아서 그 밑에 실행
				}
			}
		}
	}

	
	//클라이언트와 연결이 종료되면 수행
	@Override
	public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
	
		sessions.remove(session);	//웹소켓 연결이 종료되는 경우 WebSocketSession을 set에서 제거
		
	}
	
	
	

}

 

ChatService

package edu.kh.comm.chat.model.service;

import java.util.List;

import edu.kh.comm.chat.model.vo.ChatMessage;
import edu.kh.comm.chat.model.vo.ChatRoom;
import edu.kh.comm.chat.model.vo.ChatRoomJoin;

public interface ChatService {

	/** 채팅 목록 조회
	 * @return chatRoomList
	 */
	List<ChatRoom> selectChatRoomList();

	/** 채팅방 만들기
	 * @param room
	 * @return chatRoomNo
	 */
	int openChatRoom(ChatRoom room);

	
	/** 채팅방 입장 + 채팅 메세지 목록 조회
	 * @param join
	 * @return list
	 */
	List<ChatMessage> joinChatRoom(ChatRoomJoin join);

	
	/** 채팅 메세지 삽입
	 * @param cm
	 * @return result
	 */
	int insertMessage(ChatMessage cm);

	/** 채팅방 나가기
	 * @param join
	 * @return result
	 */
	int exitChatRoom(ChatRoomJoin join);

}

 

ChatServiceImpl

package edu.kh.comm.chat.model.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import edu.kh.comm.chat.model.dao.ChatDAO;
import edu.kh.comm.chat.model.vo.ChatMessage;
import edu.kh.comm.chat.model.vo.ChatRoom;
import edu.kh.comm.chat.model.vo.ChatRoomJoin;
import edu.kh.comm.common.Util;

@Service
public class ChatServiceImpl implements ChatService{

	@Autowired
	private ChatDAO dao;

	// 채팅 목록 조회
	@Override
	public List<ChatRoom> selectChatRoomList() {
		return dao.selectChatRoomList();
	}

	// 채팅방 만들기
	@Override
	public int openChatRoom(ChatRoom room) {
		return dao.openChatRoom(room);
	}

	
	// 채팅방 입장 + 내용 얻어오기
	@Override
	public List<ChatMessage> joinChatRoom(ChatRoomJoin join) {

		// 현재 회원이 해당 채팅방에 참여하고 있는지 확인
		int result = dao.joinCheck(join);
		
		if(result == 0) { // 참여하고 있지 않은 경우 참여
			dao.joinChatRoom(join);
		}
		
		// 채팅 메세지 목록 조회
		return dao.selectChatMessage(join.getChatRoomNo());
	}

	
	// 채팅 메세지 삽입
	@Override
	public int insertMessage(ChatMessage cm) {
		
//		cm.setMessage(Util.XSSHandling(cm.getMessage()));
		cm.setMessage(Util.newLineHandling(cm.getMessage()));
		
		return dao.insertMessage(cm);
	}

	
	// 채팅방 나가기
	@Transactional(rollbackFor = Exception.class)
	@Override
	public int exitChatRoom(ChatRoomJoin join) {
		
		// 채팅방 나가기
		int result = dao.exitChatRoom(join);
		
		if(result > 0) { // 채팅방 나가기 성공 시
			
			// 현재 방에 몇명이 있나 확인
			int cnt = dao.countChatRoomMember(join.getChatRoomNo());
			
			// 0명일 경우 방 닫기
			if(cnt == 0) {
				result = dao.closeChatRoom(join.getChatRoomNo());
			}
			
		}
		
		return result;
	}
	
}

 

ChatDAO

package edu.kh.comm.chat.model.dao;

import java.util.List;

import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import edu.kh.comm.chat.model.vo.ChatMessage;
import edu.kh.comm.chat.model.vo.ChatRoom;
import edu.kh.comm.chat.model.vo.ChatRoomJoin;

@Repository
public class ChatDAO {

	@Autowired
	private SqlSessionTemplate sqlSession;

	/** 채팅방 목록 조회
	 * @return  chatRoomList
	 */
	public List<ChatRoom> selectChatRoomList() {
		return sqlSession.selectList("chattingMapper.selectChatRoomList");
	}

	/** 채팅방 만들기
	 * @param room
	 * @return chatRoomNo
	 */
	public int openChatRoom(ChatRoom room) {
		
		int result = sqlSession.insert("chattingMapper.openChatRoom", room);
		
		if(result > 0) return room.getChatRoomNo();
		return 0; 
	}

	
	/** 채팅방 참여 여부 확인
	 * @param join
	 * @return result
	 */
	public int joinCheck(ChatRoomJoin join) {
		return sqlSession.selectOne("chattingMapper.joinCheck", join);
	}
	
	
	/** 채팅방 참여하기
	 * @param join
	 */
	public void joinChatRoom(ChatRoomJoin join) {
		sqlSession.insert("chattingMapper.joinChatRoom", join);
	}

	
	/** 채팅 메세지 목록 조회
	 * @param chatRoomNo
	 * @return list
	 */
	public List<ChatMessage> selectChatMessage(int chatRoomNo) {
		return sqlSession.selectList("chattingMapper.selectChatMessage", chatRoomNo);
	}

	/** 채팅 메세지 삽입
	 * @param cm
	 * @return result
	 */
	public int insertMessage(ChatMessage cm) {
		return sqlSession.insert("chattingMapper.insertMessage", cm);
	}

	/** 채팅방 나가기
	 * @param join
	 * @return result
	 */
	public int exitChatRoom(ChatRoomJoin join) {
		return sqlSession.delete("chattingMapper.exitChatRoom", join);
	}

	/** 채팅방 인원 수 확인
	 * @param chatRoomNo
	 * @return cnt
	 */ 
	public int countChatRoomMember(int chatRoomNo) {
		return sqlSession.selectOne("chattingMapper.countChatRoomMember", chatRoomNo);
	}

	/** 채팅방 닫기
	 * @param chatRoomNo
	 * @return result
	 */
	public int closeChatRoom(int chatRoomNo) {
		return sqlSession.update("chattingMapper.closeChatRoom", chatRoomNo);
	}

	
	
}

 

chatting-mapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="chattingMapper">

	<resultMap type="chatRoom" id="chatroom_rm">
		<id property="chatRoomNo" column="CHAT_ROOM_NO" />

		<result property="title" column="TITLE" />
		<result property="status" column="STATUS" />
		<result property="memberNo" column="MEMBER_NO" />
		<result property="memberNickname" column="MEMBER_NICK" />
		<result property="cnt" column="CNT" />
	</resultMap>

	<resultMap type="chatMessage" id="chatMessage_rm">
		<id property="cmNo" column="CM_NO" />

		<result property="message" column="MESSAGE" />
		<result property="createDate" column="CREATE_DT" />
		<result property="chatRoomNo" column="CHAT_ROOM_NO" />

		<result property="memberNo" column="MEMBER_NO" />
		<result property="memberNickname" column="MEMBER_NICK" />
	</resultMap>

	<!--=========================================================================================-->

	<!-- 채팅방 목록 조회 -->
	<select id="selectChatRoomList" resultMap="chatroom_rm">
		SELECT CHAT_ROOM_NO, TITLE, MEMBER_NICK,
		(SELECT COUNT(*) FROM CHAT_ROOM_JOIN B WHERE A.CHAT_ROOM_NO = B.CHAT_ROOM_NO) CNT
		FROM CHAT_ROOM A
		JOIN MEMBER_S USING(MEMBER_NO)
		WHERE STATUS = 'Y'
		ORDER BY CHAT_ROOM_NO DESC
	</select>

	
	<!-- 채팅방 만들기 -->
	<insert id="openChatRoom" useGeneratedKeys="true">
	
		<selectKey keyProperty="chatRoomNo" resultType="_int" order="BEFORE">
			SELECT SEQ_CR_NO.NEXTVAL FROM DUAL
		</selectKey>
		
		
		INSERT INTO CHAT_ROOM VALUES
		(#{chatRoomNo}, #{title}, DEFAULT, #{memberNo})
	</insert>


	<!-- 채팅방 참여 여부 확인 -->
	<select id="joinCheck" resultType="_int">
		SELECT COUNT(*) FROM CHAT_ROOM_JOIN
		WHERE CHAT_ROOM_NO = #{chatRoomNo}
		AND MEMBER_NO = #{memberNo}
	</select>

	<!-- 채팅방 참여하기 -->
	<insert id="joinChatRoom">
		INSERT INTO CHAT_ROOM_JOIN
		VALUES(#{memberNo}, #{chatRoomNo})
	</insert>

	<!-- 채팅 메세지 목록 조회 -->
	<select id="selectChatMessage" resultMap="chatMessage_rm">
		SELECT MESSAGE, CREATE_DT, MEMBER_NO, MEMBER_NICK
		FROM CHAT_MESSAGE
		JOIN MEMBER_S USING(MEMBER_NO)
		WHERE CHAT_ROOM_NO = #{chatRoomNo}
		ORDER BY CM_NO
	</select>


	<!-- 채팅 메세지 삽입 -->
	<insert id="insertMessage">
		INSERT INTO CHAT_MESSAGE
		VALUES(SEQ_CM_NO.NEXTVAL, #{message}, DEFAULT, #{chatRoomNo}, #{memberNo})
	</insert>
	

	<!-- 채팅방 나가기 -->
	<delete id="exitChatRoom">
		DELETE FROM CHAT_ROOM_JOIN
		WHERE MEMBER_NO = #{memberNo}
		AND CHAT_ROOM_NO = #{chatRoomNo}
	</delete>

	<!-- 채팅방 인원 수 확인 -->
	<select id="countChatRoomMember" resultType="_int">
		SELECT COUNT(*) FROM CHAT_ROOM_JOIN
		WHERE CHAT_ROOM_NO = #{chatRoomNo}
	</select>

	<!-- 채팅방 닫기 -->
	<update id="closeChatRoom">
		UPDATE CHAT_ROOM SET
		STATUS = 'N'
		WHERE CHAT_ROOM_NO = #{chatRoomNo}
	</update>

</mapper>