Websocket이란?
WebSocket은 클라이언트와 서버(브라우저와 서버)를 연결하고 실시간으로 통신이 가능하도록 하는 통신 방법이다. WebSocket과 HTTP의 주요 차이점은 HTTP에서 발생하는 것처럼 별도의 요청을 보내지 않고도 데이터를 송수신할 수 있다는 것이다. 지속적으로 업데이트되는 정보를 수신해야 하는 채팅이나 주식 보고서 등에서 WebSocket 프로토콜을 사용되고 있다.
HTTP 통신과의 차이는?
기존 HTTP는 단방향 통신이었다. 클라이언트에서 서버로 Request를 보내면 서버는 클라이언트로 Response를 보내는 방식으로 동작했다. 또한, HTTP는 기본적으로 무상태(Stateless)이므로 상태를 저장하지 않는다.
하지만 웹소켓은 양방향 통신으로 연결이 이루어지면 클라이언트가 요청하지 않아도 데이터가 저절로 서버로부터 올 수 있다. HTTP처럼 별도의 요청을 보내지 않아도 데이터를 수신할 수 있다는 것이다.
예를 들어, 웹 상에서 구글 Docs를 이용해 여러 사용자가 동시에 한 문서를 편집하고 있다고 하자. 구글 Docs를 사용해 본 유저는 알겠지만, 새로고침을 누르지 않아도 실시간으로 다른 사용자가 편집한 부분이 자동으로 적용되는 모습을 볼 수 있다. 이는 WebSocket을 이용한 기술이다.
또한, 웹소켓은 HTTP와 다르게 상태(Stateful) 프로토콜이다. 즉, 클라이언트와 서버가 한 번 연결되면 같은 연결을 이용해 통신하므로 TCP 커넥션 비용을 아낄 수 있다.
Websocket의 동작 방법

Websocket은 HTTP에서는 포트 80, HTTPS에서는 포트 443 위에서 동작한다.
웹소켓은 TCP연결처럼 핸드셰이크를 이용해 연결을 맺는다. 이때 HTTP 업그레이드 헤더를 사용하여 HTTP 프로토콜에서 웹소켓 프로토콜로 변경한다. 즉, 최초 접속시에는 HTTP 프로토콜을 이용해 핸드셰이킹을 한다. 이후 연결이 맺어지면 어느 한쪽이 연결을 끊지 않는 이상 영구적인(persistent) 동일한 채널이 맺어지고, HTTP 프로토콜이 웹소켓 프로토콜로 변경된다. 이때 데이터를 암호화하기 위해 WSS 프로토콜 등을 이용할 수도 있다.
프로젝트 구현
Websocket 서버 구축 시 참고한 게시글
https://www.daddyprogrammer.org/post/4077/spring-websocket-chatting/
프로젝트 생성 및 개발 환경 세팅

- Build : Gradle
- Languge : Java 17
- Spring Boot : v3.1.2
- Dependencies
- Spring Web
- Lombok
- WebSocket
- IDE : IntelliJ-Ultimate IDEA

