본문 바로가기

Spring Boot

[Spring Boot] 스프링부트에서 WebSocket, STOMP를 이용한 채팅기능 구현하기 (2) - 도메인 모델과 Redis 연동

반응형

사용된 플러그인 : 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 클래스의 역할

  1. MessageType: 메시지의 용도를 구분합니다.
    • ENTER: 사용자가 채팅방에 입장했을 때
    • TALK: 일반적인 채팅 메시지
  2. @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 클래스의 역할

  1. Serializable 구현
    • Redis에 객체를 저장하기 위해 필요
    • 직렬화를 통해 객체를 문자열로 변환하여 저장
  2. 팩토리 메서드 패턴 사용
    • 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 설정의 역할

  1. RedisConnectionFactory
    • Redis 서버와의 연결을 관리
    • Lettuce 라이브러리를 사용하여 Redis 연결 구현
  2. RedisTemplate
    • Redis 데이터 접근을 위한 높은 수준의 추상화 제공
    • 직렬화/역직렬화 설정으로 객체 저장 가능
    • 다양한 Redis 자료구조(String, Hash, List 등) 조작 가능
  3. 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의 역할

  1. Redis Hash 자료구조 사용
    • CHAT_ROOMS: 채팅방 정보 저장
    • CHAT_ROOM_USERS: 채팅방 참여자 정보 저장
  2. 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의 역할

  1. Redis 채널 메시지 수신
  2. 수신된 메시지를 ChatMessage 객체로 변환
  3. WebSocket을 통해 연결된 클라이언트들에게 메시지 전달

 

 

반응형