티스토리 뷰
이번에는 어떤기능을 게시판에 넣어볼까 하다가
게시판과는 크게 상관없지만 구현해보고 싶었던 기능인 채팅기능을 한번 해볼까 합니다.
채팅을 구현하기에 앞서 웹소켓에 대해 간단히 짚고 넘어가 봅시다.
- WebSocket
WebSocket은 Client와 Server사이 전이중통신(Full-Duplex)를 제공합니다.
HTTP 통신은 기본적으로 비연결성(Connectless) 통신이므로, Client에게 한 번 보내고 나면 연결이 끊겨 지속적으로 데이터를 주고 받을 수 없습니다.
그에 반해 WebSocket은 연결지향성으로 처음에만 HTTP로 HandShaking을 통해 연결을 맺으면 그 연결을 계속 유지합니다.
채팅뿐 아니라 주식웹사이트 등 지속적으로 사용자의 요청없이도 동적인 정보를 표시해야 할 때 사용합니다.
구현하려는 채팅기능
- 채팅방이 여러개 있을 수 있다.
- 사용자가 채팅방을 만들 수 있다.
- 채팅방하나에 다수의 유저가 들어올 수 있음.
- 채팅방의 마지막 사람이 나가면 해당 채팅방을 지운다.
기본적인 websocket만 사용해 채팅을 구현합니다.
원래 채팅을 위해서는 캐싱을 통해 채팅 내용을 저장하고 이를 모아서 DB에 저장하는 방식을 사용합니다만, 이건 나중에 따로 정리해 보도록 하고
이번에는 단지 연결성에만 집중해 보도록 하겠습니다.
코드를 보기전에 대략적인 느낌을 알아봅시다.
- ws://도메인/ws/chat 으로 웹소켓을 연결하고
- json데이터를 보냅니다.
{
"type":"ENTER", //(ENTER,TALK)
"roomId":"e4f57c65-0b0f-4972-b20c-4a024a0a4f81", // ( 해당채팅방의 UUID)
"sender":"사용자1", //(사용자 이름)
"message":"message" // 메세지
}
- 서비스에서 roomId로 방을 찾아오고
- 핸들러를 통해 type:ENTER 라면 찾아온 (ChatRoom)방에 세션을 추가되며 입장하게 됩니다..
- type을 TALK로 변경하여 데이터를 보내면 핸들러를 통해 메세지가 보내집니다.
- 서비스에서 채팅방들을 관리합니다.
Spring에서 웹소켓 사용하기
*자세한 설명은 주석에 적어놓았습니다.
의존성을 추가합니다.
implementation 'org.springframework.boot:spring-boot-starter-websocket'
먼저 웹소켓관련 설정을 해줍시다.
@RequiredArgsConstructor
@Configuration
@EnableWebSocket
public class WebSockConfig implements WebSocketConfigurer {
private final WebSocketHandler webSocketHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
/*
* 스프링에서 웹소켓을 사용하기 위해서 클라이언트가 보내는 통신을 처리할 핸들러가 필요하다
* 직접 구현한 웹소켓 핸들러 (webSocketHandler)를 웹소켓이 연결될 때, Handshake 할 주소 (/ws/chat)와 함께 addHandler 메소드의 인자로 넣어준다.
*/
registry.addHandler(webSocketHandler, "ws/chat")
.setAllowedOrigins("*");//CORS 설정
}
}
@EnableWebSocket 으로 웹소켓을 활성화 시키고 WebSocketCongifurer 구현체를 만들어 줍니다.
스프링에서 통신을 처리할 핸들러가 필요한데 아래에서 구현합니다.addHandler에 핸들러 말고도 연결을 맺을 주소로 "ws/chat"을 사용했습니다.웹소켓을 연결하려면 ws://도메인/ws/chat 을 사용하면 됩니다.
핸들러를 작성합니다.
@Slf4j
@RequiredArgsConstructor
@Component
public class WebSocketHandler extends TextWebSocketHandler {
private final ObjectMapper objectMapper;
private final ChatService chatService;
private ChatRoom chatRoom;
private String roomId;
//웹소켓연결되었을때
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
log.info("웹소켓이 연결됨");
}
//양방향데이터 통신
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
//메세지의 내용을 읽어
String payload = message.getPayload();
log.info("{}", payload);
//ChatMessage 타입으로 변환하고
ChatMessage chatMessage = objectMapper.readValue(payload, ChatMessage.class);
//메세지에 포함된 해당룸의 UUID를 가져온다
roomId = chatMessage.getRoomId();
//가져온 UUID로 ChatRoom 객체를 찾고
chatRoom = chatService.findRoomById(chatMessage.getRoomId());
//메세지 타입에 따라 로직을 결정
chatRoom.handlerActions(session, chatMessage, chatService);
}
//웹소켓 닫혔을때
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
//웹소켓이 닫히면(해당 채팅방을 나가거나, 앱을 종료했을 때)
//해당 세션을 제거
chatRoom.getSessions().remove(session);
//마지막남은 한명이 나가고 session count 가 0이 되면 해당 방을 제거
chatService.deleteRoom(roomId);
log.info("웹소켓이 닫힘");
}
}
통신에는 JSON을 이용하기때문에 ObjectMapper로 값을 가져와 사용합니다.
- afterConnectionEstablished: 웹소켓을 연결하는 순간 동작.
- handleTextMessage: 웹소켓이 연결되어 있을 때 데이터를 SEND하면 동작.
- afterConnectionClosed: 웹소켓을 Close했을 때 동작.
ChatMessage DTO
@Getter
@Setter
public class ChatMessage {
public enum MessageType{
ENTER, TALK
}
private MessageType type;
private String roomId;
private String sender;
private String message;
}
json데이터를 ChatMessage로 변환하는데 사용합니다.
ChatRoom
@Getter
public class ChatRoom {
private String roomId;
private String name;
private Set<WebSocketSession> sessions = new HashSet<>();
@Builder
public ChatRoom(String roomId, String name) {
this.roomId = roomId;
this.name = name;
}
public void handlerActions(WebSocketSession session, ChatMessage chatMessage, ChatService chatService) {
// message 에 담긴 타입을 확인한다.
// 이때 message 에서 getType 으로 가져온 내용이
// ChatDTO 의 열거형인 MessageType 안에 있는 ENTER 과 동일한 값이라면
if (chatMessage.getType().equals(ChatMessage.MessageType.ENTER)) {
// sessions 에 넘어온 session 을 담고,
sessions.add(session);
// message 에는 입장하였다는 메시지를 띄운다
chatMessage.setMessage(chatMessage.getSender() + "님이 입장했습니다.");
sendMessage(chatMessage, chatService);
} else if (chatMessage.getType().equals(ChatMessage.MessageType.TALK)){
//메세지 보내기
chatMessage.setMessage(chatMessage.getMessage());
sendMessage(chatMessage, chatService);
}
}
private <T> void sendMessage(T message, ChatService chatService) {
sessions.parallelStream()
.forEach(session -> chatService.sendMessage(session, message));
}
}
ChatRoom은 오픈채팅방의 이름과 고유값 그리고 연결될 사용자의 세션Set이 필요합니다.
ChatService
@Slf4j
@RequiredArgsConstructor
@Service
public class ChatService {
private final ObjectMapper objectMapper;
private Map<String, ChatRoom> chatRooms;
@PostConstruct
private void init() {
chatRooms = new LinkedHashMap<>();
}
//활성화된 모든 채팅방을 조회
public List<ChatDto> findAllRoom() {
List<ChatDto> collect = chatRooms.values().stream().map(chatRoom -> new ChatDto(chatRoom.getRoomId(), chatRoom.getName(), (long) chatRoom.getSessions().size())).collect(Collectors.toList());
return collect;
}
//채팅방 하나를 조회
public ChatRoom findRoomById(String roomId) {
return chatRooms.get(roomId);
}
//새로운 방 생성
public ChatRoom createRoom(String name) {
String randomId = UUID.randomUUID().toString();
ChatRoom chatRoom = ChatRoom.builder()
.roomId(randomId)
.name(name)
.build();
chatRooms.put(randomId, chatRoom);
return chatRoom;
}
//방 삭제
public void deleteRoom(String roomId) {
ChatRoom chatRoom = findRoomById(roomId);
//해당방에 아무도 없다면 자동 삭제
if(chatRoom.getSessions().size() == 0) chatRooms.remove(roomId);
}
public <T> void sendMessage(WebSocketSession session, T message) {
try{
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(message)));
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
서비스에서 방 생성, 조회, 삭제를 구현합니다.
Controller
@RequiredArgsConstructor
@RestController
@RequestMapping("/chat")
public class ChatController {
private final ChatService chatService;
@PostMapping
public ChatRoom createRoom(@RequestParam String name) {
return chatService.createRoom(name);
}
@DeleteMapping
public void deleteRoom(@RequestParam String name) {
chatService.deleteRoom(name);
}
@GetMapping
public Result findAllRoom() {
List<ChatDto> allRoom = chatService.findAllRoom();
return new Result(allRoom, allRoom.size());
}
}
크롬확장프로그램인 Simple Web Socket Client로 테스트를 해볼 수 있습니다.
이제 앱을 만들고 확인해 봅시다.
용량제한때문에 잘 안보이네요...
'Spring > 게시판API서버' 카테고리의 다른 글
[Spring] 게시판 API (9) PushAlarm FCM 활용 (0) | 2023.03.27 |
---|---|
[Spring] 게시판 API (8) 댓글과 대댓글 (0) | 2023.03.23 |
[Spring] 게시판 API (7) 파일(이미지) 업로드 (0) | 2023.03.17 |
[Spring] 게시판 API (6-2) JWT 적용 (0) | 2023.03.13 |
[Spring] 게시판 API (6-1) Spring Security 적용하기 (0) | 2023.03.13 |
- Total
- Today
- Yesterday
- ChattingApp
- 웹소켓 채팅
- Authentication
- MessageBroker
- Bcypt
- Spring
- 푸시알림동작원리
- ERD설계
- authorization
- bcrypt
- FCM
- Kubernetes
- Security
- Stomp RabbitMQ
- Spring 채팅
- Stomp Kafka
- loadbalancing
- Cache
- Spring Stomp
- Spring WebSocket
- 로컬캐시
- nativeQuery
- 게시판 채팅
- Vmmem종료
- springboot
- Flutter
- Vmmem
- Spring RabbitMQ
- spring orphan
- Spring 대댓글
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |