본문 바로가기
Python/Django

[Django]데이터베이스 vs Redis: 실시간 접속자 관리 성능 비교와 최적화

by Byeong 2024. 11. 30.

문제 

채팅 프로젝트를 진행하며 문제가 발생했다. 

  1. 접속자 API 데이터를 불러오지 못함.
  2. 기존 접속자는 새로운 사용자가 들어와도 접속자 목록에 반영되지 않음.

 실시간 사용자가 많은 경우 채팅방 연결마다 DB 요청이 증가해 병목현상이난 쿼리 처리 속도가 느려질 가능성이 있어 개선할 필요성을 느꼈다.


원인

기존의 데이터 흐름

채팅방 입장 → 사용자 정보 저장  → 웹소켓 접속 → 접속자 리스트 전송 → 데이터 수신

 

 처음엔 데이터베이스에 즉각적으로 저장된 값을 가져오는 방식이 문제라고 생각했으나, 웹소켓 종료 시 사용자 데이터를 제거하는 과정에서 예상치 못한 문제가 있었다. 그리고 React의 안정성을 위한 중복 요청으로 인해 웹소켓이 두 번 연결되며 데이터가 누락되거나 저장되지 않는 경우가 있음을 확인했다.

또한, 기존 접속자는 새로운 사용자가 들어올 때 접속자 상태를 업데이트할 필요가 있었다.


해결 방법 1 : 접속 시 상태 업데이트

웹소켓 접속 시 group_send를 통해 접속자 상태를 업데이트하도록 구현

#consumers.py

class ChatConsumer(AsyncWebsocketConsumer):
    async def connect(self):
    	
        ...	
        await self.accept()
        
        # 사용자 목록 갱신을 위한 이벤트
        await self.channel_layer.group_send(
                    self.room_group_name,{
                        "type": "chat.update_users",  
                        "activate": "ture" 
                    }
                )

 

새로운 접속자가 생기면 모든 사용자에게 이벤트를 보내 접속자 목록을 갱신했다. 하지만 여전히 데이터 저장에 문제가 있었다.


해결방법 2 : redis를 활용한 접속자 관리 

Redis를 사용해 실시간 접속자를 관리하도록 구조를 변경했다.

 

1.  Redis 비동기 함수 구현 

#chat_redis.py 

import redis.asyncio as redis
from redis.asyncio.connection import ConnectionPool

redis_pool = ConnectionPool(host = ''),
                                port= ''),
                                password=''), 
                                db=0,
                                max_connections=10
                                )
redis_client = redis.Redis(connection_pool=redis_pool)

async def add_user_to_redis(key: str, value: str):
    await redis_client.sadd(key, value)

async def remove_user_to_redis(key: str, value: str):
    await redis_client.srem(key, value)

 

Redis set 형식으로 접속자를 저장하고 연결 종료 시 제거하도록 구현

 

 

 

 

 비동기 방식으로 구현했는데 이유는 채팅서버의 경우 동기식이 아닌 비동기식으로 진행하는데 원활한 실시간 소통을 위한 wedsocket 때문이다. 

동기식  방식: 하나의 작업이 완료될 때까지 다음 작업이 대기.

비동기식 방식: 작업이 완료될 때까지 대기하지 않고, 다른 작업을 동시에 실행.

 

 

2. Consumer에서 Redis 사용

class ChatConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        ...
        
        await add_user_to_redis(self.room_group_name, self.user.username)
        
        ...
        
        # 사용자 목록 갱신을 위한 이벤트
        users_redis = await get_users_from_redis(self.room_group_name)
        await self.channel_layer.group_send(
            self.room_group_name,{
                "type": "chat.update_users",  
                 "activate": "ture" 
            }
        )

 

Redis에서 접속자 목록을 가져와 모든 사용자에게 업데이트

 

3. 사용자 목록 API

#views.py

class ConnectedUsersAPIView(APIView):
    def get(self, request, room_id):
        users_redis = rd.smembers(f"chat_room_id_{room_id}")
        users = [user for user in users_redis]
        return Response({"users": users}, status=status.HTTP_200_OK)