Websocket 연결 후 테스트 해보기
먼저 기초적인 Websocket 연결을 해서 통신이 원활하게 이루어지는지 확인해 보자.
필요한 의존성 확인
< build.gradle >
plugins {
id 'java'
id 'org.springframework.boot' version '3.1.2'
id 'io.spring.dependency-management' version '1.1.2'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
WebSocket Handler 추가하기
Socket 통신은 일반적으로 한 개의 서버와 다수의 클라이언트가 관계를 맺는 1:N 관계를 맺는다. 따라서 다수의 클라이언트로부터 들어오는 데이터를 받아서 처리를 해주는 Handler 역할의 클래스가 필요하다. 여기서는 TextWebSocketHandler를 상속받고 handleTextMessage() 메서드를 Override 해줄 예정이다.
그리고 잘 동작하는지 확인하기 위해 Client에서 받은 메세지는 콘솔에 log로 남겨주고 Client에 환영 메시지를 넘겨줄 것이다.
< WebSocketHandler.java >
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
@Slf4j // 로그남기기위해 추가
@Component
public class WebSocketHandler extends TextWebSocketHandler {
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String payload = message.getPayload();
log.info("전달받은 payload: {}", payload);
session.sendMessage(new TextMessage("Welcome~!!"));
}
}
Websocket Config 추가하기
추가한 Handler를 사용해서 Websocket을 활성하 시켜주기 위한 Config 파일을 추가해 준다.
WebSocketConfigurer를 상속받고 registerWebSocketHandlers() 메서드를 Override 해준다.
< WebsocketConfig.java >
import com.example.websocket.util.WebsocketHandler;
import lombok.RequiredArgsConstructor;
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 // 설정파일임을 나타내고 해당 클래스를 Bean 등록 해주는 어노테이션
@EnableWebSocket // Websocket 활성화 시켜주는 어노테이션
@RequiredArgsConstructor// 생성자를 자동으로 선언해주는 lombok 어노테이션
public class WebsocketConfig implements WebSocketConfigurer {
private final WebsocketHandler websocketHandler; // WebsocketHandler를 가져온다.
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry
// Websocket에 접속하기 위한 Endpoint: /ws/chat
.addHandler(websocketHandler, "/ws/chat")
// 도메인이 다른 서버에서도 접속 가능하도록 CORS:setAllowedOrigins("*") 추가
.setAllowedOrigins("*");
}
}
Postman을 사용해서 Websocket 연결 테스트하기
Websocket 통신을 확인하기 위해 Postman을 사용해서 테스트 해볼 예정이다.
포스트맨 다운로드
https://www.postman.com/downloads/
먼저 Spring Boot 서버를 구동시킨다.

Postman을 실행 시킨 후 새로운 WebSocket Request를 생성해 준다.

설정해 준 Endpoint 인 ws://localhost:8080/ws/chat 주소 입력 후 Connect 버튼 클릭
웹소켓의 url 주소는 http로 시작하는 HTTP 통신과는 다르게 ws로 시작하는 주소체계를 갖는다.

연결이 성공하면 아래의 사진처럼 표시된다

연결이 성공하면 아래의 사진처럼 표시된다

서버도 Websocket을 통해 Data를 잘 전달받은 것을 로그를 통해 알 수 있다.

