나중에 요청할 결과를 미리 저장해둔 후 빠르게 서비스해 주는 것
이는 '파레토 법칙(80%의 결과는 20%의 원인으로 인해 발생한다)'과 관련된다.
사용자가 늘어나면 DB에 무리가 가기 시작한다.
매 트랜잭션마다 디스크에 접근해야하므로, 부하가 많아지면 성능이 떨어질 수 밖에 없다.
이 경우, 다음 방법들을 고려해볼 수 있다.
내가 참여한 프로젝트에서 채팅 내역 조회 시 사용한 전략이다.
Cache 에 데이터 존재 유무 확인
Cache 에 데이터가 있다면, Cache 의 데이터 사용
Cache 에 데이터가 없다면, Cache 의 실제 DB 데이터 사용
DB 에서 가져 온 데이터를 Cache 에 저장
주로 쓰기 작업이 많아서, INSERT 쿼리를 일일이 날리지 않고 한꺼번에 여러 개의 INSERT 쿼리를 날려서 배치 처리를 하기 위해 사용한다.
컴퓨터로 시험을 진행할 경우, 많은 수험생들이 한꺼번에 시험지 제출 버튼을 누르면 DB 에 쓰기 요청이 몰리게되어 DB가 죽을 수도 있다.
이때, write back 은 캐시 메모리에 데이터를 저장해 두고, 이후에 DB 디스크에 업데이트를 하므로 좀 더 안전하게 쓰기 작업을 할 수 있다.
단, 메모리 공간에 머무르는 동안 서버에 장애가 발생하여 다운된다면 데이터가 손실될 수 있다는 단점이 있다.
모든 데이터를 Cache 에 저장
Cache 의 데이터를 일정 주기마다 DB 에 한꺼번에 저장 (배치)
DB 에 저장한 데이터를 Cache 에서 제거
"Remote Dictionary Server"
외부에서 사용 가능한 Key-Value 쌍의 해시 맵 형태의 서버
NoSQL 의 일종이다
정의
단점
Redis 는 인메모리를 사용하나, 단점인 휘발성을 보완하는 영속성을 가질 수 있다.
Redis 는 영속성을 지원하는 인 메모리 데이터 저장소
데이터를 디스크에 저장하는 2 가지 방식
RDB (Snapshotting) 방식
AOF (Append On File) 방식
한 번에 하나의 명령만 처리할 수 있습니다.
따라서, 중간에 처리 시간이 긴 명령어가 들어오면 그 뒤에 명령어들은 모두 앞에 있는 명령어가 처리될 때까지 대기하게 된다.
(단, get, set 명령어의 경우 초당 10만 개 이상 처리할 수 있을 만큼 빠르다)
이 때문에, Race Condition 를 방지할 수 있다.
dependencies {
//embedded-redis
implementation group: 'it.ozimov', name: 'embedded-redis', version: '0.7.2'
// redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}
spring.profiles.include=key
spring.data.redis.host=localhost
spring.data.redis.port=6379
spring.data.redis.database=0
@Profile("local") // 1.
@Configuration
public class EmbeddedRedisConfig {
@Value("${spring.data.redis.port}")
private int redisPort;
private RedisServer redisServer; // redis 데이터베이스 서버
// redis 서버 초기화 및 시작
@PostConstruct
public void redisServer() {
redisServer = new RedisServer(redisPort);
redisServer.start();
}
// redis 서버 중지. container 에서 빈 제거 전 실행
@PreDestroy
public void stopRedis() {
if (redisServer != null) {
redisServer.stop();
}
}
}
1.
@Configuration
public class RedisConfig {
// redis 연결, redis 의 pub/sub 기능을 이용하기 위해 pub/sub 메시지를 처리하는 MessageListener 설정(등록)
@Bean
public RedisMessageListenerContainer redisMessageListener(RedisConnectionFactory connectionFactory) { // 1.
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory); // 2.
return container;
}
// Redis 데이터베이스와의 상호작용을 위한 RedisTemplate 을 설정. JSON 형식으로 담기 위해 직렬화
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer()); // Key Serializer
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(String.class)); // Value Serializer
return redisTemplate;
}
// Redis 에 메시지 내역을 저장하기 위한 RedisTemplate 을 설정
@Bean
public RedisTemplate<String, MessageDto> redisTemplateMessage(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, MessageDto> redisTemplateMessage = new RedisTemplate<>();
redisTemplateMessage.setConnectionFactory(connectionFactory);
redisTemplateMessage.setKeySerializer(new StringRedisSerializer()); // Key Serializer
redisTemplateMessage.setValueSerializer(new Jackson2JsonRedisSerializer<>(String.class)); // Value Serializer
return redisTemplateMessage;
}
}
1.
2.
@Service
@RequiredArgsConstructor
public class RedisPublisher {
private final RedisTemplate<String, Object> redisTemplate;
// Redis Topic 에 메시지 발행. 메시지를 발행 후, 대기 중이던 redis 구독 서비스(RedisSubscriber)가 메시지를 처리
public void publish(ChannelTopic topic, MessageDto message) {
redisTemplate.convertAndSend(topic.getTopic(), message);
}
}
@Service
@RequiredArgsConstructor
public class RedisSubscriber implements MessageListener { // 1.
private final ObjectMapper objectMapper;
private final RedisTemplate redisTemplate;
private final SimpMessageSendingOperations messagingTemplate;
// 2. Redis 에서 메시지가 발행(publish)되면, listener 가 해당 메시지를 읽어서 처리
@Override
public void onMessage(Message message, byte[] pattern) { // 3.
try {
// redis 에서 발행된 데이터를 받아 역직렬화
String publishMessage = (String) redisTemplate.getStringSerializer().deserialize(message.getBody());
// 4. 해당 객체를 MessageDto 객체로 맵핑
MessageDto roomMessage = objectMapper.readValue(publishMessage, MessageDto.class);
// 5. Websocket 구독자에게 채팅 메시지 전송
messagingTemplate.convertAndSend("/sub/chat/room/" + roomMessage.getRoomId(), roomMessage);
} catch (Exception e) {
log.error(e.getMessage());
}
}
}
1.
2.
3.
4.
5.
@Controller
@RequiredArgsConstructor
public class MessageController {
private final RedisPublisher redisPublisher;
private final MessageRoomService messageRoomService;
private final MessageService messageService;
// 대화 & 대화 저장
@MessageMapping("/message") // 1.
public void message(MessageDto messageDto) {
// 클라이언트의 쪽지방(topic) 입장, 대화를 위해 리스너와 연동
messageRoomService.enterMessageRoom(messageDto.getRoomId());
// Websocket 에 발행된 메시지를 redis 로 발행. 해당 쪽지방을 구독한 클라이언트에게 메시지가 실시간 전송됨 (1:N, 1:1 에서 사용 가능)
redisPublisher.publish(messageRoomService.getTopic(messageDto.getRoomId()), messageDto);
// DB & Redis 에 대화 저장
messageService.saveMessage(messageDto);
}
// 대화 내역 조회
@GetMapping("/api/room/{roomId}/message")
public ResponseEntity<List<MessageDto>> loadMessage(@PathVariable String roomId) {
return ResponseEntity.ok(messageService.loadMessage(roomId));
}
}
1.
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class MessageRoomController {
private final MessageRoomService messageRoomService;
// 쪽지방 생성
@PostMapping("/room")
public MessageResponseDto createRoom(@RequestBody MessageRequestDto messageRequestDto, @AuthenticationPrincipal UserDetailsImpl userDetails) {
return messageRoomService.createRoom(messageRequestDto, userDetails.getUser());
}
// 사용자 관련 쪽지방 전체 조회
@GetMapping("/rooms")
public List<MessageResponseDto> findAllRoomByUser(@AuthenticationPrincipal UserDetailsImpl userDetails) {
return messageRoomService.findAllRoomByUser(userDetails.getUser());
}
// 사용자 관련 쪽지방 선택 조회
@GetMapping("room/{roomId}")
public MessageRoomDto findRoom(@PathVariable String roomId, @AuthenticationPrincipal UserDetailsImpl userDetails) {
return messageRoomService.findRoom(roomId, userDetails.getUser());
}
// 쪽지방 삭제
@DeleteMapping("room/{id}")
public MsgResponseDto deleteRoom(@PathVariable Long id, @AuthenticationPrincipal UserDetailsImpl userDetails) {
return messageRoomService.deleteRoom(id, userDetails.getUser());
}
}
@Slf4j
@Service
@RequiredArgsConstructor
public class MessageService {
private final RedisTemplate<String, MessageDto> redisTemplateMessage;
private final MessageRepository messageRepository;
private final MessageRoomRepository messageRoomRepository;
// 대화 저장
public void saveMessage(MessageDto messageDto) {
// DB 저장
Message message = new Message(messageDto.getSender(), messageDto.getRoomId(), messageDto.getMessage());
messageRepository.save(message);
// 1. 직렬화
redisTemplateMessage.setValueSerializer(new Jackson2JsonRedisSerializer<>(Message.class));
// 2. redis 저장
redisTemplateMessage.opsForList().rightPush(messageDto.getRoomId(), messageDto);
// 3. expire 을 이용해서, Key 를 만료시킬 수 있음
redisTemplateMessage.expire(messageDto.getRoomId(), 1, TimeUnit.MINUTES);
}
// 6. 대화 조회 - Redis & DB
public List<MessageDto> loadMessage(String roomId) {
List<MessageDto> messageList = new ArrayList<>();
// Redis 에서 해당 채팅방의 메시지 100개 가져오기
List<MessageDto> redisMessageList = redisTemplateMessage.opsForList().range(roomId, 0, 99);
// 4. Redis 에서 가져온 메시지가 없다면, DB 에서 메시지 100개 가져오기
if (redisMessageList == null || redisMessageList.isEmpty()) {
// 5.
List<Message> dbMessageList = messageRepository.findTop100ByRoomIdOrderByCreatedAtAsc(roomId);
for (Message message : dbMessageList) {
MessageDto messageDto = new MessageDto(message);
messageList.add(messageDto);
redisTemplateMessage.setValueSerializer(new Jackson2JsonRedisSerializer<>(Message.class)); // 직렬화
redisTemplateMessage.opsForList().rightPush(roomId, messageDto); // redis 저장
}
} else {
// 7.
messageList.addAll(redisMessageList);
}
return messageList;
}
}
1.
Message 클래스(테이블)의 형식에 맞춰야 저장되므로, Message 형태로 분해(직렬화) 재조립(역직렬화)한 것!
2.
3.
4.
5.
findTop100ByRoomIdOrderByCreatedAtAsc
6.
캐싱 전략 中 look aside cache 를 사용했다.
1. Redis 에서 해당 채팅방의 메시지 100개 가져오기
2. Cache 에 데이터가 있다면(즉, Redis 에 데이터가 있다면), Cache 의 데이터 사용
Redis 에서 가져온 메시지가 없다면, DB 에서 메시지 100개 가져오기
3. for 문 내부에서는 DB 에서 조회해 온 메시지들을 다시 Redis 에 저장한다.
(단, 이때 직렬화를 통해 데이터가 저장될 수 있는 형태로 만들어줘야 한다)
7.
@Slf4j
@Service
@RequiredArgsConstructor
public class MessageRoomService {
private final MessageRoomRepository messageRoomRepository;
private final MessageRepository messageRepository;
// 쪽지방(topic)에 발행되는 메시지 처리하는 리스너
private final RedisMessageListenerContainer redisMessageListener;
// 구독 처리 서비스
private final RedisSubscriber redisSubscriber;
// 1. redis
private static final String Message_Rooms = "MESSAGE_ROOM";
private final RedisTemplate<String, Object> redisTemplate;
private HashOperations<String, String, MessageRoomDto> opsHashMessageRoom;
// 2. 쪽지방의 대화 메시지 발행을 위한 redis topic(쪽지방) 정보
private Map<String, ChannelTopic> topics;
// 3. redis 의 Hash 데이터 다루기 위함
@PostConstruct
private void init() {
opsHashMessageRoom = redisTemplate.opsForHash();
topics = new HashMap<>();
}
// 쪽지방 생성
public MessageResponseDto createRoom(MessageRequestDto messageRequestDto, User user) {
Post post = postRepository.findById(messageRequestDto.getPostId()).orElseThrow(
() -> new IllegalArgumentException("게시글을 찾을 수 없습니다.")
);
// 4.
MessageRoom messageRoom = messageRoomRepository.findBySenderAndReceiver(user.getNickname(), messageRequestDto.getReceiver());
// 5. 처음 쪽지방 생성 또는 이미 생성된 쪽지방이 아닌 경우
if ((messageRoom == null) || (messageRoom != null && (!user.getNickname().equals(messageRoom.getSender()) && !messageRequestDto.getReceiver().equals(messageRoom.getReceiver()) && !messageRequestDto.getPostId().equals(post.getId())))) {
MessageRoomDto messageRoomDto = MessageRoomDto.create(messageRequestDto, user);
opsHashMessageRoom.put(Message_Rooms, messageRoomDto.getRoomId(), messageRoomDto); // redis hash 에 쪽지방 저장해서, 서버간 채팅방 공유
messageRoom = messageRoomRepository.save(new MessageRoom(messageRoomDto.getId(), messageRoomDto.getRoomName(), messageRoomDto.getSender(), messageRoomDto.getRoomId(), messageRoomDto.getReceiver(), user, post));
return new MessageResponseDto(messageRoom);
// 6. 이미 생성된 쪽지방인 경우
} else {
return new MessageResponseDto(messageRoom.getRoomId());
}
}
// 7. 사용자 관련 쪽지방 전체 조회
public List<MessageResponseDto> findAllRoomByUser(User user) {
List<MessageRoom> messageRooms = messageRoomRepository.findByUserOrReceiver(user, user.getNickname()); // sender & receiver 모두 해당 쪽지방 조회 가능 (1:1 대화)
List<MessageResponseDto> messageRoomDtos = new ArrayList<>();
for (MessageRoom messageRoom : messageRooms) {
// user 가 sender 인 경우
if (user.getNickname().equals(messageRoom.getSender())) {
MessageResponseDto messageRoomDto = new MessageResponseDto(
messageRoom.getId(),
messageRoom.getReceiver(), // roomName
messageRoom.getRoomId(),
messageRoom.getSender(),
messageRoom.getReceiver());
// 8. 가장 최신 메시지 & 생성 시간 조회
Message latestMessage = messageRepository.findTopByRoomIdOrderByCreatedAtDesc(messageRoom.getRoomId());
if (latestMessage != null) {
messageRoomDto.setLatestMessageCreatedAt(latestMessage.getCreatedAt());
messageRoomDto.setLatestMessageContent(latestMessage.getMessage());
}
messageRoomDtos.add(messageRoomDto);
// user 가 receiver 인 경우
} else {
MessageResponseDto messageRoomDto = new MessageResponseDto(
messageRoom.getId(),
messageRoom.getSender(), // roomName
messageRoom.getRoomId(),
messageRoom.getSender(),
messageRoom.getReceiver());
// 가장 최신 메시지 & 생성 시간 조회
Message latestMessage = messageRepository.findTopByRoomIdOrderByCreatedAtDesc(messageRoom.getRoomId());
if (latestMessage != null) {
messageRoomDto.setLatestMessageCreatedAt(latestMessage.getCreatedAt());
messageRoomDto.setLatestMessageContent(latestMessage.getMessage());
}
messageRoomDtos.add(messageRoomDto);
}
}
return messageRoomDtos;
}
// 사용자 관련 쪽지방 선택 조회
public MessageRoomDto findRoom(String roomId, User user) {
MessageRoom messageRoom = messageRoomRepository.findByRoomId(roomId);
// 게시글 조회
Post post = postRepository.findById(messageRoom.getPost().getId()).orElseThrow(
() -> new IllegalArgumentException("게시글이 존재하지 않습니다.")
);
// 사용자 조회
User receiver = userRepository.findById(post.getUser().getId()).orElseThrow(
() -> new IllegalArgumentException("사용자가 존재하지 않습니다.")
);
// 9. sender & receiver 모두 messageRoom 조회 가능
messageRoom = messageRoomRepository.findByRoomIdAndUserOrRoomIdAndReceiver(roomId, user, roomId, receiver.getNickname());
if (messageRoom == null) {
throw new IllegalArgumentException("쪽지방이 존재하지 않습니다.");
}
MessageRoomDto messageRoomDto = new MessageRoomDto(
messageRoom.getId(),
messageRoom.getRoomName(),
messageRoom.getRoomId(),
messageRoom.getSender(),
messageRoom.getReceiver());
messageRoomDto.setMessageRoomPostId(post.getId());
messageRoomDto.setMessageRoomCategory(post.getCategory().getValue()); // getValue() : category 는 int 타입이었기 때문. 없다면 String 타입으로 반환됨
messageRoomDto.setMessageRoomCountry(post.getCountry());
messageRoomDto.setMessageRoomTitle(post.getTitle());
return messageRoomDto;
}
// 10. 쪽지방 삭제
public MsgResponseDto deleteRoom(Long id, User user) {
MessageRoom messageRoom = messageRoomRepository.findByIdAndUserOrIdAndReceiver(id, user, id, user.getNickname());
// sender 가 삭제할 경우
if (user.getNickname().equals(messageRoom.getSender())) {
messageRoomRepository.delete(messageRoom);
opsHashMessageRoom.delete(Message_Rooms, messageRoom.getRoomId());
// receiver 가 삭제할 경우
} else if (user.getNickname().equals(messageRoom.getReceiver())) {
messageRoom.setReceiver("Not_Exist_Receiver");
messageRoomRepository.save(messageRoom);
}
return new MsgResponseDto("쪽지방을 삭제했습니다.", HttpStatus.OK.value());
}
// 쪽지방 입장
public void enterMessageRoom(String roomId) {
ChannelTopic topic = topics.get(roomId);
if (topic == null) {
topic = new ChannelTopic(roomId);
redisMessageListener.addMessageListener(redisSubscriber, topic); // pub/sub 통신을 위해 리스너를 설정. 대화가 가능해진다
topics.put(roomId, topic);
}
}
// redis 채널에서 쪽지방 조회
public ChannelTopic getTopic(String roomId) {
return topics.get(roomId);
}
}
1.
private static final String Message_Rooms = "MESSAGE_ROOM";
private final RedisTemplate<String, Object> redisTemplate;
private HashOperations<String, String, MessageRoomDto> opsHashMessageRoom;
2.
3.
이후는 원래 현재 프로젝트에서 내가 한거로
4.
5.
6.
7.
8.
9.
10.
@Getter
@Setter
@NoArgsConstructor
public class MessageDto {
private String sender;
private String roomId;
private String message;
private String sentTime;
// 대화 조회
public MessageDto(Message message) {
this.sender = message.getSender();
this.roomId = message.getRoomId();
this.message = message.getMessage();
}
}
@Getter
@NoArgsConstructor
public class MessageRequestDto {
private String receiver; // 메세지 수신자
private Long postId; // 게시글 id
}
@Getter
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class MessageResponseDto {
private Long id;
private String roomName;
private String sender;
private String roomId;
private String receiver;
private Long postId;
private String message;
private LocalDateTime createdAt;
// 쪽지방 생성
public MessageResponseDto(MessageRoom messageRoom) {
this.id = messageRoom.getId();
this.roomName = messageRoom.getRoomName();
this.sender = messageRoom.getSender();
this.roomId = messageRoom.getRoomId();
this.receiver = messageRoom.getReceiver();
this.postId = messageRoom.getPost().getId();
}
// 사용자 관련 쪽지방 전체 조회
public MessageResponseDto(Long id, String roomName, String roomId, String sender, String receiver) {
this.id = id;
this.roomName = roomName;
this.roomId = roomId;
this.sender = sender;
this.receiver = receiver;
}
public MessageResponseDto(String roomId) {
this.roomId = roomId;
}
public void setLatestMessageContent(String message) {
this.message = message;
}
public void setLatestMessageCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
}
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class MessageRoomDto implements Serializable { // Redis 에 저장되는 객체들이 직렬화가 가능하도록
private static final long serialVersionUID = 6494678977089006639L; // 역직렬화 위한 serialVersionUID 세팅
private Long id;
private String roomName;
private String roomId;
private String sender; // 메시지 송신자
private String receiver; // 메시지 수신자
private Long postId;
private int category; // 게시글 카테고리
private String title; // 게시글 제목
private String country; // 게시글 나라
// 쪽지방 생성
public static MessageRoomDto create(MessageRequestDto messageRequestDto, User user) {
MessageRoomDto messageRoomDto = new MessageRoomDto();
messageRoomDto.roomName = messageRequestDto.getReceiver();
messageRoomDto.roomId = UUID.randomUUID().toString();
messageRoomDto.sender = user.getNickname();
messageRoomDto.receiver = messageRequestDto.getReceiver();
return messageRoomDto;
}
// 사용자 관련 쪽지방 선택 조회
public MessageRoomDto(Long id, String roomName, String roomId, String sender, String receiver) {
this.id = id;
this.roomName = roomName;
this.roomId = roomId;
this.sender = sender;
this.receiver = receiver;
}
public void setMessageRoomPostId(Long postId) {
this.postId = postId;
}
public void setMessageRoomCategory(int category) {
this.category = category;
}
public void setMessageRoomTitle(String title) {
this.title = title;
}
public void setMessageRoomCountry(String country) {
this.country = country;
}
}
@Entity
@Getter
@Table(name = "message")
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Message extends Timestamped {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "sender")
private String sender;
@Column(name = "roomId")
private String roomId;
@Column(name = "receiver")
private String receiver;
@Column(name = "message")
private String message;
@Column(name = "sentTime")
private String sentTime;
// 1.
@ManyToOne
@JoinColumn(name = "roomId", referencedColumnName = "roomId", insertable = false, updatable = false)
private MessageRoom messageRoom;
// 대화 저장
public Message(String sender, String roomId, String message) {
super();
this.sender = sender;
this.roomId = roomId;
this.message = message;
}
}
1.
MessageRoom 의 컬럼명을 Message 엔티티에서 roomId 로 하고, MessageRoom 엔티티에서도 roomId 로 설정하기 위해
@Entity
@Setter
@Getter
@Table(name = "messageRoom")
@NoArgsConstructor
public class MessageRoom {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String roomName;
private String sender; // 채팅방 생성자
@Column(unique = true)
private String roomId;
private String receiver; // 채팅방 수신자
@OneToMany(mappedBy = "messageRoom", cascade = CascadeType.REMOVE)
private List<Message> messageList = new ArrayList<>();
@ManyToOne
@JoinColumn(name = "userId", nullable = false)
private User user;
@OneToOne // 채팅방과 Post 는 1:1 연관관계
@JoinColumn(name = "postId")
private Post post;
// 쪽지방 생성
public MessageRoom(Long id, String roomName, String sender, String roomId, String receiver, User user, Post post) {
this.id = id;
this.roomName = roomName;
this.sender = sender;
this.roomId = roomId;
this.receiver = receiver;
this.user = user;
this.post = post;
}
}
public interface MessageRepository extends JpaRepository<Message, Long> {
List<Message> findTop100ByRoomIdOrderByCreatedAtAsc(String roomId);
Message findTopByRoomIdOrderByCreatedAtDesc(String roomId);
}
public interface MessageRoomRepository extends JpaRepository<MessageRoom, Long> {
List<MessageRoom> findByUserOrReceiver(User user, String receiver);
MessageRoom findByIdAndUserOrIdAndReceiver(Long id, User user, Long id1, String nickname);
MessageRoom findBySenderAndReceiver(String nickname, String receiver);
MessageRoom findByRoomIdAndUserOrRoomIdAndReceiver(String roomId, User user, String roomId1, String nickname);
MessageRoom findByRoomId(String roomId);
}
stomp 를 테스트 할 환경이 마땅치 않았기 때문에,
MessageController 에 메시지 작성 post 요청을 만들고, MessageService 에 해당 요청을 받아서 redis 에 저장 기능 등을 테스트 했다.
실제 stomp 테스트는 프론트 쪽과 연결 이후에 진행되었다.
참고: Spring websocket chatting server(3) - 여러대의 채팅서버간에 메시지 공유하기 by Redis pub/sub
참고: [데이터베이스] Redis란?
참고: Redis란? 레디스의 기본적인 개념 (인메모리 데이터 구조 저장소)
참고: Redis란 무엇일까..?
참고: [운영체제] Race Condition과 예방할 방법(세마포어, 뮤텍스)
좋은 글 감사합니다 👍