반응형
사용된 플러그인 : Lombok, Slf4j
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.webjars:webjars-locator-core'
implementation 'org.webjars:sockjs-client:1.5.1'
implementation 'org.webjars:stomp-websocket:2.3.4'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'it.ozimov:embedded-redis:0.7.2'
1. 채팅 도메인 모델 구현
이번 포스팅에서 구현할 단계
- 도메인 모델 (ChatMessage, ChatRoom)
- 채팅 시스템의 핵심 데이터 구조 정의
- 메시지와 채팅방의 상태 관리
- Redis 설정
- Redis 연결 및 데이터 접근 설정
- 메시지 직렬화/역직렬화 설정
- Repository
- Redis를 사용한 채팅방 데이터 관리
- 채팅방 생성 및 사용자 관리
- Publisher/Subscriber
- 실시간 메시지 전달 처리
- WebSocket과 Redis를 연동한 메시지 브로드캐스팅
1.1 ChatMessage 클래스
먼저 채팅 메시지를 표현할 모델 구현
@Getter
@Setter
public class ChatMessage {
// 메시지 타입: 입장, 채팅
public enum MessageType {
ENTER, TALK
}
private MessageType type; // 메시지 타입
private String roomId; // 방번호
private String sender; // 메시지 보낸사람
private String message; // 메시지
@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
private LocalDateTime timestamp; // 메시지 발송 시간
}
ChatMessage 클래스의 역할
- MessageType: 메시지의 용도를 구분합니다.
- ENTER: 사용자가 채팅방에 입장했을 때
- TALK: 일반적인 채팅 메시지
- @JsonSerialize/@JsonDeserialize
- Redis에 메시지를 저장하고 불러올 때 LocalDateTime 타입을 직렬화/역직렬화하기 위한 설정
- Redis는 데이터를 문자열로 저장하기 때문에 필요합니다.
두번째로는 채팅방을 표현할 모델 구현하기입니다.
1.2 ChatRoom 클래스 구현
@Getter
@Setter
public class ChatRoom implements Serializable {
private static final long serialVersionUID = 6494678977089006639L;
private String roomId; // 채팅방 고유 ID
private String name; // 채팅방 이름
private boolean isOneToOne; // 1:1 채팅방 여부
private Set<String> participants; // 참가자 목록
// 일반 채팅방 생성 팩토리 메서드
public static ChatRoom create(String name) {
ChatRoom chatRoom = new ChatRoom();
chatRoom.roomId = UUID.randomUUID().toString();
chatRoom.name = name;
chatRoom.isOneToOne = false;
chatRoom.participants = new HashSet<>();
return chatRoom;
}
// 1:1 채팅방 생성 팩토리 메서드
public static ChatRoom createOneToOne(String name, String user1, String user2) {
ChatRoom chatRoom = new ChatRoom();
chatRoom.roomId = UUID.randomUUID().toString();
chatRoom.name = name;
chatRoom.isOneToOne = true;
chatRoom.participants = new HashSet<>(Set.of(user1, user2));
return chatRoom;
}
}
ChatRoom 클래스의 역할
- Serializable 구현
- Redis에 객체를 저장하기 위해 필요
- 직렬화를 통해 객체를 문자열로 변환하여 저장
- 팩토리 메서드 패턴 사용
- create(): 다수가 참여하는 일반 채팅방 생성
- createOneToOne(): 1:1 채팅방 생성
- 객체 생성의 캡슐화와 용도에 따른 명확한 생성 방법 제공
2. 채팅방과 메시지를 저장할 Redis 설정
NoSql인 Redis를 사용하기 위한 기본 설정을 구현합니다.
@Configuration
public class RedisConfig {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private int redisPort;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
config.setHostName(redisHost);
config.setPort(redisPort);
return new LettuceConnectionFactory(config);
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(String.class));
return redisTemplate;
}
@Bean
public RedisMessageListenerContainer redisMessageListener(
RedisConnectionFactory connectionFactory,
MessageListenerAdapter listenerAdapter,
ChannelTopic topic) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.addMessageListener(listenerAdapter, topic);
return container;
}
}
Redis 설정의 역할
- RedisConnectionFactory
- Redis 서버와의 연결을 관리
- Lettuce 라이브러리를 사용하여 Redis 연결 구현
- RedisTemplate
- Redis 데이터 접근을 위한 높은 수준의 추상화 제공
- 직렬화/역직렬화 설정으로 객체 저장 가능
- 다양한 Redis 자료구조(String, Hash, List 등) 조작 가능
- RedisMessageListenerContainer
- Redis의 pub/sub 기능을 위한 리스너 설정
- 메시지 수신 시 적절한 핸들러로 전달
3. Redis를 이용한 채팅방 Repository 구현
@Slf4j
@Repository
@RequiredArgsConstructor
public class RedisChatRoomRepository implements ChatRoomRepository {
private final RedisTemplate<String, Object> redisTemplate;
private static final String CHAT_ROOMS = "CHAT_ROOM";
private static final String CHAT_ROOM_USERS = "CHAT_ROOM_USERS";
private HashOperations<String, String, ChatRoom> opsHashChatRoom;
private HashOperations<String, String, Set<String>> opsHashChatRoomUsers;
@PostConstruct
private void init() {
opsHashChatRoom = redisTemplate.opsForHash();
opsHashChatRoomUsers = redisTemplate.opsForHash();
}
public ChatRoom createChatRoom(String name, String userId) {
ChatRoom chatRoom = ChatRoom.create(name);
opsHashChatRoom.put(CHAT_ROOMS, chatRoom.getRoomId(), chatRoom);
addUserToChatRoom(userId, chatRoom.getRoomId());
return chatRoom;
}
public void addUserToChatRoom(String userId, String roomId) {
Set<String> users = getUsersInRoom(roomId);
users.add(userId);
opsHashChatRoomUsers.put(CHAT_ROOM_USERS, roomId, users);
}
public Set<String> getUsersInRoom(String roomId) {
return Optional.ofNullable(opsHashChatRoomUsers.get(CHAT_ROOM_USERS, roomId))
.orElse(new HashSet<>());
}
}
Repository의 역할
- Redis Hash 자료구조 사용
- CHAT_ROOMS: 채팅방 정보 저장
- CHAT_ROOM_USERS: 채팅방 참여자 정보 저장
- HashOperations
- Redis Hash 자료구조에 대한 작업을 수행하는 인터페이스
- 키-값 쌍으로 데이터를 저장하고 조회
4. Redis Publisher/Subscriber 구현
4.1 RedisPublisher (메시지 발행자)
@Slf4j
@RequiredArgsConstructor
@Service
public class RedisPublisher {
private final RedisTemplate<String, Object> redisTemplate;
private final ChatRoomRepository chatRoomRepository;
public void publish(ChannelTopic topic, ChatMessage message) {
chatRoomRepository.saveChatMessage(message);
log.info("게시된 메시지: " + message);
redisTemplate.convertAndSend(topic.getTopic(), message);
}
}
Publisher의 역할
- 채팅 메시지를 Redis 채널에 발행
- 메시지 저장 및 브로드캐스팅 담당
4.2 RedisSubscriber (메시지 구독자)
@Slf4j
@RequiredArgsConstructor
@Service
public class RedisSubscriber implements MessageListener {
private final ObjectMapper objectMapper;
private final StringRedisTemplate redisTemplate;
private final SimpMessageSendingOperations messagingTemplate;
@Override
public void onMessage(Message message, byte[] pattern) {
try {
String publishMessage = (String) redisTemplate.getStringSerializer()
.deserialize(message.getBody());
ChatMessage roomMessage = objectMapper.readValue(publishMessage, ChatMessage.class);
messagingTemplate.convertAndSend(
"/sub/chat/room/" + roomMessage.getRoomId(),
roomMessage
);
} catch (Exception e) {
log.error(e.getMessage());
}
}
}
Subscriber의 역할
- Redis 채널 메시지 수신
- 수신된 메시지를 ChatMessage 객체로 변환
- WebSocket을 통해 연결된 클라이언트들에게 메시지 전달
반응형
'Spring Boot' 카테고리의 다른 글
| [Spring Boot] 스프링부트에서 WebSocket, STOMP를 이용한 채팅기능 구현하기 (1) (0) | 2024.11.25 |
|---|---|
| 단위 테스트에서 private 메소드 테스트가 필요한가? (4) | 2024.10.28 |
| @requiredargsconstructor @autowired 차이 (3) | 2024.10.06 |