채팅기능 고도화 시키기
위에 까지 진행했을 때의 상태는 하나의 서버에 여러 클라이언트가 접속을 할 수 있는 상태이다. 이는 채팅방이 하나만 존재해 그 방에 모든 클라이언트가 입장해 있는 형태라고 볼 수 있다. 이를 더 고도화시켜서 여러 채팅방을 만들고 해당 채팅방에 존재하는 클라이언트끼리만 대화를 주고받을 수 있는 채팅서버로 업그레이드시켜 보자.
Spring Boot 서버에 Client가 접속을 하면 각각의 Websocket Session을 가지게 되고 채팅방은 입장한 Client들의 Session을 보관 및 처리를 해줌으로써 채팅방 기능을 구현할 수 있다.
ChatMessageDto 추가하기
채팅을 주고받을 Message 관련 DTO를 작성해 준다. DTO의 구성은 다음과 같이 설계해 준다.
먼저 크게 채팅방에 입장하는 경우, 채팅방에 메시지를 보내는 경우(type)를 구분해 주기 위해 enum 클래스를 사용해 준다.
다음 채팅방 고유의 번호(roomId)를 나타내주는 필드, 보내는 사람(sender), 보낼 내용(message)으로 구성해 준다.
< ChatMessageDto.java >
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class ChatMessageDto {
public enum MessageType {
ENTER, // 입장
TALK // 채팅
}
private MessageType type; // 메세지 타입
private String roomId; // 방 고유번호
private String sender; // 보낸사람
private String message; // 내용
}
ChatRoomDto 추가하기
다음으로는 채팅방을 구현해 주기 위한 DTO를 생성해 준다. 멤버 필드로는 채팅방 고유번호(roomId), 방 이름(name), 클라이언트의 세션을 관리하기 위한 WebSocketSession 리스트를 갖는다.
< ChatMessageDto.java >
import lombok.Builder;
import lombok.Getter;
import org.springframework.web.socket.WebSocketSession;
import java.util.HashSet;
import java.util.Set;
@Getter
public class ChatRoomDto {
private String roomId;
private String name;
private Set<WebSocketSession> sessions = new HashSet<>();
@Builder // 빌더패턴으로 생성자를 선언해준다 (편의성)
public ChatRoomDto(String roomId, String name) {
this.roomId = roomId;
this.name = name;
}
// 채팅방에 입장, 채팅 기능이 있으므로 분기 처리를 위한 메소드
public void handleActions(
WebSocketSession session, ChatMessageDto chatMessage, ChatService chatService
) {
// 채팅방에 입장하는 것이라면
if (chatMessage.getType().equals(ChatMessageDto.MessageType.ENTER)) {
// WebSocketSession 리스트에 추가 (클라이언트 추가)
sessions.add(session);
// 입장한 클라이언트를 채팅방의 다른 클라이언트들에게 알려주기위한 메시지 세팅
chatMessage.setMessage("[" + chatMessage.getSender() + "]" + "님이 입장했습니다.");
}
// 채팅방의 다른 모든 클라이언트에게 메시지 전송
this.sendMessage(chatMessage, chatService);
}
// 리스트에 저장되어있는 모든 클라이언트에게 메시지를 전송하기 위한 메소드
private <T> void sendMessage(T message, ChatService chatService) {
sessions.parallelStream()
.forEach(session -> chatService.sendMessage(session, message));
}
}
ChatService 추가하기
다음으로 채팅방을 생성, 조회, 하나의 세션에 메시지를 발송하는 로직을 담당하는 Service 클래스를 생성해 준다. 채팅방 조회를 위해서 생성한 채팅방의 정보를 저장하기 위한 Map을 구현해 준다.
- 채팅방 생성 : Random UUID로 고유의 ID를 가진 채팅방 객채를 생성하고 Map에 추가
- 채팅방 조회 : Map에 담긴 채팅방의 정보를 조회
- 메시지 발송 : 지정한 WebSocket Session에 메시지를 발송
< ChatService.java >
import com.example.websocket.dto.ChatRoomDto;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import java.io.IOException;
import java.util.*;
@Slf4j
@RequiredArgsConstructor
@Service
public class ChatService {
private final ObjectMapper objectMapper; // JSON 데이터를 Parsing 하는데 필요한 객체
private Map<String, ChatRoomDto> chatRooms;
@PostConstruct // 의존성 주입이 완료되고 초기화를 해주기위한 어노테이션
private void init() {
chatRooms = new LinkedHashMap<>();
}
// 채팅방 전체 조회
public List<ChatRoomDto> findAllRoom() {
return new ArrayList<>(chatRooms.values());
}
// 채팅방 단독 조회
public ChatRoomDto findRoomById(String roomId) {
return chatRooms.get(roomId);
}
// 채팅방 생성
public ChatRoomDto createRoom(String name) {
String randomId = UUID.randomUUID().toString();
ChatRoomDto chatRoom = ChatRoomDto.builder()
.roomId(randomId)
.name(name)
.build();
chatRooms.put(randomId, chatRoom);
return chatRoom;
}
// 메시지 발송
public <T> void sendMessage(WebSocketSession session, T message) {
try {
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(message)));
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
}
ChatController 추가하기
채팅방 생성, 조회는 Rest API로 구현할 것이기 때문에 RestController로 구현해 준다.
< ChatService.java >
import com.example.websocket.dto.ChatRoomDto;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/chat")
@RequiredArgsConstructor
public class ChatController {
private final ChatService chatService;
// 채팅방 생성
@PostMapping
public ChatRoomDto createRoom(@RequestParam String name) {
return chatService.createRoom(name);
}
// 채팅방 조회
@GetMapping
public List<ChatRoomDto> findAllRoom() {
return chatService.findAllRoom();
}
}
WebSocket Handler 수정하기
고도화를 위해 추가된 채팅 로직을 handler에 추가해 준다.
- Client로부터 받은 채팅메시지를 ChatMessageDto 객채로 변환
- 전달받은 데이터 안에 포함된 방 고유번호(roomId)로 채팅방 객체를 찾아서 반환
- 채팅방에 입장해 있는 모든 Client들에게 타입에 따른 메시지 발송
< WebSocketHandler.java >
import com.example.websocket.ChatService;
import com.example.websocket.dto.ChatMessageDto;
import com.example.websocket.dto.ChatRoomDto;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
@Slf4j
@Component
@RequiredArgsConstructor
public class WebSockChatHandler extends TextWebSocketHandler {
private final ObjectMapper objectMapper;
private final ChatService chatService;
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String payload = message.getPayload();
log.info("payload {}", payload);
// 채팅 메시지 객체로 변환
ChatMessageDto chatMessage = objectMapper.readValue(payload, ChatMessageDto.class);
// 방 객체 반환
ChatRoomDto room = chatService.findRoomById(chatMessage.getRoomId());
// 메시지 뿌리기
room.handleActions(session, chatMessage, chatService);
}
}
채팅기능 TEST
Postman을 가지고 테스트를 진행해 보자.
먼저 Spring Boot 프로젝트를 실행을 시켜준다.
채팅방 생성하기


