[Framework] TIL 069 - 23.10.26

유진·2023년 10월 25일
0

07_Framework

WebSocket

  • Socket?
    A, B 양 끝 단을 이어주는 것

  • WebScoket?
    Client(여러대) <----------> Server(1대)

A, B 실시간으로 소통하고 싶다면?
'Server'가 연결해줌 == 1 : 1 채팅

A, B, C, D 실시간으로 소통하고 싶다면?
'Server'가 연결해줌 == 1 : N 채팅

  • HTTP 요청?
    html 문서 요청 <-> 응답

  • WebSocket protocol

1 : 1 채팅 (ex. 카카오톡)

pom.xml

+ Jackson-databind, Spring WebSocket, GSON 모듈 있어야함!

TestWebSocketHandler.java

package edu.kh.project.main.model.websocket;

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

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;

										// TextWebSocketHandler: text 형식(ex.채팅)으로 주고받을 때 사용하는 Handler
public class TestWebSocketHandler extends TextWebSocketHandler{

	// Set : 중복 X, 순서 X
	// synchronizedSet : 동기화된 Set 객체 반환
	private Set<WebSocketSession> sessions
		= Collections.synchronizedSet(new HashSet<>());
	
	
	// WebSocketSession : 클라이언트 - 서버 간 전이중통신을 담당하는 객체
	//					 클라이언트의 세션을 가로채서 저장하고 있음    // 세션? 접속한 클라이언트 1개당 생성
	

	// - 클라이언트와 연결이 완료되고, 통신할 준비가 되면 실행
	@Override                              // (가로 챈) 클라이언트 세션
	public void afterConnectionEstablished(WebSocketSession session) throws Exception {
	
		// 클라이언트 웹소켓 연결을 요청하면 sessions에
		// 클라이언트와 전이중통신을 담당하는 객체 WebSocketSession을 추가
		sessions.add(session);
	}

	// - 클라이언트로부터 텍스트 메세지를 받았을 때 실행
	@Override                                                  // 클라이언트가 보낸 메세지(ex. Hi)
	protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
		
		System.out.println("전달 받은 내용 : " + message.getPayload());
		
		// /testSock으로 연결된 객체를 만든 클라이언트들(sessions)에게
		// 전달받은 내용을 보냄
		for(WebSocketSession s:sessions) {
			s.sendMessage(message);
		}
		
	}

	// - 클라이언트와 연결이 종료되면 실행
	@Override
	public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
		// sessions에서 나간 클라이언트의 정보를 제거
		sessions.remove(session);
	}
	

	
	
	
}






/* WebSocket
- 브라우저와 웹서버간의 전이중통신을 지원하는 프로토콜이다
- HTML5버전부터 지원하는 기능이다.
- 자바 톰캣7버전부터 지원했으나 8버전부터 본격적으로 지원한다.
- spring4부터 웹소켓을 지원한다. 
(전이중 통신(Full Duplex): 두 대의 단말기가 데이터를 송수신하기 위해 동시에 각각 독립된 회선을 사용하는 통신 방식. 
대표적으로 전화망, 고속 데이터 통신)



WebSocketHandler 인터페이스 : 웹소켓을 위한 메소드를 지원하는 인터페이스
    -> WebSocketHandler 인터페이스를 상속받은 클래스를 이용해 웹소켓 기능을 구현


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

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


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

TextWebSocketHandler :  WebSocketHandler 인터페이스를 상속받아 구현한 텍스트 메세지 전용 웹소켓 핸들러 클래스
 
    handlerTextMessage(WebSocketSession session, TextMessage message)
    - 클라이언트로부터 '텍스트 메세지'를 받았을때 실행          ->   텍스트 == 글자
     




*/