Redis에 저장된 데이터를 클라이언트에 전달


한 걸음 더 : 웹소켓을 통한 접속자 목록 관리

현재까지의 접속자 확인 흐름

 

클라이언트 웹소켓 접속 요청 → 접속자를 Redis에 저장 → 웹소켓 연결 → 접속자 연결 이벤트 전송

 

위 방식은 동작은 하지만, 데이터베이스 접근이나 서버 API 요청이 발생한다는 점에서 개선의 필요성을 느꼈다.

 

API 요청
왼쪽 : 데이터베이스 이용 / 오른쪽 : redis 이용

 

 

왼쪽 

데이터베이스 방식: SQLite를 사용한 경우 37ms로 짧은 시간이 소요되지만, 3번의 쿼리가 발생함.

 

 

 

오른쪽

Redis 방식: 쿼리가 발생하지 않지만 다소 시간이(206ms) 걸림.

 

접속시간 비교

  데이터베이스 이용 접속시간 edis 이용 접속시간
접속 시간 1 2.60122 초 3.33604 초
접속 시간 2 2.57475 초 2.21812 초
접속 시간 3 2.59918 초 2.10289 초

 

 Redis 방식은 첫 접속에서는 다소 시간이 걸리지만, 이후 요청에서는 데이터베이스보다 빠른 성능을 보여준다. 

빈번한 요청 처리에는 Redis 방식이 더 적합하다는 것을 알 수 있다.

 

 

하지만 비동기 Redis를 처리한다면 API 요청 없이 접속자 리스트를 웹소켓에서 처리하면 좋겠다는 생각을 했다. 

 

클라이언트 웹소켓 접속 요청 → 접속자 Redis 저장 → 접속자 이벤트 및 목록 전송


Redis와 웹소켓을 활용한 개선

구현 방법 

비동기식 redis를 이용 접속자를 확인

 

1. chat_redis.py

#chat_redis.py

...
async def get_users_from_redis(key: str):
    users = await redis_client.smembers(key)
    return users
...

 chat_redis.py에서 Redis 값을 비동기적으로 가져오는 함수 추가

 

 

2.consumer.py

#consumers.py

async def connect(self):
...
    # 사용자 목록 갱신을 위한 이벤트
    users_redis = await get_users_from_redis(self.room_group_name)
    await self.channel_layer.group_send(
        self.room_group_name,{
            "type": "chat.update_users",
            "users": list(users_redis) # Redis에서 가져온 사용자 목록
        }
    ) 
    
...


#채팅방 접속자 디코딩 후 JSON으로 변환
async def chat_update_users(self, event):
    users = event["users"]
    users_str = [user.decode('utf-8') for user in users] # 바이트 디코드
    await self.send(text_data=json.dumps({
                "users": users_str
                }))

Redis 데이터를 활용해 접속자 목록을 전송하도록 변경 

Redis에 들어있던 데이터는 바이트 값으로 저장되어 있어 반드시 디코드를 해줘야 한다. 


결과 

  서버요청 / API 퀘리 시간 / 횟수 접속시간 1 접속시간 2 접속시간 3
웹소켓 이용 0 0 / 0 3.59765 초 2.41659 초 2.35043 초
redis 이용 1 206ms 3.33604 초 2.21812 초 2.10289 초
데이터베이스 이용 1 37ms 2.60122 초 2.57475 초 2.59918 초

 

 웹소켓 방식은 데이터베이스 접근이나 API 요청이 발생하지 않아 서버 효율성이 크게 향상되었으며, 접속 시간도 단축되어 사용자 경험이 개선됐다.


느낀 점

 이번 경험을 통해 웹소켓과 비동기 시스템의 활용 방법을  깊이 이해하게 됐다. Redis를 활용한 접속자 관리 방식은 서버 부하를 줄이고, 빈번히 발생하는 요청을 효과적으로 처리할 수 있었다. 남은 이슈에도 비슷한 방법을 적용해 실용적인 해결책을 찾아야겠다.