(스프링부트 3.2.4로 진행됩니다)
웹소켓이라는 기술을 평소에 사용해보고 싶기도 했어서 이번 프로젝트에 한번 적용해보려고 한다.
내가 원하는 느낌은 자소설닷컴의 느낌과 비슷하다.

요런 느낌으로 로그인하면 하나의 채팅방을 통해 모두가 자유롭게 대화할 수 있는 장소를 하나 마련해보려고 한다.
(학습하면서 진행하는 부분이라 다음 내용이 언제 올라갈지 모르겠습니다.)
우선 웹소켓을 사용하냐 socket.io를 사용하냐의 문제가 있었다.
그전에 간단하게 네트워크에서 어떻게 실시간 통신을 해왔는지 살펴보자.
1. Polling
- 일정한 주기(시간)를 가지고 서버와 응답을 주고받는 방식
- 웹 자체가 영구 연결이 불가능하기에 매번 주고 받아야함.(서버 부하 문제 발생)
- 주기가 길면 실시간성이 매우 떨어진다.
2. Long Polling
- 폴링과 유사
- 서버에서 요청을 미리 받아 놓고, 클라이언트에서 사용이 불가하면 대기하다가 서버에서 클라이언트로 보낼 응답이 생긴다면 응답을 보내고 연결 종료
- 클라이언트는 다시 요청을 보내고 응답을 기다리는 방식
3. SSE (Server Sent Event)
- 클라이언트와 서버가 연결한 이후, 서버에서 클라이언트로 데이터를 보내주는 방식(단방향)
- 즉 서버에서만 송신이 가능하다는 특징.
4. 웹 소켓 (Web Socket)
- 실시간 양방향 데이터 전송을 위한 프로토콜
- 기존 http는 요청한 클라이언트에게만 응답이 가능하지만, ws 프로토콜을 통해 포트 공유 가능
- 최초 연결은 http, https로 연결을 진행
- 매우 빠르게 작동, 적은 데이터 이용
5. 소켓io (Socket.io)
- 브로드캐스팅(접속한 유저 모두에게 메시지 발송), 접속 끊어지면 재접속, 채팅방 등등 다양한 기능을 제공하는 라이브러리
- 유지보수 측면에서 이점이 많다.
나는 여기서 웹소켓을 선택했다.
아무래도 채팅방도 하나이기도 하고, 빠른 속도, 적은 데이터가 끌리기도 했다. 또 socket.io가 제공하는 기능들은 내가 직접 구현해야겠지만, 하나씩 알아보면 학습에 도움이 되지 않을까?라는 생각도 있다.
우선 먼저 웹소켓을 등록해야한다.
implementation 'org.springframework.boot:spring-boot-starter-websocket'
웹소켓을 관리하는 Config 하나 만든다.
@Configuration
@EnableWebSocket
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketConfigurer {
private final WebSocketHandler webSocketHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
// 클라이언트에서 접속할 웹소켓 주소를 "/ws"로 설정
registry.addHandler(webSocketHandler, "/ws").setAllowedOrigins("*");
}
}
우선 먼저 해주는 일이 웹소캣 핸들러를 등록하는 일이다. 밑에서 핸들러는 정의를 진행한다.
일단 웹소켓에 접속할 주소를 적어준다. /ws로 설정하면 ws://localhost:8080/ws 해당 주소로 요청을 보내면 웹소켓으로 접속을 진행하게 됩니다.
이후 요청을 컨트롤할 핸들러 작성
@Component
@Slf4j
public class WebSocketHandler extends TextWebSocketHandler {
private final List<WebSocketSession> sessions = new CopyOnWriteArrayList<>();
/**
* 클라이언트로부터 메시지가 도착했을 때 호출되는 메소드
* */
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// 클라이언트로부터 받은 메시지
String payload = message.getPayload();
log.info("클라이언트에게 온 메시지 : " + payload);
// 모든 클라이언트에게 메시지 전송
for(WebSocketSession webSocketSession : sessions){
if(session.isOpen()){
webSocketSession.sendMessage(new TextMessage("서버에서 보내는 메시지 : " + payload));
}
}
}
}
해당 핸들러는 TextWebSocketHandler의 내용을 상속받아서 다양한 메서드를 재정의하여 수행하는 핸들러입니다.
TextWebSocketHandler는 텍스트 메시지만 처리하는 웹소켓 핸들러를 구현하는 데 편리한 기능을 제공하는 기본 클래스입니다.

