보라코딩
SpringBoot + React 채팅 구현 (웹소켓, stomp, redis) 본문
WebSocketConfig (WebSocket, Stomp 설정)
implementation 'org.springframework.boot:spring-boot-starter-websocket'
@Configuration // Config로 지정
@EnableWebSocketMessageBroker // 웹소켓 사용하는 브로커 활성화
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config){
// 클라이언트에게 topic으로 시작하는 것에 대한 구독 기능 제공
config.enableSimpleBroker("/sub");
// 클라이언트에서 서버로 메시지 보낼 때 app 경로 사용하도록 설정
config.setApplicationDestinationPrefixes("/pub");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry){
// ws 엔드포인트 등록하고 모든 도메인에서 접근 허용, SockJS 사용해서 브라우저가 웹소켓 지원하지 않을때 폴백 옵션 활성화(다른 수단으로 통신)
registry.addEndpoint("/ws").setAllowedOriginPatterns("*").withSockJS();
}
}
RedisConfig (Redis 설정)
Redis 설치 필요
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
application.properties에 설정 필요
spring.redis.host=localhost
spring.redis.port=6379
Spring Data Redis 는 RedisTemplate와 RedisRepository 이용해서 Redis에 접근
- RedisTemplate : 스프링 프레임워크에서 제공하는 Redis와 상호작용하는 도구. Redis와 통신 담당
- Key-Value 형태로 저장하며 객체를 직렬화해서 저장하고, Jackson 라이브러리 사용해서 JSON형식으로 객체 직렬화
- Jackson 라이브러리 : Java 객체를 JSON 형식으로 직렬화, JSON을 다시 Java 객체로 역직렬화
@Configuration
public class RedisConfig {
private final Logger log = LoggerFactory.getLogger(getClass());
@Bean
public RedisTemplate<String, ChattingMessage> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// RedisTemplate 빈을 생성하는 메서드
RedisTemplate<String, ChattingMessage> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
// Key Serializer를 StringRedisSerializer로 설정
template.setKeySerializer(new StringRedisSerializer());
// Value Serializer를 Jackson2JsonRedisSerializer로 설정
Jackson2JsonRedisSerializer<ChattingMessage> jsonSerializer = new Jackson2JsonRedisSerializer<>(ChattingMessage.class);
template.setValueSerializer(jsonSerializer);
// Hash Key Serializer를 StringRedisSerializer로 설정
template.setHashKeySerializer(new StringRedisSerializer());
// Hash Value Serializer를 Jackson2JsonRedisSerializer로 설정
template.setHashValueSerializer(jsonSerializer);
// 로깅: RedisTemplate이 성공적으로 구성되었음을 로그로 출력
log.info("RedisTemplate configured successfully");
return template;
}
}
ChatService (Redis 저장 및 조회 Service)
- MESSAGE_KEY : 모든 채팅방의 메시지를 redis에 저장할 때 사용되는 키 (고유한 식별자)
=> 각 채팅방의 키는 messages:535 이런 식으로 저장됨
- redisTemplate.opsForList().rightPush(roomKey, message);
: Redis의 리스트는 여러 개의 요소를 순서대로 저장할 수 있는 데이터 구조.
각 요소는 인덱스를 가짐. 리스트의 왼쪽 끝이나 오른쪽 끝에 새로운 요소를 추가 가능
rightPush 메서드는 리스트의 오른쪽 끝에 새로운 요소를 추가
@Service
public class ChatService {
@Autowired
private RedisTemplate<String, ChattingMessage> redisTemplate;
private final Logger log = LoggerFactory.getLogger(getClass());
// Redis에 저장할 메시지의 키
private static final String MESSAGE_KEY = "messages";
// 메시지 저장 메서드
public void saveMessage(ChattingMessage message){
log.info("ChatService_saveMessage : " + message);
// 각 채팅방에 대한 별도의 키 생성
String roomKey = MESSAGE_KEY + ":" + message.getRoomNo();
// 해당 채팅방의 메시지 목록에 새로운 메시지 추가
redisTemplate.opsForList().rightPush(roomKey, message);
}
// 전체 채팅방의 메시지 목록 조회 메서드
public List<ChattingMessage> getMessages() {
log.info("ChatService_getMessages");
Long size = redisTemplate.opsForList().size(MESSAGE_KEY);
if (size != null) {
return redisTemplate.opsForList().range(MESSAGE_KEY, 0, size - 1);
}
return Collections.emptyList();
}
// 특정 채팅방의 메시지 목록 조회 메서드
public List<ChattingMessage> getMessagesByRoom(String roomNo) {
log.info("ChatService_getMessagesByRoom : "+ roomNo);
String roomKey = MESSAGE_KEY + ":" + roomNo;
Long size = redisTemplate.opsForList().size(roomKey);
log.info("Room key: {}, Message count: {}", roomKey, size);
if (size != null) {
List<ChattingMessage> messages = redisTemplate.opsForList().range(roomKey, 0, size - 1);
log.info("Messages retrieved: {}", messages);
return messages;
}
log.info("No messages found.");
return Collections.emptyList();
}
}
WebSocketController
- SimpleMessagingTemplate : STOMP(메시징 위한 간단, 유연한 프로토콜로 websocket 위에서 동작)를 지원
- 역할 : convertAndSend 메서드 사용하여 메시지 전송
@RestController
public class WebSocketController {
@Autowired
private SimpMessagingTemplate simpMessagingTemplate;
@Autowired
private ChatService chatService;
private final Logger log = LoggerFactory.getLogger(getClass());
// 클라이언트에서 특정 채팅방의 메시지 조회
@GetMapping("/api/messages/{roomNo}")
public List<ChattingMessage> getMessagesByRoom(@PathVariable String roomNo) {
log.info("Received request to get messages for room: {}", roomNo);
return chatService.getMessagesByRoom(roomNo);
}
// WebSocket에서 사용하는 메시지 매핑 : 채팅방 입장 메시지 처리
@MessageMapping("/chat/join/{roomNo}")
public void join(@DestinationVariable String roomNo, @Payload ChattingMessage chatDto) {
log.info("User '{}' joined the room '{}'", chatDto.getUser(), roomNo);
// 채팅방 입장 메시지 저장
chatService.saveMessage(chatDto);
// 채팅방 입장 메시지를 해당 채팅방의 구독자에게 전송
chatDto.setMessage(chatDto.getUser() + "님이 입장하셨습니다.");
simpMessagingTemplate.convertAndSend("/sub/chat/join/" + roomNo, chatDto);
}
// WebSocket에서 사용하는 메시지 매핑 : 채팅방 메시지 전송 처리
@MessageMapping("/chat/message/{roomNo}")
public void sendMessage(@DestinationVariable String roomNo, @Payload ChattingMessage chatDto) {
log.info("User '{}' sent a message in the room '{}': '{}'", chatDto.getUser(), roomNo, chatDto.getMessage());
// 채팅방 메시지 저장
chatService.saveMessage(chatDto);
// 메시지가 ~로 시작한다면 해당 채팅방의 모든 구독자에게 전송
if (chatDto.getMessage().startsWith("님이 입장하셨습니다.")) {
simpMessagingTemplate.convertAndSend("/sub/chat/room/" + roomNo, chatDto);
}
}
}
React
연결
const socket = new SockJS("http://localhost:8080/ws");
const stompClient = Stomp.over(socket);
stompClient.connect({}, () => {
stompClient.subscribe("/sub/chat/room/" + room, (message) => {});
입장 메세지 전송
stompClient.send(
"/pub/chat/join/" + room,
{},
JSON.stringify({
message: user + "입장",
user: user,
roomNo: room,
timeStamp: currentTime,
})
);
메세지 전송
if (stompClientRef.current && currentMessage.trim() !== "") {
console.log("stompClient true!");
stompClientRef.current.send(
"/pub/chat/message/" + room,
{},
JSON.stringify({
message: currentMessage,
user: user,
roomNo: room,
timeStamp: currentTime,
})
);
레디스에서 데이터 가져오기
axios
.get("/api/messages/" + room).then((response) => {console.log(response.data);setMessageHistory(response.data);}).catch((error) => {console.error("Error fetching messages:", error);});
'코딩 > Spring' 카테고리의 다른 글
스프링부트 (JPA) 정리하기 좋은 강의 (0) | 2024.03.03 |
---|---|
자바 테스트코드 강의 추천! (0) | 2024.02.14 |
JWT(JSON Web Token), Filter, Config (0) | 2023.09.30 |
jwt를 이용한 회원가입 (리액트 + JPA) (0) | 2023.08.13 |
Spring Boot JPA CRUD 정리 (0) | 2023.08.08 |