servlet-context.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:beans="http://www.springframework.org/schema/beans"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:tx="http://www.springframework.org/schema/tx"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xmlns:websocket="http://www.springframework.org/schema/websocket"
	xsi:schemaLocation="http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket-4.3.xsd
		http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd
		http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
		http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd">

	<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
	
	<!-- Enables the Spring MVC @Controller programming model -->
	<annotation-driven />

	<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
	<resources mapping="/resources/**" location="/resources/" />

	<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
	<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<!-- 접두사 -->
		<beans:property name="prefix" value="/WEB-INF/views/" />
		<!-- 접미사 -->
		<beans:property name="suffix" value=".jsp" />
	</beans:bean>
	
	
	<!-- 
		base-package 이하에 작성된 @Component 와
		자식 어노테이션 @Controller, @Service, @Repository 이 붙어있는
		클래스를 찾아서 Bean으로 등록
	 -->
	<context:component-scan base-package="edu.kh.project" />
	
	
	
	<!-- 트랜잭션 처리 시 @Transactional 어노테이션을 사용할 예정
		
		해당 어노테이션을 인식하라는 설정이 필요함
		
		@Transactional: 클래스 또는 메서드 수행 후 트랜잭션 처리를 하라고 알려주는 어노테이션
		
	 -->
	<!-- @Transactinal 어노테이션 인식, 활성화 -->
	<tx:annotation-driven transaction-manager="transactionManager"/>
	
	<!-- AOP Proxy를 이용한 관점 제어 자동화 -->
	<aop:aspectj-autoproxy />
	
	<!-- 게시판 인터셉터 관련 부분 -->
	<interceptors>
		<interceptor>
			<!-- 인터셉터가 동작할 url 패턴 -->
			<mapping path="/**"/>
			<beans:bean id="boardTypeInterceptor" class="edu.kh.project.common.interceptor.BoardTypeInterceptor"/>
		</interceptor>
	</interceptors>
	
	<!-- 웹소켓 처리 클래스를 bean으로 등록 -->
	<beans:bean id="testHandler" 
				class="edu.kh.project.main.model.websocket.TestWebSocketHandler"/>
	
	
	<!-- 어떤 주소로 웹소켓 요청이 오면 세션을 가로챌지 지정 -->
	<websocket:handlers>
		<websocket:mapping handler="testHandler" path="/testSock"/>	
		
		<!-- 요청 클라이언트의 세션을 가로채서 WebSocketSession에 넣어주는 역할 -->
		<websocket:handshake-interceptors>
			<beans:bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor"/>
		</websocket:handshake-interceptors>
	
		<!-- SockJS를 이용한 웹소켓 연결 요청임을 명시 -->
		<websocket:sockjs/>
	</websocket:handlers>
	
	
	
</beans:beans>

main.js





const loginFrm = document.getElementById("loginFrm");

const memberEmail = document.querySelector("#loginFrm input[name='memberEmail']");
const memberPw = document.querySelector("#loginFrm input[name='memberPw']");

if(loginFrm != null){
    // 로그인 시도를 할 때
    loginFrm.addEventListener("submit", e => {

        // 이메일이 입력되지 않은 경우
        // 문자열.trim() : 문자열 좌우 공백 제거
        if(memberEmail.value.trim().length == 0){
            alert("이메일을 입력해주세요.");

            memberEmail.value = ""; // 잘못 입력된 값(공백) 제거
            memberEmail.focus(); // 이메일 input태그에 초점을 맞춤

            e.preventDefault(); // (기본이벤트 제거 : 제출 못하게하기)
            return; 
        }


        // 비밀번호가 입력되지 않은 경우
        if(memberPw.value.trim().length == 0){
            alert("비밀번호를 입력해주세요.");

            memberPw.value = ""; // 잘못 입력된 값(공백) 제거
            memberPw.focus(); // 이메일 input태그에 초점을 맞춤

            e.preventDefault(); // 제출 못하게하기
            return; 
        }


    });
}




// -----------------------------------------------

// fetch API : 웹 브라우저에서 서버로 HTTP 요청을 하게해주는 최신 인터페이스