내부를 보면 또 다른 AbstractWebSocketHandler를 상속하고 있다.

얘도 다양한 메서드를 갖고 있으며, WebSocketHandler 인터페이스를 구현하고 있다.

최종적으로 해당 인터페이스의 메서드를 구현함으로써 다양한 웹소켓 기능을 사용하는 것이다.
내가 해당 서비스에서 주로 사용하게 될 메서드를 알아보자.
handleTextMessage
- 클라이언트로부터 메시지가 도착했을 때 호출되는 메서드.
- session을 통해 접속한 유저의 정보를 파악, message를 통해 받은 메시지를 꺼내서 사용
afterConnectionEstablished
- 클라이언트와 연결이 성공했을 때 호출되는 메서드
afterConnectionClosed
- 클라이언트와 연결이 끊어졌을 때 호출되는 메서드
우선 위 3개의 메서드만 구현해서 테스트를 진행해보려고 한다.
또한 참여한 모든 세션에게 메시지를 전송해야 하므로 WebSocketSession을 리스트로 만들어서 현재 들어온 세션을 관리해 보자.
@Component
@Slf4j
public class WebSocketHandler extends TextWebSocketHandler {
private final List<WebSocketSession> sessions = new CopyOnWriteArrayList<>();
/**
* 클라이언트로부터 메시지가 도착했을 때 호출되는 메소드
* */
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// 클라이언트로부터 받은 메시지
String payload = message.getPayload();
log.info("클라이언트에게 온 메시지 : " + payload);
// 모든 클라이언트에게 메시지 전송
for(WebSocketSession webSocketSession : sessions){
if(session.isOpen()){
webSocketSession.sendMessage(new TextMessage("서버에서 보내는 메시지 : " + payload));
}
}
}
/**
* 클라이언트와 연결이 성공했을 때 호출되는 메소드
* */
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
sessions.add(session);
log.info("클라이언트와 연결 성공 : " + session.getId() + ", 접속 IP : " + session.getRemoteAddress().getHostName() + ", 접속 URI : " + session.getUri() + ", 접속 ID : " + session.getAttributes().get("userId"));
// 모든 클라이언트에게 메시지 전송
for(WebSocketSession webSocketSession : sessions){
if(session.isOpen()){
webSocketSession.sendMessage(new TextMessage("서버에서 보내는 메시지 : " + session.getId() + "님이 접속하셨습니다."));
}
}
}
/**
* 클라이언트와 연결이 끊겼을 때 호출되는 메소드
* */
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
sessions.remove(session);
log.info("클라이언트와 연결 끊김, 세션 ID : " + session.getId() + ", CloseStatus : " + status);
// 모든 클라이언트에게 메시지 전송
for(WebSocketSession webSocketSession : sessions){
if(session.isOpen()){
webSocketSession.sendMessage(new TextMessage("서버에서 보내는 메시지 : " + session.getId() + "님이 퇴장하셨습니다."));
}
}
}
}
유저가 연결하면 접속 메시지를 모두에게 전송
유저가 메시지 보내면 모든 유저에게 전송
유저가 연결을 해지하면 모두에게 해지 전송
해당 내용 테스트를 postman의 기능으로 한번 동작해 보겠습니다.
postman에는 간단하게 웹소켓 테스트를 진행할 수 있는 기능이 있습니다.

new 선택

웹소켓 선택

그러면 위와 같은 창이 나옵니다. 서버를 켠 상태로 여기에 ws://localhost:8080/ws 입력


정상적으로 연결된 것을 확인할 수 있습니다.
다른 유저를 또 입장시키려면 포스트맨을 2개 켜서 연결을 진행해 봅시다!

누군가 또 들어왔네요! 인사를 해봅시다

정상적으로 수신하는 걸 확인할 수 있습니다.
우선 백엔드에서 간단한 기초 설정은 완료했습니다.
'프로젝트 > RESTAPI 추천 서비스' 카테고리의 다른 글
코드 리팩토링(1) (0) | 2024.05.04 |
---|---|
jwt Filter 가독성 리팩토링 진행 (0) | 2024.04.13 |
어김없이 또 발생한 N+1 문제(요청 기록 조회 API) (0) | 2024.04.09 |
No validator could be found for constraint (Valid 에러 발생) (0) | 2024.04.08 |
(스프링부트 3.2.4로 진행됩니다)
웹소켓이라는 기술을 평소에 사용해보고 싶기도 했어서 이번 프로젝트에 한번 적용해보려고 한다.
내가 원하는 느낌은 자소설닷컴의 느낌과 비슷하다.

