본문 바로가기
Spring

[스프링부트] 9. 웹소켓

by moca7 2024. 11. 6.

 

 

 

 

ㅁ 

 

 

- 채팅 웹소켓

- 핸들러 패키지의 chatendchcoghandler 복사해온다.

 

 

 
package com.br.boot.handler;

import java.util.ArrayList;
import java.util.List;

import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import com.br.boot.dto.MemberDto;

import lombok.extern.slf4j.Slf4j;

@Slf4j // 로그를 출력해보기 위해 롬복의 @Slf4j 어노테이션 작성.
public class ChatEchoHandler extends TextWebSocketHandler {

  // 웹소켓 세션 객체(클라이언트)들을 저장하는 리스트
  private List<WebSocketSession> sessionList = new ArrayList<>();
 
 
  /**
   * 1) afterConnectionEstablished : 웹소켓에 클라이언트가 연결되었을 때 처리할 내용 정의
   *
   * @param session - 현재 웹소켓과 연결된 클라이언트 객체 (즉, 채팅방에 접속된 클라이언트)
   * // param이 매개변수에 대한 설명을 작성하는 키워드다.
   */
  @Override
  public void afterConnectionEstablished(WebSocketSession session) throws Exception {
    // 이 session이 바로 이 웹소켓과 연결된 클라이언트 객체다.
    // 다수의 클라이언트들이 연결될거고 각각의 클라이언트들 마다 고유한 id 정보가 session에 담겨있을 것이다.
   
    /*
    log.debug("====== websocket 연결됨 =====");
    log.debug("WebSocketSession 객체: {}", session);
    log.debug("session id: {}", session.getId());
    log.debug("session Attributes 목록: {}", session.getAttributes()); // {sessionId=xxxx, loginUser=MemberDto객체}
    log.debug("현재 채팅방에 참가한 로그인한 회원: {}", session.getAttributes().get("loginUser")); // MemberDto 객체 뽑기
    */
   
    sessionList.add(session);
   
    for(WebSocketSession sss : sessionList) {
      String msg = "entry|" + ((MemberDto)session.getAttributes().get("loginUser")).getUserId() + "님이 입장하였습니다.";
      sss.sendMessage(new TextMessage(msg));
    }
   
  }
 
 
 

  /**
   * 2) handleMessage : 웹소켓으로 데이터(텍스트, 파일 등)가 전송되었을 경우 처리할 내용 정의
   *
   * @param session - 현재 웹소켓으로 데이터를 전송한 클라이언트 객체
   * @param message - 전송된 데이터에 대한 정보를 가지고 있는 객체
   */
  @Override
  public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
   
    /*
    log.debug("====== 메세지 들어옴 =====");
    log.debug("WebSocketSession 객체: {}", session);
    log.debug("WebSocketMessage 객체: {}", message);
    log.debug("메세지 내용: {}", message.getPayload());
    */
   
    // 현재 해당 웹소켓에 연결되어있는 모든 클라이언트들(작성자본인포함)에게 현재 들어온 메세지 재발송
    for(WebSocketSession sss : sessionList) {
     
      // 메세지유형(chat/entry/exit) | 채팅방에띄워주고자하는메세지내용 | 발신자아이디 | ...(프로필이미지경로 등) <- 나중에 |으로 split.
      String msg = "chat|" + message.getPayload() + "|" + ((MemberDto)session.getAttributes().get("loginUser")).getUserId();
      sss.sendMessage(new TextMessage(msg)); // 웹소켓에서 클라이언트로 메세지를 보냄. 그냥 보내면 안되고 TextMessage 객체로 보내야 한다.
      // room.jsp에서 onMesage 함수가 자동 실행
     
    }
   
    // 특정 회원과 채팅방을 만든다거나 해당 회원과 나눈 채팅 내역을 보존하려면 메세지를 발송할 때 마다 db에 기록해야 한다.
    // 그때 실행되는 메소드가 handleMessage 메소드다.
    // insert 해주는 서비스측 메소드를 여기서 실행시키면 된다.
    // 해당 클래스에 Service 클래스를 DI(의존성주입)해서 채팅메세지를 insert하는 메소드를 여기서 실행하면 됨.
  }
 
 
 
 
   
  /**
   * 3) afterConnectionClosed : 웹소켓에 클라이언트가 연결이 끊겼을 때 처리할 내용 정의
   */
  @Override
  public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
   
    /*
    log.debug("====== websocket 연결됨 =====");
    log.debug("WebSocketSession 객체: {}", session);
    log.debug("session id: {}", session.getId());
    log.debug("현재 채팅방에서 나간 회원: {}", session.getAttributes().get("loginUser")); // MemberDto 객체 뽑기
    */
   
    sessionList.remove(session);
   
    for(WebSocketSession sss : sessionList) {
      String msg = "exit|" + ((MemberDto)session.getAttributes().get("loginUser")).getUserId() + "님이 퇴장하였습니다.";
      sss.sendMessage(new TextMessage(msg));
    }
  }
 
}
 

 

 

 

