TIL

24/02/16 TIL __ 실시간 시청자수 계산 오류(유저테스트)

GABOJOK 2024. 2. 16. 23:59

 

 

 

 

유저테스트를 하는데 실시간 방송 시청자수 계산에 오류가 있었다.
분명 10명은 넘게 있었는데, 유저가 3명이 있다고 나와있었다.

혼자서 창을 여러개 띄워서 테스트 할때는 40개까지 문제가 없었는데,

유저가 20명 정도 오자 이슈가 발생했다.

 

 

그래서 이 부분을 다시 찾아보게 되었다.

아래는 기존 로직이다

@SubscribeMessage('count_live_chat_user')
async countLiveChatUser(client: Socket, channelId: string) {
    Logger.log(`5초마다 실행되는 카운트 라이브 챗 유저 함수 ${client.id}`);
    const room = this.server.sockets.adapter.rooms.get(channelId)?.size;
    console.log('룸의 사이즈', room);
    const rooms = this.server.sockets.adapter.rooms;

    let newWatchCount = [];
    const obj = {};
    const keys = Object.fromEntries(rooms);

    for (let data in keys) {
        if (data.length > 20) {
            newWatchCount.push(`${data}_${keys[data].size}`);
        }
    }

    newWatchCount.map(async (e) => {
        const arr = e.split('_');
        return (obj[arr[0]] = arr[1]);
    });

    if (Object.keys(obj).length >= 1) {
        await this.redis.hSet('watchCtn', obj);
    }
}



@UseGuards(WsGuard)
@SubscribeMessage('create_room')
async createLiveRoomChat(client: Socket, channelId: string): Promise<any> {
    const createChatRoom = await this.chatService.createChatRoom(channelId, client);
    Logger.log(`채팅방이 생성되었어요.${client.id}`)
    await this.countLiveChatUser(client, channelId);
    this.whileRepeat = true;
    this.interval = setInterval(async () => {
        Logger.log(`라이브 방송 참여자 수 계산하는 인터벌이 작동중입니다.${client.id}`);
        if (!this.whileRepeat) return;
        await this.countLiveChatUser(client, channelId);
    }, 5000);

    return createChatRoom;
    return true;
}

 

 

setInterval로 실시간 유저수를 계산하고,
유저가 방송을 시작하면 유저수 계산 로직인 setInterval이 작동하기 시작하는 로직이다.

그러다 보니, 현재 로직으로는 채팅방이 10개가 생기면 10개의 인터벌이 생성된다.


즉 5초마다 현재 있는 모든 방들에 시청자 수를 계산한는 로직이 아니라,
방이 생길때마다 바로바로 만들어 주는 형식이다.
그리고 방송이 종료되면?그땐 인터벌이 종료된다.

 

이걸 어떻게 바꾸는게 더 좋을지 고민했다.
메인 화면에 방송이 1개이상 있으면 cron을 이용해 5초마다 계산을 해볼까? 라는 생각을 가지고 

setInterval과  cron의 장단점을 비교해 보기 시작했다.

 

 

 

💜 setInterval과 cron의 장단점

 

nest에 cron 이라는게 있다는걸 동기분께서 알려주셔서 알게 되었는데,

이미 setInterval로 처리를 해두었고 딱히 뭐가 다른지 잘 모르겠어서 그냥 넘어갔었다.

그런데 이런 이슈가 생기고 나니 cron이라는것과 setInterval의 차이가 궁금해 졌다.

 

찾아보니 setInterval은

  • 자바스크립트 환경에서 코드 내에 직접 주기적 작업을 하는 방식인데,
  • 별도의 시스템 스케줄러가 아니라 nodejs 서버에서 실행된다.
  • 그러나 이벤트 루프 기반으로 실행중인 프로세스 내에 동작하기 때문에 부하가 커지면, 지연이 발생하는 특성이 있다.
  • 또한 여러개의 setInterval를 사용한다면 메모리 누수같은 이슈가 생길 수 있다.

이에 반해 cron 은

  • 시스템 레벨에서 시간 기반으로 스케줄링 하기에 정해진 시간에 작업을 실행한다.
  • 또 별도의 프로세서에서 실행되기 때문에, 애플리케이션 성능에 영향을 덜 준다.
  • 복잡한 스케줄링이 지원되는 특성이 있는데, 예를들면 몇월 몇일 몇시 몇분 단위 처럼 ! 그때 정확히 실행이 되어야 하는 경우 사용하면 좋다.
  • 단점으로는 직접적인 사용자와의 상호작용이 필요할 때는 적합하지 않은 특성이 있다.

 

 

정확하게 어떤 성능적 차이가 있는지는 테스트를 해봐야 알것 같아서 
cron으로 해보기로 했다.

일단 nestjs 공식문서를 보며 cron을 적용해 봤다.

//실시간 방송 시청자수
@Cron('*/5 * * * * *')
async roomUserCtn(socket: Socket, channelId: string) {
console.log('==========>', socket); //==========> undefined [class Server extends StrictEventEmitter]
const roomUserCtn = await socket.nsp.adapter.rooms.get(channelId)?.size;
console.log('roomUserCtn', roomUserCtn);
//return roomUserCtn;
}




서비스 단 로직을 이렇게 바꿔봤는데, undefind가 나온다.
cron을 안쓸때는 값이 잘 나오는데 말이다.
이상해서 찾아보니 cron 은 socket에 의존할 수 없기 때문에, 이런 결과가 나온다고 한다.ㅠㅠ

 

Cron 작업에서 직접 Socket 객체를 사용하는 것은 기본적으로 지원되지 않습니다.
Cron 작업은 주기적으로 실행되는 타이머 기반 작업으로,특정 소켓 연결이나 요청에 대한 정보 없이 독립적으로 실행됩니다.

 

 


그럼 방법이 없는건가 좌절하던 그때, server에 socket을 주입해 사용하는 방법을 보게 되었다.
이렇게 사용할때 혹시 실시간 유저의 참여 수를 반영되지 않는건가 걱정되었지만,

 

Socket.IO 서버 인스턴스를 `setServer` 메서드를 통해 한 번 `ChatService`에 설정한 후에도, 해당 인스턴스를 통해 접근하는 소켓 연결들은 실시간으로 변경되는 사용자 연결 상태를 반영합니다

라는 글이 있어서 일단 시도해 보기로 했다.

 


일단 redis.adapter를 사용해 소켓 서버를 관리하고 있기 때문에, 
redis.adapter 파일에 serverInstance를 내보내 주는 함수를 만든 후, 
main.ts에 아래처럼 서버를 chatService에 넣어줬다.

const chatService = app.get(ChatService);
chatService.setServer(redisIoAdapter.getServerInstance());





chat.service 에 로직을

//socket 객체 저장
public setServer(server: Server) {
this.server = server;
}

//실시간 방송 시청자수
@Cron('*/5 * * * * *')
async roomUserCtn() {
const roomsMapObj = this.server.sockets.adapter.rooms;
const watchCtn = {};
for (let [k, e] of roomsMapObj) {
const channelIdExists = await this.userService.FindChannelIdByChannel(k);
watchCtn[k] = e.size;
}
return watchCtn;
}



이런식으로 추가 후, app.controller에서 해당 함수를 불러와 사용했다.
내일은 대망의 테스트를 통해 결과를 알아봐야 겠다.