요런 느낌으로 로그인하면 하나의 채팅방을 통해 모두가 자유롭게 대화할 수 있는 장소를 하나 마련해보려고 한다.
(학습하면서 진행하는 부분이라 다음 내용이 언제 올라갈지 모르겠습니다.)
우선 웹소켓을 사용하냐 socket.io를 사용하냐의 문제가 있었다.
그전에 간단하게 네트워크에서 어떻게 실시간 통신을 해왔는지 살펴보자.
1. Polling
- 일정한 주기(시간)를 가지고 서버와 응답을 주고받는 방식
- 웹 자체가 영구 연결이 불가능하기에 매번 주고 받아야함.(서버 부하 문제 발생)
- 주기가 길면 실시간성이 매우 떨어진다.
2. Long Polling
- 폴링과 유사
- 서버에서 요청을 미리 받아 놓고, 클라이언트에서 사용이 불가하면 대기하다가 서버에서 클라이언트로 보낼 응답이 생긴다면 응답을 보내고 연결 종료
- 클라이언트는 다시 요청을 보내고 응답을 기다리는 방식
3. SSE (Server Sent Event)
- 클라이언트와 서버가 연결한 이후, 서버에서 클라이언트로 데이터를 보내주는 방식(단방향)
- 즉 서버에서만 송신이 가능하다는 특징.
4. 웹 소켓 (Web Socket)
- 실시간 양방향 데이터 전송을 위한 프로토콜
- 기존 http는 요청한 클라이언트에게만 응답이 가능하지만, ws 프로토콜을 통해 포트 공유 가능
- 최초 연결은 http, https로 연결을 진행
- 매우 빠르게 작동, 적은 데이터 이용
5. 소켓io (Socket.io)
- 브로드캐스팅(접속한 유저 모두에게 메시지 발송), 접속 끊어지면 재접속, 채팅방 등등 다양한 기능을 제공하는 라이브러리
- 유지보수 측면에서 이점이 많다.
나는 여기서 웹소켓을 선택했다.
아무래도 채팅방도 하나이기도 하고, 빠른 속도, 적은 데이터가 끌리기도 했다. 또 socket.io가 제공하는 기능들은 내가 직접 구현해야겠지만, 하나씩 알아보면 학습에 도움이 되지 않을까?라는 생각도 있다.
우선 먼저 웹소켓을 등록해야한다.
implementation 'org.springframework.boot:spring-boot-starter-websocket'
웹소켓을 관리하는 Config 하나 만든다.
@Configuration
@EnableWebSocket
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketConfigurer {
private final WebSocketHandler webSocketHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
// 클라이언트에서 접속할 웹소켓 주소를 "/ws"로 설정
registry.addHandler(webSocketHandler, "/ws").setAllowedOrigins("*");
}
}
우선 먼저 해주는 일이 웹소캣 핸들러를 등록하는 일이다. 밑에서 핸들러는 정의를 진행한다.
일단 웹소켓에 접속할 주소를 적어준다. /ws로 설정하면 ws://localhost:8080/ws 해당 주소로 요청을 보내면 웹소켓으로 접속을 진행하게 됩니다.
이후 요청을 컨트롤할 핸들러 작성
@Component
@Slf4j
public class WebSocketHandler extends TextWebSocketHandler {
private final List<WebSocketSession> sessions = new CopyOnWriteArrayList<>();
/**
* 클라이언트로부터 메시지가 도착했을 때 호출되는 메소드
* */
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// 클라이언트로부터 받은 메시지
String payload = message.getPayload();
log.info("클라이언트에게 온 메시지 : " + payload);
// 모든 클라이언트에게 메시지 전송
for(WebSocketSession webSocketSession : sessions){
if(session.isOpen()){
webSocketSession.sendMessage(new TextMessage("서버에서 보내는 메시지 : " + payload));
}
}
}
}
해당 핸들러는 TextWebSocketHandler의 내용을 상속받아서 다양한 메서드를 재정의하여 수행하는 핸들러입니다.
TextWebSocketHandler는 텍스트 메시지만 처리하는 웹소켓 핸들러를 구현하는 데 편리한 기능을 제공하는 기본 클래스입니다.