- import해도 빨간줄 뜬다.

그말은 얘네들을 제공하는 라이브러리들이 연결되어있지 않다는 거다.

- 기존에 웹소켓 할때도 라이브리를 추가했었다.

- 그리고 에코핸들러 클래스를 만들고나서 웹소켓 관련 등록구문을 작성했었다.

 

 

 

 

- 패키지 익스플로러에서 프로젝트 우클릭 - 스프링 - add starters

- websocket 검색해서 하나 나온거 체크하고 next하고 pom.xml체크하고 finish.

 

 

 

 

 

 

 

ㅁ pom.xml

 

 
<?xml version="1.0" encoding="UTF-8"?>
 
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.11</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
 
  <groupId>com.br</groupId>
  <artifactId>boot</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>war</packaging>
  <name>boot</name>
  <description>Demo project for Spring Boot</description>
 
  <url/>
  <licenses>
    <license/>
  </licenses>
  <developers>
    <developer/>
  </developers>
  <scm>
    <connection/>
    <developerConnection/>
    <tag/>
    <url/>
  </scm>
 
  <properties>
    <java.version>17</java.version>
  </properties>
 
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-devtools</artifactId>
      <scope>runtime</scope>
      <optional>true</optional>
    </dependency>

    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-tomcat</artifactId>
      <scope>provided</scope>
    </dependency>
   
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
   
    <!-- jsp 사용을 위한 jasper 라이브러리 -->
    <dependency>
      <groupId>org.apache.tomcat.embed</groupId>
      <artifactId>tomcat-embed-jasper</artifactId>
      <!--<version>11.0.0</version>-->
    </dependency>
   
    <!-- jstl 사용을 위한 라이브러리들 -->
    <dependency>
      <groupId>jakarta.servlet</groupId>
      <artifactId>jakarta.servlet-api</artifactId>
      <!--<version>6.1.0</version>-->
      <!--<scope>provided</scope>-->
    </dependency>

    <dependency>
        <groupId>jakarta.servlet.jsp.jstl</groupId>
        <artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
        <!--<version>3.0.2</version>-->
    </dependency>
   
    <dependency>
        <groupId>org.glassfish.web</groupId>
        <artifactId>jakarta.servlet.jsp.jstl</artifactId>
        <!--<version>3.0.1</version>-->
    </dependency>
   
   
    <!-- BCryptPasswordEncoder 사용을 위한 라이브러리 -->
    <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-core</artifactId>
      <!--<version>5.7.5</version>-->
    </dependency>
   
    <!-- MyBatis, Oralce 관련 라이브러리-->
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>3.0.3</version>
    </dependency>
    <dependency>
      <groupId>com.oracle.database.jdbc</groupId>
      <artifactId>ojdbc11</artifactId>
      <scope>runtime</scope>
    </dependency>
   
    <!-- log4jdbc 라이브러리 -->
    <dependency>
      <groupId>org.bgee.log4jdbc-log4j2</groupId>
      <artifactId>log4jdbc-log4j2-jdbc4.1</artifactId>
      <version>1.16</version>
    </dependency>
   
   
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter-test</artifactId>
      <version>3.0.3</version>
      <scope>test</scope>
    </dependency>
   
    <!-- 웹소켓 관련 starter 라이브러리 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>
   
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
          <excludes>
            <exclude>
              <groupId>org.projectlombok</groupId>
              <artifactId>lombok</artifactId>
            </exclude>
          </excludes>
        </configuration>
      </plugin>
    </plugins>
  </build>

</project>
 

 

- 웹소켓 관련한 dependency가 하나 추가된걸 볼수 있다

starter가 붙은건 집약형태다.

 

 

 

 

 

ㅁ (스프링) servlet.contxet.xml

 

 
<?xml version="1.0" encoding="UTF-8"?>

  <!-- 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/" />
  <resources mapping="/upload/**" location="file:///upload/" />

  <!-- 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>
 
  <context:component-scan base-package="com.br.spring" />
 
 
  <!-- webSocket 관련 등록 구문 -->
  <beans:bean class="com.br.spring.handler.ChatEchoHandler" id="chatEchoHandler" />
  <websocket:handlers>
    <websocket:mapping handler="chatEchoHandler" path="/chat" />
    <websocket:handshake-interceptors>
      <beans:bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor" />
    </websocket:handshake-interceptors>
    <websocket:sockjs />
  </websocket:handlers>
 
 
  <!-- interceptor 관련 등록 구문 -->
  <interceptors>
    <interceptor>
      <mapping path="/member/myinfo.do" />
      <mapping path="/board/regist.do" />
           
      <beans:bean class="com.br.spring.interceptor.LoginCheckInterceptor" id="LoginCheckInterceptor"/>
    </interceptor>
   
   
    <!--
    <interceptor>
   
    </interceptor>
    -->
  </interceptors>
 
 
  <!-- Scheduler -->
  <task:annotation-driven />
 