/** 
 * fetch(url)
 * .then(response => response.json() / response.text())          // 파싱
 * .then(data => console.log(data))                              // 데이터 가공
 * .catch(error => console.log(error));
 * 
 * 첫 번째 then() 함수는 서버 요청에 대한 응답이 왔을때 실행됨
 * - 응답받은 데이터가 반환되는 값이 단순 자료형 1개면 text(),
 * 객체(Map)면 json() 으로 파싱(구문해석)한 후 다음 then() 함수로 넘겨준다.
 * 
 * 
 * 두 번째 then() 함수는 response.json()/text()으로 상황에 맞게
 * 데이터가 파싱 완료되면 실행.
 * 파싱된 데이터가 전달되며, 이 값을 로직에 맞게 가공한다.
 * 
 * 
*/

// 닉네임이 일치하는 회원의 전화번호 조회
const inputNickname = document.getElementById("inputNickname");
const btn1 = document.getElementById("btn1");
const result1 = document.getElementById("result1");

btn1.addEventListener("click", () => {

    // fetch API를 이용해서 ajax
    // GET 방식 요청 (파라미터를 쿼리스트링으로 추가)

    // Promise : 비동기 함수 호출 또는 연산이 완료되었을 때
    //         이후에 처리할 함수나 에러를 처리하기 위한
    //         함수를 설정하는 모듈
    //         -> 비동기 연산의 최종 결과 객체

    fetch("/selectMemberTel?nickname=" + inputNickname.value)
    .then( resp => resp.text() ) // 응답 객체(자료형 1일때)를 문자열 형식으로 파싱
    .then( data => {
        // 데이터 가공
        console.log(data);
        result1.innerText = data;
    })
    .catch( err => console.log(err) );

});

// fetch() API 를 이용한 POST 방식 요청

// 이메일을 입력받아 일치하는 회원의 정보를 조회
const inputEmail = document.getElementById("inputEmail");
const btn2 = document.getElementById("btn2");
const result2 = document.getElementById("result2");

btn2.addEventListener("click", () => {

    // JSON.stringify() : JS 객체 -> JSON
    // JSON.parse()     : JSON    -> JS 객체

    // JSON : Javascript 객체 문법으로, 구조화된 데이터를 표현하기 위한
    //          문자 기반의 표준 포맷이다.
    //          서버에서 클라이언트로 데이터를 전송하여 표현하거나,
    //          그 반대의 경우에 사용한다.
    
    // GET 방식
     fetch("/selectMember?email=" + inputEmail.value)
    .then( resp => resp.json() ) // 응답 객체(자료형 1일때)를 문자열 형식으로 파싱
    .then( data => {
        // 데이터 가공
        console.log(data);
        result2.innerText = JSON.stringify(data);
    })
    .catch( err => console.log(err) );
    
    /*
    // POST 방식 
    let obj = {};
    obj.email = inputEmail.value;
    
    fetch("/selectMember", { // K:V 형식으로 작성해야 함
        method : "POST",
        headers : {"Content-Type" : "application/json"},
                // 요청 보내는 자원을 명시
                // -> js 객체를 json 형식으로 만들어 파라미터로 전달
        //body : JSON.stringify({"email":inputEmail.value,"pw":inputpw.value}) // JS객체 형태 : { K : V }
        body : JSON.stringify(obj) // JS객체 형태 : { K : V }
        
        // 이렇게 보내는 방법도 가능!
        //let obj = {};
        //obj.email = inputEmail.value;
        //obj.pw = inputPw.value;
        
        //body : JSON.stringify(obj) // JS객체 형태 : { K : V }
    })
    .then(resp => resp.json()) // 응답 객체를 자바스크립트 객체 형태로
                                // 파싱하는것
    .then(member => {
        console.log(member); // javascript 객체
        result2.innerText = JSON.stringify(member);
    })
    .catch( err => console.log(err) );
    */

});


// ----------------------------------------------------------------------

// 웹소켓 테스트
// 1. SockJS 라이브러리 추가

// 2. SockJS를 이용해서 클라이언트용 웹소켓 객체 생성
let testSock = new SockJS("/testSock");

function sendMessage(name, str) {

    // 매개변수를 JS 객체에 저장
    let obj = {}; // 비어있는 객체

    obj.name = name; // 객체에 일치하는 key 가 없다면 자동으로 추가
    obj.str = str;

    // console.log(obj);

    testSock.send( JSON.stringify(obj) ); // JS객체 -> JSON

}

