📍HTTP VS WebSocket
일반적인 서버와의 통신인 HTTP는 요청이 오면 응답을 반환하는 구조입니다.
즉 채팅같이 실시간적으로 변하는 로직에는 쉴새없이 HTTP로 API를 호출해야하는 상황입니다.
이를 위해 연결지향을 이용한 2가지 해결책이 존재합니다.
- Server-Sent Evnet
- WebSocket
저희는 둘 중 웹소켓을 이용해볼 예정입니다.
📍WebSocket?
웹소켓은 쉽게 말해 연결을 유지한다고 생각하면 됩니다.
처음 핸드셰이크를 통해 연결을 생성하면 이후에 필요한 메세지만 주고받기에 주고받는 데이터의 양에서 차이가 많이 납니다.

📍SpringBoot로 Websocket 구현해보기
1️⃣ WebSocketConfig
@Configuration
@EnableWebSocket
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketConfigurer {
private final SocketHandler socketHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(socketHandler, "/chat/{roomId}")
.setAllowedOrigins("*");
}
}
- Spring 에서는 간단하게 WebSocketConfig 를 구성할 수 있습니다.
registerWebSocketHandlers 메서드를 통해 Spring이 제공하는 WebSocket 핸들러와 URL을 매핑합니다. - webSocketHandler 핸들러를 사용하며, chat/{roomId}를 엔드포인트로 설정해줍니다.
- 이렇게 설정해 놓게되면, ws://localhost:8080/chat/{roomId} 로 웹소켓 연결이 가능하게 됩니다.
- .setAllowedOrigins("*") 부분은 CORS 설정 부분입니다.
2️⃣ SocketHandler
@Component
@RequiredArgsConstructor
public class SocketHandler extends TextWebSocketHandler {
private final Map<String, List<WebSocketSession>> chatRooms = new HashMap<>();
private final ChatMessageService chatMessageService;
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String payload = message.getPayload();
String[] data = payload.split(":", 3); // ":" 기준으로 메시지 분리 (sender:roomId:content)
if (data.length < 3) {
session.sendMessage(new TextMessage("Invalid message format. Use 'sender:roomId:message' format."));
return;
}
String sender = data[0]; // 발신자
String roomId = data[1]; // 채팅방 ID
String chatMessage = data[2]; // 메시지 내용
// 메시지 저장
chatMessageService.saveMessage(roomId, sender, chatMessage);
// 해당 채팅방에 메시지 전송
if (chatRooms.containsKey(roomId)) {
for (WebSocketSession webSocketSession : chatRooms.get(roomId)) {
if (webSocketSession.isOpen()) {
webSocketSession.sendMessage(new TextMessage(sender + ": " + chatMessage));
}
}
}
}
@Override
public void afterConnectionEstablished(WebSocketSession session) {
String uri = session.getUri().toString();
String roomId = uri.substring(uri.lastIndexOf("/") + 1);
chatRooms.computeIfAbsent(roomId, k -> new ArrayList<>()).add(session);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
chatRooms.forEach((roomId, sessions) -> sessions.remove(session));
}
}
- 상속 받은 TextWebSocketHandler는 Spring에서 WebSocket의 텍스트 메시지를 처리하는 기본 핸들러 클래스입니다.
- chatRoom이라는 map을 통해서 방과, 그 방에 속한 사람(세션)을 관리할 예정입니다.
- handleTextMessage는 WebSocket으로 수신한 메시지를 처리하는 메서드로, 받은 메시지를 특정 채팅방에 연결된 클라이언트들에게 전달하는 역할을 합니다.
로직은 inho:1:hello 와 같이 클라이언트가 전송하게 되면, 발신자가 inho, 방이 1, 메시지가 hello가 되도록 처리됩니다. - saveMessage로 데이터베이스에 메시지를 저장하며, chatRooms에서 해당 roomId에 연결된 모든 세션(WebSocket 클라이언트)을 찾습니다. 그 후 연결된 세션 각각에 메시지를 전송합니다. 연결이 열려 있는 세션에만 메시지를 전송합니다.
- afterConnectionEstablished 메서드는 새로운 연결이 완료되면 호출되는 메서드로, 클라이언트 세션을 채팅방에 추가합니다.
WebSocket 연결 요청 URI에서 roomId를 추출하고 이를 키로 하는 map을 생성합니다.(있다면 그 map을 가져옴.) 그 후 세션을 저장합니다. - afterConnectionClosed 메서드는 WebSocket 연결이 종료되었을 때 호출됩니다. chatRooms에서 종료된 세션을 모든 채팅방에서 제거하는 로직입니다.
3️⃣ ChatRoom, ChatMessage
@Entity
@Getter
@NoArgsConstructor
public class ChatRoom {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String roomName;
@ManyToOne
@JoinColumn(name = "from_member")
private Member fromMember;
@ManyToOne
@JoinColumn(name = "to_member")
private Member toMember;
@OneToMany(mappedBy = "chatRoom", cascade = CascadeType.ALL, orphanRemoval = true)
private List<ChatMessage> messages = new ArrayList<>();
@Builder
public ChatRoom(String name, Member fromMember, Member toMember) {
this.roomName = name;
this.fromMember = fromMember;
this.toMember = toMember;
}
}
public class ChatMessage {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String sender;
private String content;
@CreationTimestamp
private LocalDateTime timestamp;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "chat_room_id", nullable = false)
private ChatRoom chatRoom;
@Builder
public ChatMessage(String sender, String content, ChatRoom chatRoom, LocalDateTime timestamp) {
this.sender = sender;
this.content = content;
this.chatRoom = chatRoom;
this.timestamp = timestamp;
}
}
- Entity를 생성해 줍니다. 연관관계를 보면 알 듯이, ChatRoom은 member 두 명과 chatMessage를 외래키로 알고 있는 구조입니다.
4️⃣ 다른 로직들은..
- 기본적인 CRUD이기 때문에 생략하도록 하겠습니다.
- 채팅방 아이디와 로그인한 사람의 이름만 안다면, 전체적인 웹소켓 로직은 사용할 수 있습니다.
📍테스트해보기
hl=kohttps://chromewebstore.google.com/detail/simple-websocket-client/pfdhoblngboilpfeibdedpjgfnlcodoo?hl=ko
Simple WebSocket Client - Chrome 웹 스토어
Construct custom Web Socket requests and handle responses to directly test your Web Socket services.
chromewebstore.google.com
아직 클라이언트를 구현하지 않았으므로 클라이언트로는 구글 확장으로 제공되는 Simple WebSocket Client를 사용했습니다.