채팅방 입장하기
위의 2.4에 나와있는 것처럼 Postman에서 새로운 WebSocket - Request를 생성해 주고 응답받은 채팅방의 고유번호를 가지고 채팅방에 입장한다.
Message에 아래의 JSON data를 넣어주고 SEND를 클릭해서 서버로 Data를 보내준다.
// JSON
{
"type":"ENTER",
"roomId":"c6d5aab4-79ce-4036-8ca7-bcf797f8fea7",
"sender":"Client A",
"message":""
}

Response에서 응답받은 데이터(파란색 화살표)를 확인해 보면 채팅방에 입장을 했다는 메시지를 확인할 수 있다.

4.3 대화해보기
채팅이 잘 이루어지는지 확인하기 위해 위와 같은 방법으로 다른 요청을 생성해서 같은 채팅방에 입장해 준다.

"type"에 "TALK"를 넣어주고 "message"에 보내고 싶은 메시지를 입력 후 전송을 해본다
{
"type":"TALK",
"roomId":"c6d5aab4-79ce-4036-8ca7-bcf797f8fea7",
"sender":"Client B",
"message":"안녕하세요 Client B 입니다~!"
}

Client A의 Response를 확인해 보면 Client B가 보낸 채팅이 잘 전달된 것을 확인할 수 있다.

후기
Spring Boot 환경에서 WebSocket을 사용하여 기본적인 채팅 기능이 구현된 채팅 서버를 구축해 보았다.
이해가 한 번에 되진 않았지만 몇 번 따라 하다 보니 전체적인 흐름이 조금씩 이해가 가기 시작한 것 같다.
다음에는 WebSocket에서도 HTTP의 방식처럼 정해진 프로토콜 대로 통신을 할 수 있는 방식 중 하나인 STOMP를 사용해서 채팅방을 조금 더 고도화를 해볼까 한다.
'Back-end > Spring Boot' 카테고리의 다른 글
| [Spring Boot] 개발환경 만들기 - local DB(MariaDB)로 사용하기 (0) | 2023.05.24 |
|---|