// 웹소켓 객체(testSock)가 서버로 부터 전달받은 메세지가 있는 경우
testSock.onmessage = e => {

    // e : 이벤트객체
    // e.data : 전달 받은 메세지(JSON) 

    let obj = JSON.parse(e.data); // JSON -> JS객체

    console.log(`보낸사람 : ${obj.name} / ${obj.str}`);

}

main.jsp

  • 꼭 script main.js 위에 SockJS 추가해주어야 함! ( 코드는 위에서 아래로 읽히기 때문 )


servlet-context.xml

mybatis-config.xml

DB

* Table 2개 생성
-- 채팅
CREATE TABLE "CHATTING_ROOM" (
	"CHATTING_NO"	NUMBER		NOT NULL,
	"CH_CREATE_DATE"	DATE	DEFAULT SYSDATE	NOT NULL,
	"OPEN_MEMBER"	NUMBER		NOT NULL,
	"PARTICIPANT"	NUMBER		NOT NULL
);
COMMENT ON COLUMN "CHATTING_ROOM"."CHATTING_NO" IS '채팅방 번호';
COMMENT ON COLUMN "CHATTING_ROOM"."CH_CREATE_DATE" IS '채팅방 생성일';
COMMENT ON COLUMN "CHATTING_ROOM"."OPEN_MEMBER" IS '개설자 회원 번호';
COMMENT ON COLUMN "CHATTING_ROOM"."PARTICIPANT" IS '참여자 회원 번호';
ALTER TABLE "CHATTING_ROOM" ADD CONSTRAINT "PK_CHATTING_ROOM" PRIMARY KEY (
	"CHATTING_NO"
);
ALTER TABLE "CHATTING_ROOM"
ADD CONSTRAINT "FK_OPEN_MEMBER"
FOREIGN KEY ("OPEN_MEMBER") REFERENCES "MEMBER";
ALTER TABLE "CHATTING_ROOM"
ADD CONSTRAINT "FK_PARTICIPANT"
FOREIGN KEY ("PARTICIPANT") REFERENCES "MEMBER";
DROP TABLE "MESSAGE";


CREATE TABLE "MESSAGE" (
	"MESSAGE_NO"	NUMBER		NOT NULL,
	"MESSAGE_CONTENT"	VARCHAR2(4000)		NOT NULL,
	"READ_FL"	CHAR	DEFAULT 'N'	NOT NULL,
	"SEND_TIME"	DATE	DEFAULT SYSDATE	NOT NULL,
	"SENDER_NO"	NUMBER		NOT NULL,
	"CHATTING_NO"	NUMBER		NOT NULL
);
COMMENT ON COLUMN "MESSAGE"."MESSAGE_NO" IS '메세지 번호';
COMMENT ON COLUMN "MESSAGE"."MESSAGE_CONTENT" IS '메세지 내용';
COMMENT ON COLUMN "MESSAGE"."READ_FL" IS '읽음 여부';
COMMENT ON COLUMN "MESSAGE"."SEND_TIME" IS '메세지 보낸 시간';
COMMENT ON COLUMN "MESSAGE"."SENDER_NO" IS '보낸 회원의 번호';
COMMENT ON COLUMN "MESSAGE"."CHATTING_NO" IS '채팅방 번호';
ALTER TABLE "MESSAGE" ADD CONSTRAINT "PK_MESSAGE" PRIMARY KEY (
	"MESSAGE_NO"
);
ALTER TABLE "MESSAGE"
ADD CONSTRAINT "FK_CHATTING_NO"
FOREIGN KEY ("CHATTING_NO") REFERENCES "CHATTING_ROOM";
ALTER TABLE "MESSAGE"
ADD CONSTRAINT "FK_SENDER_NO"
FOREIGN KEY ("SENDER_NO") REFERENCES "MEMBER";
-- 시퀀스 생성
CREATE SEQUENCE SEQ_ROOM_NO NOCACHE;
CREATE SEQUENCE SEQ_MESSAGE_NO NOCACHE;

header.jsp


  • 로그인 시, header에 채팅 탭 보임


0개의 댓글