저는 방을 하나 생성해 놓았기 때문에, 1번 방으로 접근하겠습니다.

OPENED로 Status가 변경되었다면 성공입니다. 추가로 메세지가 성공적으로 도착하는지 확인하기 위해서, 창을 하나 더 띄워서 연결해 놓습니다.


이런 식으로 {이름}:{방번호}:{메시지} 형식으로 보내면, 성공적으로 메시지가 전송되는 것을 볼 수 있습니다!
📍배포환경에서의 이슈
- Websocket은 HTTP와 엄연히 다른 프로토콜이기 때문에, nginx에서 환경설정 추가가 필요합니다.
때문에 location /chat/을 통해 웹소켓 통신을 처리하기 위한 설정을 진행이 필요합니다.
(nginx코드)
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /run/nginx.pid;
# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;
events {
worker_connections 1024;
}
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
keepalive_timeout 65;
types_hash_max_size 4096;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Load modular configuration files from the /etc/nginx/conf.d directory.
include /etc/nginx/conf.d/*.conf;
# HTTPS 서버 블록
server {
server_name mixmix2.store;
root /usr/share/nginx/html;
access_log /var/log/nginx/proxy/access.log;
error_log /var/log/nginx/proxy/error.log;
# WebSocket 관련 설정
location /chat/ {
proxy_pass http://3.36.156.34:8080; # Spring Boot 서버 주소
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
# 캐시 우회를 위한 설정
proxy_cache_bypass $http_upgrade;
# CORS 헤더
add_header 'Access-Control-Allow-Origin' 'http://localhost:3000';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PATCH, OPTIONS, PUT, DELETE';
add_header 'Access-Control-Allow-Headers' 'Authorization,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
add_header 'Access-Control-Allow-Credentials' 'true';
# Preflight OPTIONS 요청 처리
if ($request_method = 'OPTIONS') {
return 204;
}
}
# 일반 API 요청 처리
location / {
include /etc/nginx/proxy_params;
proxy_pass http://3.36.156.34:8080;
# CORS 설정
add_header 'Access-Control-Allow-Origin' 'http://localhost:3000';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PATCH, OPTIONS, PUT, DELETE';
add_header 'Access-Control-Allow-Headers' 'Authorization,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
add_header 'Access-Control-Allow-Credentials' 'true';
# OPTIONS 요청 처리
if ($request_method = 'OPTIONS') {
return 204;
}
}
# SSL 설정 (Certbot 관리)
listen [::]:443 ssl ipv6only=on; # managed by Certbot
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/mixmix2.store/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/mixmix2.store/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
# HTTP -> HTTPS 리다이렉트
server {
if ($host = mixmix2.store) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
listen [::]:80;
server_name mixmix2.store;
return 404; # managed by Certbot
}
}
- 다음으로 HTTPS로 배포를 진행했으면 ws도 s를 붙여서 url을 구성해야 합니다.

- 이런 식으로..
📍참고자료
https://innu3368.tistory.com/214
WebSocket을 이용한 실시간 채팅 구현하기 1: 서버
WebSocket을 이용한 실시간 채팅 구현 기록은 다음의 네 단계에 걸쳐 작성할 예정이다. WebSocket을 이용한 실시간 채팅 구현 1: 서버 WebSocket을 이용한 실시간 채팅 구현 2: 클라이언트 WebSocket을 이용
innu3368.tistory.com
실시간 채팅 서비스 만들어보기
우리만의 아지트(채팅방) 만들기
velog.io
'springboot' 카테고리의 다른 글
[SpringBoot] 끄적끄적 프로젝트 쿼리 성능 개선을 해보자 (0) | 2025.01.06 |
---|---|
[SpringBoot] QueryDSL 그게 뭔데 다들 쓰는거지? (0) | 2024.12.28 |
[SpringBoot] Session을 활용한 회원가입/로그인 유지 (1) | 2024.11.01 |
[SpringBoot] Spring Boot로 REST API 만들기 (0) | 2024.09.28 |
[SpringBoot] IoC/DI 가 뭘까? (0) | 2024.08.28 |