</beans:beans>
 

 

 

- 스프링을 쓸 때는 servlet-context.xml에

- <beans:bean class="com.br.spring.handler.ChatEchoHandler" id="chatEchoHandler" />

ChatEchoHandler가 빈으로 등록되어있어야 하고,

- <websocket:mapping handler="chatEchoHandler" path="/chat" />

어떤 url요청시 어떤 핸들러를 구동시킬건지 작성되어있어야 하고

-<websocket:handshake-interceptors>

handshake ineterceptors를 등록했고

- <websocket:sockjs />

sockjs 사용을 위한 구문을 작성했었다.

 

 

 

 

 

 

ㅁ ChatEchoHandler

 

 
package com.br.boot.handler;

import java.util.ArrayList;
import java.util.List;

import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import com.br.boot.dto.MemberDto;

import lombok.extern.slf4j.Slf4j;

@Component // serlvet-context.xml에 빈등록 구문 작성 대체.
@Slf4j // 로그를 출력해보기 위해 롬복의 @Slf4j 어노테이션 작성.
public class ChatEchoHandler extends TextWebSocketHandler {

  // 웹소켓 세션 객체(클라이언트)들을 저장하는 리스트
  private List<WebSocketSession> sessionList = new ArrayList<>();
 
 
  /**
   * 1) afterConnectionEstablished : 웹소켓에 클라이언트가 연결되었을 때 처리할 내용 정의
   *
   * @param session - 현재 웹소켓과 연결된 클라이언트 객체 (즉, 채팅방에 접속된 클라이언트)
   * // param이 매개변수에 대한 설명을 작성하는 키워드다.
   */
  @Override
  public void afterConnectionEstablished(WebSocketSession session) throws Exception {
    // 이 session이 바로 이 웹소켓과 연결된 클라이언트 객체다.
    // 다수의 클라이언트들이 연결될거고 각각의 클라이언트들 마다 고유한 id 정보가 session에 담겨있을 것이다.
   
    /*
    log.debug("====== websocket 연결됨 =====");
    log.debug("WebSocketSession 객체: {}", session);
    log.debug("session id: {}", session.getId());
    log.debug("session Attributes 목록: {}", session.getAttributes()); // {sessionId=xxxx, loginUser=MemberDto객체}
    log.debug("현재 채팅방에 참가한 로그인한 회원: {}", session.getAttributes().get("loginUser")); // MemberDto 객체 뽑기
    */
   
    sessionList.add(session);
   
    for(WebSocketSession sss : sessionList) {
      String msg = "entry|" + ((MemberDto)session.getAttributes().get("loginUser")).getUserId() + "님이 입장하였습니다.";
      sss.sendMessage(new TextMessage(msg));
    }
   
  }
 
 
 

  /**
   * 2) handleMessage : 웹소켓으로 데이터(텍스트, 파일 등)가 전송되었을 경우 처리할 내용 정의
   *
   * @param session - 현재 웹소켓으로 데이터를 전송한 클라이언트 객체
   * @param message - 전송된 데이터에 대한 정보를 가지고 있는 객체
   */
  @Override
  public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
   
    /*
    log.debug("====== 메세지 들어옴 =====");
    log.debug("WebSocketSession 객체: {}", session);
    log.debug("WebSocketMessage 객체: {}", message);
    log.debug("메세지 내용: {}", message.getPayload());
    */
   
    // 현재 해당 웹소켓에 연결되어있는 모든 클라이언트들(작성자본인포함)에게 현재 들어온 메세지 재발송
    for(WebSocketSession sss : sessionList) {
     
      // 메세지유형(chat/entry/exit) | 채팅방에띄워주고자하는메세지내용 | 발신자아이디 | ...(프로필이미지경로 등) <- 나중에 |으로 split.
      String msg = "chat|" + message.getPayload() + "|" + ((MemberDto)session.getAttributes().get("loginUser")).getUserId();
      sss.sendMessage(new TextMessage(msg)); // 웹소켓에서 클라이언트로 메세지를 보냄. 그냥 보내면 안되고 TextMessage 객체로 보내야 한다.
      // room.jsp에서 onMesage 함수가 자동 실행
     
    }
   
