스프링부트 구현

스프링부트로 실시간 채팅을 구현해보자 [웹소켓]

rexondex 2024. 10. 4. 14:44

 

이 프로젝트는 스프링부트를 활용하여 웹소켓을 통해 실시간 채팅 기능을 구현한 간단한 단일 HTML 페이지입니다.

프로젝트의 주요 기능 및 기술 스택은 다음과 같습니다:

  • 웹소켓을 이용한 실시간 채팅: 클라이언트와 서버 간의 실시간 데이터 전송을 위해 웹소켓을 사용하여 사용자가 입력한 메시지를 즉시 다른 사용자에게 전달할 수 있습니다.
  • 스프링부트: 백엔드 프레임워크로 스프링부트를 사용하여 서버를 구성하고 웹소켓 핸들러를 설정했습니다.
  • OAuth를 통한 로그인 및 보안 연결: 사용자 인증을 위해 OAuth를 구현하였으며, 안전한 데이터 전송을 위해 SSL 인증서를 적용하였습니다.
  • 포트 설정 및 SSL 구성: SSL 연결을 위해 포트 8443을 사용하고, 키 저장소 및 암호와 같은 SSL 관련 설정을 포함하여 HTTPS 연결을 지원합니다.

이러한 기술적 요소를 통해 안전하고 실시간으로 상호작용할 수 있는 채팅 애플리케이션을 완성하였습니다.

 


 

[ 데모프로젝트 생성 ] https://start.spring.io/

start.spring.io

 

스프링 이니셜라이저로 스크린샷과 같은 환경에서 6개의 라이브러리와 함께 데모프로젝트 생성했습니다.

 

스프링시큐리티OAuth2는 실시간 채팅 환경에서 사용자 식별을 위한 라이브러리로 추가해 놓았습니다. 필요하다면 추후 구현할 예정입니다.

 


 

[ 진행 순서 ]

프로젝트 구조

 

그러면 하나하나 설정을 완료해 보겠습니다.

  1. 필요한 4가지 클래스를 작성
  2. 자체 서명 인증서 생성 ( keystore.pfx )
  3. application.properties 설정
  4. index.html 작성

 


 

[ WebConfig ] 부터 순서대로 작성하겠습니다.

package com.example.demo.web;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("https://localhost:8443") // localhost 주소를 명시적으로 나열합니다.
                .allowCredentials(true);
    }
}

 


 

[ MyWebSocketHandler ]

package com.example.demo.websocket;

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 java.util.Collections;
import java.util.HashSet;
import java.util.Set;

public class MyWebSocketHandler extends TextWebSocketHandler {

    private static final Set<WebSocketSession> sessions = Collections.synchronizedSet(new HashSet<>());

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        sessions.add(session);
        session.sendMessage(new TextMessage("Welcome! You are connected with session ID: " + session.getId()));
    }

    @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        // 사용자의 메시지와 세션 ID를 함께 전송
        String msgPayload = session.getId() + ": " + message.getPayload();

        // 모든 클라이언트에게 메시지를 전송
        for (WebSocketSession s : sessions) {
            if (s.isOpen()) {
                s.sendMessage(new TextMessage(msgPayload));
            }
        }
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        sessions.remove(session);
    }
}

 


 

[ WebSocketConfig ] 

package com.example.demo.websocket;

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;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(new MyWebSocketHandler(), "/websocket")
                .setAllowedOrigins("https://localhost:8443", "http://localhost:8443"); // CORS 설정
    }
}

 


 

[ WebSocketController ]

package com.example.demo.websocket;

import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;

@Controller
public class WebSocketController {

    @MessageMapping("/sendMessage") // 클라이언트에서 보낸 메시지 처리
    @SendTo("/topic/messages") // 모든 구독자에게 메시지 전송
    public String sendMessage(String message) {
        return message; // 클라이언트에게 메시지 전송
    }
}

 


 

[ Self-Signed Certificate (자체 서명 인증서) 생성 ]

 

- 테스트용 인증을 위해서 인증서를 생성하는 과정입니다. pfx 파일을 생성해 기초적인 과정을 진행하기 위한 것이며 실제 프로젝트에서는 사용하면 안됩니다.

keytool -genkeypair -alias your-alias -keyalg RSA -keystore keystore.pfx -storetype PKCS12 -storepass your-password -validity 365

 

[ 빈 화면에 마우스 우클릭 > 터미널에서 열기(또는 PowerShell 열기) > 위 명령어 실행 ]

 

6~7가지 항목을 물어보는데 저는 모두 엔터로 스킵하고 마지막 질문에서 'yes' 입력해 키 생성 완료했습니다.

 

그러면 우클릭한 경로에 keystore.pfx 가 생성됩니다.

 

저는 이 .pfx 파일을 template 경로 application.properties 가 있는 곳에 두었습니다.

 


 

[ application.properties 설정 ]

server.port=8443
server.ssl.key-store=classpath:keystore.pfx
server.ssl.key-store-password=your-password
server.ssl.keyStoreType=PKCS12
server.ssl.keyAlias=your-alias

 

