일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- Java #백준 #코딩테스트
- 15552
- 알고리즘
- 카카오클라우드스쿨
- node
- 2447
- SpringTokenizer
- Spring
- sequelize
- 9020
- kakaocloud
- 백준
- 11054
- 파이썬
- Java #오븐시계 #백준
- 카카오 클라우드 스쿨
- 7568
- 1110
- Spring #Java #Spring Boot
- kakaocloudschool
- Java #코딩테스트
- 코딩테스트
- 11053
- java
- Spring #Java #Spring Boot #싱글톤
- 24479
- Spring #Java #Spring Boot #@BeforeEach #@AfterEach
- boj
- Spring #Spring Boot #Java
- python
- Today
- Total
YoungSoo
[WebSocket] 명령과 조회를 분리하는 CQRS로 개발하기 - 3 본문
지난 게시물에서는 WebSocket과 Stomp를 사용해서 pub/sub 방식으로 간단한 채팅을 구현하는 실습을 진행했습니다.
이번 게시물에서는 웹 소켓과 Rest API를 통해서 구현한 기본적인 채팅 서비스를 구현해 보겠습니다.
사용자 입장의 채팅 서비스
채팅 서비스를 사용자 입장에서 처음 채팅을 시작할 때 게시글에 있는 채팅 보내기를 통해 채팅방을 구성하게 됩니다. 이때 채팅방과 채팅 참여자가 테이블에 등록이 됩니다. 채팅 목록을 조회하고 채팅방을 선택할 때 WebSocket 연결을 하고 Stomp를 통해 pub/sub 형식의 채팅이 이어지게 됩니다. 이때, 채팅방 고유 번호를 사용해 각 채팅방마다 발신자와 구독자에게만 메시지를 전달하게 됩니다.
필요 의존성 추가
// Chat
implementation 'org.springframework.boot:spring-boot-starter-websocket'
// implementation 'org.springframework.kafka:spring-kafka'
implementation 'org.webjars:stomp-websocket:2.3.3'
Kafka는 아직 사용하지 않았지만 추후에 기능을 추가하기 위해 추가한 것을 참고해 주시면 감사하겠습니다.
WebSocketConfig 추가
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.setApplicationDestinationPrefixes("/pub"); // 메세지 받기
config.enableSimpleBroker("/sub"); // 메세지 보내기
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry
.addEndpoint("/chats/furry")
.setAllowedOriginPatterns("*")
.withSockJS();
}
}
Stomp를 사용하기 위해 WebSocketMessageBrokerConfigurer를 구현할 WebSocketConfig를 만들어 메시지를 받고 다시 보내는 주소를 설정해 주었습니다. 또한 WebSocket 연결을 위한 Endpoint를 설정하고 CORS 설정을 해주었습니다.
Controller
@MessageMapping("/chats/{chat_room_id}")
@SendTo("/sub/chats/{chat_room_id}")
public ApiResponse<ChatMessageResponseDTO> chatting(ChatMessageRequestDTO chatMessageRequestDTO, @DestinationVariable(value = "chat_room_id") Long chat_room_id, SimpMessageHeaderAccessor headerAccessor) {
// Token 추출
String accessToken = headerAccessor.getFirstNativeHeader("Authorization");
try {
ChatMessageResponseDTO chatMessageResponseDTO = chatMessageService.saveChatMessage(chat_room_id, chatMessageRequestDTO, accessToken);
return ApiResponse.success("success", chatMessageResponseDTO);
}catch (Exception e){
log.error("메시지 전송 실패: " + e.getMessage(), e);
return ApiResponse.fail(400, "메시지 전송 실패: " + e.getMessage());
}
}
Controller에서 메시지를 받고 Service에서 로직을 처리한 뒤에 메시지 정보가 리턴하게 됩니다. 이러한 과정을 끊김 없이 진행하기 위해 Http 메서드가 아닌 WebSocket을 통해 진행하게 되었습니다.
Service
// 메시지 저장 메서드
public ChatMessageResponseDTO saveChatMessage(final Long chatRoomId, final ChatMessageRequestDTO chatMessageRequestDTO, String accessToken) throws Exception{
try {
JwtResponse jwtResponse = tokenService.getMember(accessToken);
ChatRoom chatRoom = chatRoomService.findChatRoom(chatRoomId);
// Entity로 변환 후 저장
ChatMessage chatMessage = chatMessageRequestDTO.dtoToEntity(chatMessageRequestDTO, jwtResponse, chatRoom);
chatMessageRepository.save(chatMessage);
// DTO로 변환 후 응답
return chatMessage.entityToDTO(chatMessage);
} catch (Exception e){
log.error("ChatMessageService 오류 발생: " + e.getMessage(), e);
throw new Exception("ChatMessageService 오류 발생: " + e.getMessage());
}
}
함께 보내온 메시지와 채팅방 고유 번호 그리고 토큰을 받아 로직을 진행해 줍니다. JWT 토큰의 유효성을 검증하고 메시지를 저장한 후 메시지를 리턴해주게 됩니다. 그렇게 되면 JPA의 Save 메서드를 통해 ChatMessage Entity를 저장해 주게 되고 이 결과를 리턴해주면 종료가 됩니다.
Tests
저는 이러한 과정을 진행하기 전에 Test를 통해 먼저 하게 되었습니다. Test를 하는 과정에서 ChatMessage에 해당하는 ChatRoom이 없었기에 ChatMessage를 같이 저장해 주기 위해 cascadeType을 All로 저장해 주어 실제 개발을 하는 과정에서 detached entity passed to persist 해당 오류가 발생했었습니다. 이 오류는 이미 저장되어 있는 ChatRoom 테이블의 값을 읽어와 다시 저장하려고 해 발생하는 오류였고, 오류를 해결하는 과정에서 cascade 설정이 원인인 것을 알게 되었습니다. 이것을 해결하기 위해 cascade를 지웠고, Test 또한 고쳐주었습니다. 하지만 잘못된 Test를 한 것이다라는 생각이 들었고, Test에 대한 공부를 해야겠다는 생각이 들었습니다.
채팅 구현

이렇게 채팅 기능을 구현하게 되었습니다. 아직 추가해야할 기능들이 많지만 기본적인 채팅이 가능하게 되었습니다. ㅎㅎ
이후에는 읽음 기능을 추가하고 알림 기능을 추가하며 점차 기능을 늘려 나가려고 합니다.
글을 마치며
현재는 조회를 CQRS가 아닌 기존의 방식대로 구현했지만 더 빠르게 구성하기 위해 NoSQL과 MessageQueue를 통해 CQRS를 구현해 더 빠르게 읽어오도록 할 예정입니다. 다음 게시물에서 CQRS 도입과 읽음 표시에 대한 기능을 추가하는 글을 작성하겠습니다. 읽어주셔서 감사합니다!
'프로젝트' 카테고리의 다른 글
[오류 해결] not upgraded to websocket (0) | 2023.10.07 |
---|---|
[WebSocket] 명령과 조회를 분리하는 CQRS로 개발하기 - 4 (0) | 2023.07.29 |
[WebSocket] 명령과 조회를 분리하는 CQRS로 개발하기 - 2 (0) | 2023.07.05 |
[CQRS] 명령과 조회를 분리하는 CQRS로 개발하기 - 1 (0) | 2023.07.04 |
[Furry_Friend_Chat] 채팅 서비스 요구사항 분석 및 설계 (0) | 2023.07.03 |