내부를 보면 또 다른 AbstractWebSocketHandler를 상속하고 있다.

얘도 다양한 메서드를 갖고 있으며, WebSocketHandler 인터페이스를 구현하고 있다.

최종적으로 해당 인터페이스의 메서드를 구현함으로써 다양한 웹소켓 기능을 사용하는 것이다.
내가 해당 서비스에서 주로 사용하게 될 메서드를 알아보자.
handleTextMessage
- 클라이언트로부터 메시지가 도착했을 때 호출되는 메서드.
- session을 통해 접속한 유저의 정보를 파악, message를 통해 받은 메시지를 꺼내서 사용
afterConnectionEstablished
- 클라이언트와 연결이 성공했을 때 호출되는 메서드
afterConnectionClosed
- 클라이언트와 연결이 끊어졌을 때 호출되는 메서드
우선 위 3개의 메서드만 구현해서 테스트를 진행해보려고 한다.
또한 참여한 모든 세션에게 메시지를 전송해야 하므로 WebSocketSession을 리스트로 만들어서 현재 들어온 세션을 관리해 보자.
@Component
@Slf4j
public class WebSocketHandler extends TextWebSocketHandler {
private final List<WebSocketSession> sessions = new CopyOnWriteArrayList<>();
/**
* 클라이언트로부터 메시지가 도착했을 때 호출되는 메소드
* */
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// 클라이언트로부터 받은 메시지
String payload = message.getPayload();
log.info("클라이언트에게 온 메시지 : " + payload);
// 모든 클라이언트에게 메시지 전송
for(WebSocketSession webSocketSession : sessions){
if(session.isOpen()){
webSocketSession.sendMessage(new TextMessage("서버에서 보내는 메시지 : " + payload));
}
}
}
/**
* 클라이언트와 연결이 성공했을 때 호출되는 메소드
* */
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
sessions.add(session);
log.info("클라이언트와 연결 성공 : " + session.getId() + ", 접속 IP : " + session.getRemoteAddress().getHostName() + ", 접속 URI : " + session.getUri() + ", 접속 ID : " + session.getAttributes().get("userId"));
// 모든 클라이언트에게 메시지 전송
for(WebSocketSession webSocketSession : sessions){
if(session.isOpen()){
webSocketSession.sendMessage(new TextMessage("서버에서 보내는 메시지 : " + session.getId() + "님이 접속하셨습니다."));
}
}
}
/**
* 클라이언트와 연결이 끊겼을 때 호출되는 메소드
* */
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
sessions.remove(session);
log.info("클라이언트와 연결 끊김, 세션 ID : " + session.getId() + ", CloseStatus : " + status);
// 모든 클라이언트에게 메시지 전송
for(WebSocketSession webSocketSession : sessions){
if(session.isOpen()){
webSocketSession.sendMessage(new TextMessage("서버에서 보내는 메시지 : " + session.getId() + "님이 퇴장하셨습니다."));
}
}
}
}
유저가 연결하면 접속 메시지를 모두에게 전송
유저가 메시지 보내면 모든 유저에게 전송
유저가 연결을 해지하면 모두에게 해지 전송
해당 내용 테스트를 postman의 기능으로 한번 동작해 보겠습니다.
postman에는 간단하게 웹소켓 테스트를 진행할 수 있는 기능이 있습니다.

new 선택

웹소켓 선택

그러면 위와 같은 창이 나옵니다. 서버를 켠 상태로 여기에 ws://localhost:8080/ws 입력


정상적으로 연결된 것을 확인할 수 있습니다.
다른 유저를 또 입장시키려면 포스트맨을 2개 켜서 연결을 진행해 봅시다!

누군가 또 들어왔네요! 인사를 해봅시다

정상적으로 수신하는 걸 확인할 수 있습니다.
우선 백엔드에서 간단한 기초 설정은 완료했습니다.
'프로젝트 > RESTAPI 추천 서비스' 카테고리의 다른 글
코드 리팩토링(1) (0) | 2024.05.04 |
---|---|
jwt Filter 가독성 리팩토링 진행 (0) | 2024.04.13 |
어김없이 또 발생한 N+1 문제(요청 기록 조회 API) (0) | 2024.04.09 |
No validator could be found for constraint (Valid 에러 발생) (0) | 2024.04.08 |