- 왜 8443 포트와 SSL/TLS 설정을 사용하나요? ( ChatGPT 답변 )

Spring Boot 애플리케이션에서 위와 같은 SSL 설정을 하는 이유는 다음과 같습니다:

1. 보안 통신(SSL/TLS)
- 암호화된 데이터 전송: SSL(TLS)는 클라이언트와 서버 간의 데이터를 암호화하여, 제3자가 데이터를 가로채거나 읽지 못하도록 합니다. HTTPS를 사용하여 보안을 강화하는 데 필수적입니다.
신뢰성: SSL 인증서를 통해 서버의 신원을 보증하므로, 클라이언트는 해당 서버가 신뢰할 수 있는지를 확인할 수 있습니다.

2. 설정 항목 설명

server.port=8443:
// Spring Boot 애플리케이션이 사용할 포트를 설정합니다. 일반적으로 HTTPS는 443 포트를 사용하지만, 개발 환경에서는 8443과 같은 대체 포트를 사용할 수 있습니다.

server.ssl.key-store=classpath:keystore.pfx:
// SSL 인증서가 저장된 키 저장소 파일의 경로를 지정합니다. 이 예에서는 keystore.pfx 파일이 classpath에 위치해 있습니다. 이 파일에는 서버의 공개 키와 개인 키가 포함되어 있습니다.

server.ssl.key-store-password=your-password:
// 키 저장소 파일을 열 때 필요한 비밀번호를 설정합니다. 이 비밀번호는 키 저장소의 보안을 유지하는 데 중요합니다.

server.ssl.keyStoreType=PKCS12:
// 사용되는 키 저장소의 형식을 지정합니다. PKCS12는 널리 사용되는 키 저장소 형식으로, 다양한 운영 체제에서 호환됩니다.

server.ssl.keyAlias=your-alias:
// 키 저장소에 저장된 여러 키 중에서 사용할 키의 별칭(alias)을 지정합니다. 특정 키를 사용해야 할 경우 이 설정을 통해 어떤 키를 사용할지를 지정할 수 있습니다.

3. 개발 및 배포 환경
- 이러한 SSL 설정은 개발 환경에서 테스트할 때, 실제 배포 환경에서 HTTPS를 사용하는 경우 모두 필요합니다. 이를 통해 데이터의 기밀성을 보장하고, 애플리케이션의 신뢰성을 높일 수 있습니다.
이와 같은 SSL 설정은 보안 통신을 필요로 하는 모든 웹 애플리케이션에서 필수적으로 고려해야 할 사항입니다.

 


 

[ index.html ]

<!DOCTYPE html>
<html>
<head>
    <title>WebSocket Chat</title>
    <script>
        let socket;

        function connect() {
            socket = new WebSocket("wss://localhost:8443/websocket");

            socket.onopen = function() {
                console.log("Connected to the server");
            };

            socket.onmessage = function(event) {
                const chatBox = document.getElementById("chatBox");
                chatBox.value += event.data + "\n"; // 수신된 메시지를 채팅 박스에 추가
            };

            socket.onclose = function() {
                console.log("Disconnected from the server");
            };

            socket.onerror = function(error) {
                console.error("WebSocket error: ", error);
            };
        }

        function sendMessage() {
            const messageInput = document.getElementById("messageInput");
            const message = messageInput.value;
            socket.send(message); // 메시지 전송
            messageInput.value = ""; // 입력 필드 초기화
        }

        window.onload = function() {
            connect();
        };
    </script>
</head>
<body>
<h1>WebSocket Chat</h1>
<textarea id="chatBox" rows="10" cols="30" readonly></textarea><br>
<input type="text" id="messageInput" placeholder="Enter a message...">
<button onclick="sendMessage()">Send</button>
</body>
</html>

 


 

[ 브라우저 접속 ]

 

'https://' 로 접속합니다.

https://localhost:8443

 

자체 생성 인증서이므로 보안 문제를 경고하는 화면
경고를 스킵하고 접속하면 스프링 시큐리티의 기본 /login 화면이 나옵니다.

 

spring.security.user.name=admin
spring.security.user.password=admin123

 

저는 아이디를 admin, 패스워드를 admin123 으로 설정하겠다고 application.properties에 명시했습니다.

 

아까 작성한 application.properties 밑에 추가로 작성해두면 admin 계정을 통해 index.html로 접속할 수 있습니다.

 


 

[ 실시간 채팅박스 기능 성공화면 ]

두개의 게스트 브라우저를 열어서 각자 localhost:8443에 접속시켰습니다.

 

 

이때 접속한 두개의 브라우저는 서로다른 session-id 를 가지게 됩니다.

접속시 서로 다른 session-id인 것이 확인되면 정상적으로 실행된 것입니다.

 

하나는 'c1f7...' 이고 다른 하나는 '82cb8...' 로 실행되었네요.

 

채팅 박스를 생성하여 접속자간 채팅을 보내고 확인하는 기능이 완성되었습니다.

 

 

( 이 프로젝트는 ChatGPT 질의응답과 함께 구현을 완료했습니다. )