이 부분은 모두 하실 줄 안다고 생각하여 빠르게 넘기겠습니다. 간단하게 제가 프로젝트를 만들면서 추가한 의존성만 보여드리고 넘어가도록 하겠습니다
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
웹소켓을 사용하기 위해서는 하위 의존성을 추가하자.
implementation 'org.springframework.boot:spring-boot-starter-websocket'
프로젝트 생성 후 잘 생성되었는지 반드시 WebsocketApplication으로 실행해보자.
아래코드는 스프링 doc을 참고하여 구현합니다.
@Configuration
@EnableWebSocket
public class WebSocketConfiguration implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/myHandler").setAllowedOriginPatterns("*");
}
@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}
}
- @EnableWebSocket : 웹소켓 서버를 사용하도록 추가합니다.
- /myHandler : 웹소켓 서버의 endPoint를 "/myHandler" 로 설정합니다.
- setAllowedOriginPatterns("*") : 웹소켓 서버로의 요청을 모두 수용한다. (실제로는 제한할 것)
- myHandler() : 웹소켓 핸들러로 MyHandler 클래스를 설정한다.
MyHandler의 클래스는 다음과 같다. 간단히 TextWebSocketHandler extends하여 정의한다.
public class MyHandler extends TextWebSocketHandler {
//최초 연결 시
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
}
//양방향 데이터 통신할 떄 해당 메서드가 call 된다.
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
//do something
}
//웹소켓 종료
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
}
//통신 에러 발생 시
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
}
}
MyHandler에서는 4개의 메서드를 구현하는데
- afterConnectionEstablished: 최초 연결시 call 된다.
- handleTextMessage: 연결 후 메세지를 주고 받을 때 call 된다.
- afterConnectionClosed: 웹소켓이 끊키면 call 된다.
- handleTransportError: 통신 에러 발생시 call 된다.
여기까지 기초적인 설정은 끝났으니 postman으로 테스트를 진행하면서 메서드에 어떠한 데이터들이 들어오는지 확인해보자.
postman으로 websocket 테스트 하는 방법은 많은 블로그에서 소개하고 있으니 따로 적진 않겠습니다.
postman에서 websocket로 연결 요청을 보내면 다음과 같이 afterConnectionEstalished메서드에 break point가 걸리는 것을 확인할 수 있다.
해당 메서드의 session 인자의 데이터를 확인해보면 다음과 같다.
session 객체에 id가 보이는데, 이 session id를 key로 갖는 map을 통해 session을 관리해보도록 하자.
인스턴스 필드로 map을 선언하고, afterConnectionEstablished 메서드가 호출될 때 id 와 session을 map에 put 한다. 코드로 보면 다음과 같다.
private final Map<String, WebSocketSession> sessions = new HashMap<>();
//최초 연결 시
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
final String sessionId = session.getId();
sessions.put(sessionId, session);
}
다음으로는 최초 연결 시 session객체를 저장하면서, 저장되어있는 다른 session 객체들에게 알림을 보내는 코드를 추가해보자.
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
final String sessionId = session.getId();
final String enteredMessage = sessionId + "님이 입장하셨습니다.";
sessions.put(sessionId, session);
sessions.values().forEach((s) -> {
try {
if(!s.getId().equals(sessionId) && s.isOpen()) {
s.sendMessage(new TextMessage(enteredMessage));
}
} catch (IOException e) {}
});
}
여기까지 완료했다면 postman을 통해 tab을 2개를 열어서 코드 테스트를 진행해보자.
시나리오는 다음과 같다.
- 한쪽을 연결해둔 상태에서 다른 한 탭에서 웹소켓 연결 시 최초 연결했던 탭에 연결되었다는 메시지를 받아야 한다.
이제 다른 탭에서 새로 연결을 해보면~
위 화면과 같이 입장 메시지를 받을것을 확인할 수 있다.
다음으로는 연결된 웹소켓들이 메시지를 주고받을 때 call되는 메서드를 구현한다.
이것도 별 다르지 않게 연결되어있는 모든 session에게 메시지를 보낸다.
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
//do something
final String sessionId = session.getId();
sessions.values().forEach((s) -> {
if (!s.getId().equals(sessionId) && s.isOpen()) {
try {
s.sendMessage(message);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
}
동일하게 postman으로 테스트를 진행해보자. 이번에는 두 탭 모두 연결을 한 후, 한 탭에서 메시지를 보내 메시지가 오는지 확인해보자.
일반적인 text를 입력한 후 send를 보내면 다른 쪽에 메시지가 도착하는 것을 확인할 수 있다.
해당 메서드에서는 session이 끊기게 되면 map에 저장되어있는 객체를 remove 하고, 다른 접속자들에게 leave message를 보낸다.
해당 메서드의 인자 중 CloseStatus status에는 종료상태에 대해 정의되어 있으니 필요에 따라 분기를 통해 정의할 수 있다.
//웹소켓 종료
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
final String sessionId = session.getId();
final String leaveMessage = sessionId + "님이 떠났습니다.";
sessions.remove(sessionId); // 삭제
//메시지 전송
sessions.values().forEach((s) -> {
if (!s.getId().equals(sessionId) && s.isOpen()) {
try {
s.sendMessage(new TextMessage(leaveMessage));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
}
테스트로 잘 되는 것도 확인하였다.
간단하게 구현하긴 했지만 각각의 메서드에서 메시지를 전송하는 부분은 message를 제외하면 모두 같기 때문에 하나의 메서드를 정의하고 메서드를 call하도록 수정한다. 전체코드는 다음과 같다.
package com.sj.websocket.handler;
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 java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class MyHandler extends TextWebSocketHandler {
private final Map<String, WebSocketSession> sessions = new HashMap<>();
//최초 연결 시
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
final String sessionId = session.getId();
final String enteredMessage = sessionId + "님이 입장하셨습니다.";
sessions.put(sessionId, session);
sendMessage(sessionId, new TextMessage(enteredMessage));
}
//양방향 데이터 통신할 떄 해당 메서드가 call 된다.
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
//do something
final String sessionId = session.getId();
sendMessage(sessionId, message);
}
//웹소켓 종료
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
final String sessionId = session.getId();
final String leaveMessage = sessionId + "님이 떠났습니다.";
sessions.remove(sessionId); // 삭제
sendMessage(sessionId, new TextMessage(leaveMessage));
}
//통신 에러 발생 시
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {}
private void sendMessage(String sessionId, WebSocketMessage<?> message) {
sessions.values().forEach(s -> {
if(!s.getId().equals(sessionId) && s.isOpen()) {
try {
s.sendMessage(message);
} catch (IOException e) {}
}
});
}
}
websocket을 가지고 간단하게 구현하였는데, 위 코드는 입장 시, 퇴장 시, 메시지 보낼 때 모두 다른 세션에 메시지를 보내고 있다. 만약, 특정 사람에게 보내거나, 그룹에 보내야 한다면 최초 웹소켓 연결 시 서버에 object 형식의 데이터를 보내어 그룹을 짓거나 특정 사용자에게 메시지를 보낼 수 있다.
위의 코드는 websocket repository 에서 볼 수 있습니다.
https://brunch.co.kr/@springboot/695
https://docs.spring.io/spring-framework/reference/web/websocket/server.html