웹소켓에 대해 구현을 해보려고 합니다.
WebSocket
웹소켓을 사용해서 프로젝트를 진행하려고 합니다. 처음 사용을 해보고 주로 실시간을 위해 사용을 한다고 알고 있습니다. (채팅, 주식 등) 그래서 정리를 한번하고 구현에 들어가려고 합니다. H
classruntime.tistory.com
스프링부트에서 웹소켓을 사용하기 위해서 Dependency를 추가합니다.
build.gradle
// WebSocket
implementation 'org.springframework.boot:spring-boot-starter-websocket'
그리고 웹소켓 연결을 위한 config 파일을 생성을 합니다.
WebSocketConfig.java
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(webSocketHandler(), "/ws/chat").setAllowedOrigins("*");
}
@Bean
public WebSocketHandler webSocketHandler() {
return new WebSocketHandler();
}
}
endpoint를 설정을 해줍니다. 그리고 CORS 설정을 모두 허용으로 해줍니다.
기본적으로 WebSocket은 서버와 클라이언트가 1:N의 관계를 가집니다.
서버에서 여러 클라이언트의 메세지를 처리 하기 위한 Handler가 필요합니다.
그래서 저희는 WebSocketHandler 클래스를 생성하고 TextWebSocketHandler를 상속 받아서
필요한 메소드를 오버라이드 하도록 하겠습니다.
필요한 메소드는 (연결, 해제, 오류, 통신) 이렇게 있습니다.
WebSocketHandler.java
@Slf4j
@RequiredArgsConstructor
public class WebSocketHandler extends TextWebSocketHandler {
private static List<WebSocketSession> list = new ArrayList<>();
// 웹소켓 연결
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
}
// 웹소켓 연결 해제
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
}
// 양방향 데이터 통신
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
}
// 소켓 통신 에러
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
}
}
다음과 같이 4가지 메소드를 오버라이드 합니다.
이제 채팅 구현에 들어가도록 하겠습니다.
먼저 입장을 했을 때와 퇴장을 했을 때 들어오고 나간 본인을 제외한 나머지 인원에게메세지를 보내려고 합니다.
// 웹소켓 연결
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 새로운 인원 연결
list.add(session);
log.info("웹소켓 연결 = {}", session);
// 새로운 인원 전체 알림
TextMessage textMessage = new TextMessage(session.getId() + " 님이 입장");
list.forEach(s -> {
try {
if (!s.getId().equals(session.getId())) {
s.sendMessage(textMessage);
}
} catch (Exception e) {
// TODO : Exception
log.warn("소켓 입장 오류");
}
});
}
// 웹소켓 연결 해제
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
list.remove(session);
log.info("웹소켓 연결 해제 = {}", session);
// 퇴장 인원 전체 알림
TextMessage textMessage = new TextMessage(session.getId() + " 님이 퇴장");
list.forEach(s -> {
try {
if (!s.getId().equals(session.getId())) {
s.sendMessage(textMessage);
}
} catch (Exception e) {
// TODO : Exception
log.warn("소켓 퇴장 오류");
}
});
}
다음과 같이 작성을 하게 되면 session id가 동일하지 않다면 메세지를 전송하여 입장과 퇴장을 알립니다.
아 참고로 저는 크롬 확장 프로그램인 Simple WebSocket Client를 사용하여 테스트 중 입니다.
// 양방향 데이터 통신
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String payload = message.getPayload();
log.info("payload = {}", payload);
list.forEach(s -> {
try {
s.sendMessage(message);
} catch (IOException e) {
// TODO : Exception
log.warn("소켓 메세지 전송 오류");
}
});
}
이렇게 message를 보내보도록 하겠습니다.
일단 내가 보낸 메세지는 주황색으로 표시됩니다.
그런데 제가 원하던 방식이랑 좀 다릅니다 "안녕하세요1"이 한번만 출력이 되야 하는데 두번 출력 되고 있습니다.
그런데 상대방한텐 하나의 "안녕하세요1"이 보입니다. 이걸로 봤을 땐 테스트 프로그램 자체에서 내가 보낸 메세지를 출력 해주고
저는 제가 보낸 글을 본인과 다른 상대방이 보이게 만들어서 두개로 출력되는 것 같습니다.
그래서 실제로 채팅이 되려면 현재처럼 2개가 보여야 하는게 맞다고 생각이 됩니다.
만약 이렇게 출력이 되는게 보기 싫다 ! 하시면 입장과 퇴장에서 사용한 방식과 동일하게 메세지를 보낸 본인은 안보이게 하면 될 것 같습니다.
이제 가장 기본적인 채팅 형태가 생긴 것 같습니다.
하지만 저는 채팅방을 생성하여 그 방안에 입장한 사람들만 채팅을 진행하도록 하고 싶습니다.
어떤 방식을 하면 될까 생각을 했습니다.
맨처음에 채팅방을 생성한 인원이 채팅방의 아이디를 가지고 있고 다른 인원이 해당 채팅방 아이디에 맞게 접속이 되는 형식을 생각 했습니다.
// 웹소켓 연결
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 새로운 인원 연결
log.info("웹소켓 연결 = {}", session);
String url = Objects.requireNonNull(session.getUri()).toString();
int index = url.indexOf("?");
String roomId = url.substring(index + 1);
TextMessage textMessage = new TextMessage(session.getId() + " 님이 입장");
List<WebSocketSession> roomSessions = sessions.getOrDefault(roomId, new ArrayList<>());
roomSessions.add(session);
roomSessions.forEach(s -> {
try {
s.sendMessage(textMessage);
} catch (IOException e) {
log.warn("Error sending message: {}", e.toString());
}
});
sessions.put(roomId, roomSessions);
}
이렇게 WebSocket endpoint에 쿼리스트링으로 방번호를 넘겨서 해당 방이 있다면 기존에 있던 방에 세션을 추가하고
없다면 새로 리스트를 생성 후에 맵에 추가를 하는 방식으로 진행을 할까 했습니다.
여기서 문제는 해당 방 번호를 쿼리스트링으로 넘어오는데 방 번호를 클라이언트가 어떻게 알고 넘겨줄까란 생각이 들었습니다.
그래서 저는 방을 생성하는 API를 만들고 DB에 넣어서 해당 값을 꺼내서 새로운 인원을 연결 시켜주면 될 것 같다고 생각이 들었습니다.
ChatRoom.java
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class ChatRoom extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String roomId;
@Builder
public ChatRoom(String roomId) {
this.roomId = roomId;
}
public static ChatRoom createChatRoom(String roomId) {
return ChatRoom.builder()
.roomId(roomId)
.build();
}
}
도메인을 생성을 해주고 JPA Repository도 생성을 했습니다.
ChatRoomService.java
@Slf4j
@Service
@RequiredArgsConstructor
public class ChatRoomService {
private final ChatRoomRepository chatRoomRepository;
public String createChatRoom() {
log.info("채팅 룸 생성");
String randomId = UUID.randomUUID().toString();
ChatRoom chatRoom = ChatRoom.createChatRoom(randomId);
chatRoomRepository.save(chatRoom);
log.debug("chatRoom Id = {}", chatRoom.getId());
return chatRoom.getRoomId();
}
}
채팅방 생성 하는 서비스를 만들고 반환값은 방 아이디를 반환을 해줍니다.
컨트롤러도 작성을 해줍니다.
ChatRoomController.java
@Slf4j
@RestController
@RequiredArgsConstructor
public class ChatRoomController {
private final ChatRoomService chatRoomService;
private final CommonService commonService;
@PostMapping("/chat/room")
@ResponseStatus(HttpStatus.CREATED)
public ResponseEntity<Object> createChatRoom() {
log.debug("채팅 - 채팅방 개설 API 호출");
String roomId = chatRoomService.createChatRoom();
CommonResponseDto<Object> commonResponseDto =
commonService.successResponse(
CHAT_ROOM_CREATE_SUCCESS.getDescription(), roomId);
return ResponseEntity.status(HttpStatus.CREATED).body(commonResponseDto);
}
}
반환값으로 채팅방 아이디를 넣어줍니다.
이렇게 되면 채팅방 생성 후 소켓 연동을 할 때 쿼리스트링에 해당 UUID 값을 넣어줄 수 있습니다.
이제 채팅방을 여러 개를 만들 수 있고 각각의 방으로 입장을 할 수 있습니다.
방의 UUID를 다른 인원에게 전달하여 채팅방에 들어오도록 할 수도 있습니다.
WebSocket만 사용하여 방을 구분하는 것까진 완료를 했지만 저는 더 확실하게 관리를 하고 싶었습니다.
그래서 찾아보니 STOMP와 MessageBroker를 사용하는 방식이 있었습니다.
해당 기능을 사용하여 메세지를 관리하고 채팅방을 관리하여 채팅을 완성 시켜보도록 하겠습니다.
Chats 프로젝트 (7) - OAuth2.0, Login
로그인 화면을 메인화면으로 변경하고 로그인 버튼에 API 연동을 진행 해보겠습니다. 로그인 화면을 메인으로 진행하는건 index.html 파일을 해당 로그인에 사용하던 페이지로 변경하면 됩니다. log
classruntime.tistory.com
'Project > Chats' 카테고리의 다른 글
Chat Project (10) - STOMP Header, JWT Token (1) | 2024.02.05 |
---|---|
Chats 프로젝트 (9) - STOMP, MongoDB (0) | 2024.01.24 |
Chats 프로젝트 (7) - OAuth2.0, Login (0) | 2024.01.16 |
Chats 프로젝트 (6) - JWT, Login, Redis (0) | 2024.01.15 |
Chats 프로젝트 (5) - Spring Security, Login (2) | 2024.01.11 |