    // 특정 회원과 채팅방을 만든다거나 해당 회원과 나눈 채팅 내역을 보존하려면 메세지를 발송할 때 마다 db에 기록해야 한다.
    // 그때 실행되는 메소드가 handleMessage 메소드다.
    // insert 해주는 서비스측 메소드를 여기서 실행시키면 된다.
    // 해당 클래스에 Service 클래스를 DI(의존성주입)해서 채팅메세지를 insert하는 메소드를 여기서 실행하면 됨.
  }
 
 
 
 
   
  /**
   * 3) afterConnectionClosed : 웹소켓에 클라이언트가 연결이 끊겼을 때 처리할 내용 정의
   */
  @Override
  public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
   
    /*
    log.debug("====== websocket 연결됨 =====");
    log.debug("WebSocketSession 객체: {}", session);
    log.debug("session id: {}", session.getId());
    log.debug("현재 채팅방에서 나간 회원: {}", session.getAttributes().get("loginUser")); // MemberDto 객체 뽑기
    */
   
    sessionList.remove(session);
   
    for(WebSocketSession sss : sessionList) {
      String msg = "exit|" + ((MemberDto)session.getAttributes().get("loginUser")).getUserId() + "님이 퇴장하였습니다.";
      sss.sendMessage(new TextMessage(msg));
    }
  }
 
}
 

 

 

- 클래스 위에 @Component 어노테이션을 붙여서 serlvet-context.xml에 빈등록 구문 작성을 대체한다.

- 스프링 부트에는 xml 파일을 따로 안둬서 빈 등록하는 3방법 중 xml 파일로 빈을 등록하는 방법은 못 쓴다.

자바 방식으로 빈을 등록한다.

 

 

 

 

 

ㅁ com.br.boot.config에 WebSocketConfig (일반) 클래스 생성

 

 
package com.br.boot.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;

import com.br.boot.handler.ChatEchoHandler;

import lombok.RequiredArgsConstructor;

@EnableWebSocket
@RequiredArgsConstructor
@Configuration
public class WebSocketConfig implements WebSocketConfigurer{
 
  private final ChatEchoHandler chatEchoHandler;
 

  /*
    <websocket:handlers>
      <websocket:mapping handler="chatEchoHandler" path="/chat" />
      <websocket:handshake-interceptors>
        <beans:bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor" />
      </websocket:handshake-interceptors>
      <websocket:sockjs />
    </websocket:handlers>
  */
 
 
  @Bean
  HttpSessionHandshakeInterceptor HttpSessionHandshakeInterceptor() {
    return new HttpSessionHandshakeInterceptor();
  }
 
 
  @Override
  public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
    registry.addHandler(chatEchoHandler, "/chat")
        .addInterceptors(HttpSessionHandshakeInterceptor()) // 메소드 호출구문을 쓰면 저 객체가 생성되어서 온다. 이 객체를 인터셉터로 등록한다.
        .withSockJS(); // <websocket:sockjs /> 대체
  }

}
 

 

 

- (1) implements WebSocketConfigurer한다.

- (2) @Configuration 어노테이션을 붙인다.

- (3) 빨간줄 뜬다. 오버라이딩 하라고 뜬다. 오버라이딩 한다.

- (4) private final ChatEchoHandler chatEchoHandler; 선언

- (5) @RequiredArgsConstructor 어노테이션을 붙여서 주입한다.

- 오버라이딩한 비어있는 메소드에 내용 작성.

 

 

 

- <beans:bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor" />

근데 이걸 빈으로 등록했었어야 했다.

근데 이건 우리가 만든 클래스가 아니라서 xml방식이나 자바 방식만 가능하다.

- 스프링 부트에 xml 파일은 별도로 없기 때문에 자바 방식으로 얘를 빈 등록 한다.

웹소켓 관련 빈이기 때문에 WebSocketConfig 클래스에 @Bean 어노테이션을 붙여서 HttpSessionHandshakeInterceptor를 빈으로 등록한다.

 

 

 

- sockjs

- @EnableWebSocket

 

 

 

 

ㅁ 서버 start

 

- 브라우저 2개로 로그인하고 채팅이 잘 되나 확인한다. 잘 된다.

 

 

 

'Spring' 카테고리의 다른 글

디비  (1) 2024.11.06
[스프링부트] 10. 트랜잭션 처리  (0) 2024.11.06
[스프링부트] 8. Scheduler  (0) 2024.11.06
[스프링부트] 7. Interceptor  (0) 2024.11.06
[스프링부트] 6. 파일업로드 처리  (0) 2024.11.06