Spring 프로젝트에서 Websocket 을 이용한 채팅 기능을 구현하였다.
주고 받는 채팅 메세지들은 DB 에 저장이 되고, 채팅방에 입장시 이전 메세지들이 stream 문으로 불러와진다.
stream 문 보단 for 문이 더 익숙한 나로선
실시간성이 중요한 채팅에서 stream 문과 for 문으로 각각 구현시 유의미한 성능 차이가 있을지 문득 궁금해졌다.
채팅 메세지들은 MySQL 과 Redis 에 저장되고 있고 Redis 에서만 불러오게끔 되어 있었는데
프로젝트를 진행하면서 이런 저런 테스트를 진행하다 보니
인메모리 데이터베이스인 Redis 에 있던 데이터들이 자꾸 날아가는 상황이 발생하였다.
그래서 채팅 메세지 불러오는 로직을
Redis 에 데이터가 있을 때)
1. Redis 데이터랑 MySQL 데이터가 같을 때 or Redis 데이터만 100개 이상일 때 - Redis 에서만 데이터 100개를 불러온다.
if(redisChatMessageCount == mysqlChatMessageCount || redisChatMessageCount >= 100){
List<ChatMessage> chatMessageList = redisMessageTemplate.opsForList().range("chatMessages::" + roomId, 0, 99);
List<ChatMessageResponseDto> chatMessageResponseDtoList = new ArrayList<>();
for (ChatMessage chatMessage : chatMessageList) {
chatMessageResponseDtoList.add(new ChatMessageResponseDto(chatMessage));
}
2. Redis 데이터랑 MySQL 데이터랑 다를 때 - Redis 데이터 개수 확인 후, 100 - Redis 의 값만큼 MySQL 에서 불러온다.
else{
List<ChatMessageResponseDto> chatMessageResponseDtoList = chatMessageRepository.findAllByRoomId(roomId)
.stream()
.limit(100 - redisChatMessageCount)
.map(ChatMessageResponseDto::new)
.collect(Collectors.toList());
List<ChatMessage> chatMessageList = redisMessageTemplate.opsForList().range("chatMessages::" + roomId, 0, redisChatMessageCount);
for (ChatMessage chatMessage : chatMessageList) {
chatMessageResponseDtoList.add(new ChatMessageResponseDto(chatMessage));
}
return chatMessageResponseDtoList;
}
Redis 에 데이터가 없을 때)
3. Redis 에 데이터가 아예 없을 때 - MySQL 에서만 데이터 100개를 불러온다.
else{
List<ChatMessageResponseDto> chatMessageResponseDtoList = chatMessageRepository.findAllByRoomId(roomId)
.stream()
.map(ChatMessageResponseDto::new)
.toList();
return chatMessageResponseDtoList;
}
이런 식으로 로직을 짜놓았는데
프로젝트 특성상 100개 이상의 메세지를 확인할 일도 없고
사용을 하다보니 이런 저런 문제들이 아직 많기도 해서
좀 더 효율적으로, 오류가 없게끔 구현을 해봐야겠다. (지금 드는 생각은 MySQL 데이터와 Redis 데이터를 실시간으로 동기화를 시킨다던가...)
그래도 일단 테스트는 해볼 수 있으니 진행해보았다.
테스트는 stream 문과 for 문 각각 진행을 할 것인데
메세지 개수에 따라 (1개, 10개, 100개) 총 6번의 테스트를 진행할 것이다.
추가로
데이터를 불러오는 로직 (위 3가지 case) 에 대한 테스트도 진행함으로써
MySQL 에서 불러오는 속도와 Redis 에서 불러오는 속도를 추가로 비교해볼 수 있을 것 같다.
테스트 방법은
채팅 메세지를 불러오는 로직 위에 StopWatch 클래스를 선언 및 시작해주고
dto 를 반환하기 전까지의 걸린 시간을 측정하는 식으로 진행하였다.
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// ~ 이전 메세지 불러오는 로직 ~
stopWatch.stop();
System.out.println("Case 1 (stream) : "+ stopWatch.getTotalTimeMillis() + "ms");
(Redis 에 저장된 데이터로만 불러오는 환경)
1. 저장된 메세지가 1개일 때
stream문)
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
200 | 201 | 200 | 198 | 200 | 202 | 201 | 215 | 216 | 199 |
평균 | 203.2 |
(단위 : ms)
for문)
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
202 | 200 | 201 | 202 | 198 | 200 | 200 | 201 | 199 | 202 |
평균 | 200.5 |
(단위 : ms)
2. 저장된 메세지가 10개일 때
stream문)
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
209 | 210 | 256 | 219 | 201 | 203 | 210 | 218 | 204 | 212 |
평균 | 214.2 |
(단위 : ms)
for문)
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
200 | 203 | 200 | 204 | 202 | 200 | 200 | 200 | 200 | 201 |
평균 | 201 |
(단위 : ms)
3. 저장된 메세지가 100개일 때
stream문)
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
412 | 411 | 411 | 413 | 405 | 407 | 438 | 482 | 446 | 436 |
평균 | 426.1 |
(단위 : ms)
for문)
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
414 | 401 | 397 | 390 | 397 | 390 | 397 | 407 | 391 | 393 |
평균 | 397.7 |
(단위 : ms)
메세지가 1개일 때는 차이가 거의 없다시피 했지만
10개, 100개 불러오는 메세지 갯수가 늘어갈수록 for 문이 불러오는 속도가 대체로 빨랐고
stream 문에선 가끔 측정 값이 튀는 것을 확인할 수 있는데 for 문은 비교적 안정적으로 값을 뽑아내는 듯 하다.
2번째 테스트인 데이터를 불러오는 원천 DB 에 따른 속도를 확인하려 한다.
(불러오는 메세지 100개 기준)
1. Redis
stream 문)
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
412 | 411 | 411 | 413 | 405 | 407 | 438 | 482 | 446 | 436 |
평균 | 426.1 |
(단위 : ms)
for 문)
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
414 | 401 | 397 | 390 | 397 | 390 | 397 | 407 | 391 | 393 |
평균 | 397.7 |
(단위 : ms)
(불러오는 메세지 100개 기준)
2. MySQL
stream 문)
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
24 | 14 | 10 | 12 | 25 | 11 | 18 | 29 | 15 | 11 |
평균 | 16.9 |
(단위 : ms)
for 문)
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
18 | 10 | 11 | 10 | 11 | 25 | 16 | 11 | 11 | 13 |
평균 | 13.6 |
(단위 : ms)
(불러오는 메세지 100개 기준 / Redis 50개, MySQL 50개)
3. Redis + MySQL
stream 문)
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
222 | 212 | 224 | 221 | 217 | 211 | 219 | 218 | 214 | 211 |
평균 | 216.9 |
for 문)
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
236 | 221 | 217 | 217 | 214 | 217 | 222 | 223 | 215 | 217 |
평균 | 219.9 |
예상과는 빗나간 결과가 나왔다.
빠른 속도가 강점인 Redis 보다 MySQL 에서 불러오는 속도가 더 빠르게 측정되어 뭔가 이상해 서버를 좀 뒤적거렸는데...
MySQL 은 RDS 를 이용하는 중이고
Redis 는 ec2 안에 docker 에서 띄우고 있는 Redis 에서 돌아가는 중인줄 알았는데 알고보니 프로젝트 초반, 한 팀원이 설정해두었던 클라우드 기반 redis labs 에서 사용하던 Redis 를 그대로 사용 중이었다.
MySQL 과 Redis 의 차이를 확실하게 알려면
EC2 안에서 둘다 Docker 로 띄워진 상태여야 하는데 추후에 개인 서버에서 진행해봐야겠다...
그래서 어떤 이유로 이렇게 성능 차이가 나나 검색을 해보았는데
1. for 문은 단순 인덱스 기반으로 실행되는 반복문으로 메모리적으로 접근을 하여 stream 문 보다 빠르며
2. stream 문은 함수 호출과 람다 표현식의 오버헤드가 있지만 for 문은 없다.
정도 인 것 같고
stream 에서 parallelstream 이란 것을 이용하면 멀티 코어 프로세서의 이점을 활용하여 병렬로 처리할 수 있어 속도가 더 빠를 수도 있다고 한다.
아무튼 결론은
for 문의 속도가 확실히 빠른 것 같고
실시간성이 제일 중요한 채팅 기능에선 가독성이 좋은 Stream 문 보단 For 문을 쓰는 것이 더 적합해보인다.
DB 에 따른 차이는 눈으로 확인하진 못했지만
비싼 돈 들여서 RDS 를 쓰는 이유를 알 것 같고 stream 문과 for 문의 차이는 확인했으니 만족!
참고
https://jeong-pro.tistory.com/185
https://pamyferret.tistory.com/49
'개발 ━━━━━ > Spring(boot)' 카테고리의 다른 글
[Spring] PDFBox 로 텍스트 추출시 null 값이 추출될 때 (0) | 2024.10.10 |
---|---|
[Spring] Singleton, Prototype Scope Bean (0) | 2024.09.06 |
[Spring] WebSocket 을 이용한 채팅 구현 (0) | 2023.10.09 |
[Spring] Entity 관계 (0) | 2023.09.02 |
[Spring] Filter (0) | 2023.09.01 |