현재 웹을 이용한 스트리밍 서비스를 만들기 위해 Kwitch
Twitch를 베낀라는 서비스를 만들고 있습니다. 단순히 재미 목적이며 수익 창출을 목표로 하지 않습니다!
스트리밍 서비스를 제공하기 위해선, 트위치처럼 시청자들이 현재 온라인되어 있는 채널 목록을 실시간으로 확인할 수 있어야 했다.
이 기능을 구현하기 위해서 고민했던 과정들을 적어보려고 한다.
초기엔 기능 구현에 집중해서 상단히 심플하게 구현했다.
데이터베이스에 Broadcast라는 테이블을 만들어 목록 조회시에 이를 페이징하여 제공하려는 생각이였다.
// Prisma Schema
model Broadcast {
id Int @id @default(autoincrement()) @map("broadcast_id")
channel Channel @relation(fields: [channelId], references: [id])
channelId String @map("channel_id")
startedAt DateTime @default(now()) @map("started_at")
endedAt DateTime? @map("ended_at")
viewerCount Boolean @default(0) @map("viewer_count")
@@map("broadcasts")
}
이후 endedAt
이 아직 null
인 방송은 열려있으므로, 그에 맞는 쿼리를 날려 응답을 할 생각이였다.
channelRouter.get("/live", async (req: Request, res: Response) => {
try {
const broadcasts = await prisma.broadcast.findMany({
where: {
endedAt: null
},
take: 10
});
res.json({ data: broadcasts });
} catch (err) {
res.status(500).json({ msg: "get online broadcast failed"})
}
});
하지만 이 과정은 여러가지 문제가 있다고 생각했다. 그 문제들을 나열해 보겠다.
먼저 라이브 방송을 가져오기 위해서 데이터베이스를 통해 이력들을 조회하는 방식은 상당히 비효율적이라고 생각했다. 특히 목록을 조회하는 요청은 이 서비스의 사용자들이 웹 앱에 들어오게 되면 무조건 조회하게 되며, 실시간 반영을 위해 자주 조회하게 된다. 이는 서버에 많은 부담을 가게할 것이다.
여기서 추가적으로 생각해봤던 방식은 목록의 결과를 캐싱하는 방식이였다. 이러면 속도나 주기적인 조회에 있어서 부담을 확실히 줄일 수 있다고 생각했지만, 이후에 시청자 수에 대한 랭킹 시스템을 적용할 예정인데 이 과정이 굉장히 불편해진다고 생각했다.
스키마를 살펴보면 viewerCount
라는 컬럼이 보일텐데, 이는 시청자 수를 나타낸다.
socket.on("broadcasts:join", async (channelId: string, done: Function) => {
try {
const broadcast = await prisma.broadcast.update({
where: {
channelId,
endedAt: null
},
data: { viewers: { increment: 1 } },
});
if (!channel) {
done({ success: false, message: "Channel is not online." });
return;
}
socket.join(channelId);
socket.to(channelId).emit("broadcasts:joined", user.username);
socket.to(channelId).emit("p2p:joined", socket.id);
console.log(`${user.username} joined ${channelId}.`);
done({ success: true });
} catch (err) {
done({ success: false, message: err.message });
}
});
소켓에 참여 요청을 보내면 Broadcast를 조회해서 viewers를 올리고 참여하는 방식이였는데, 문제는 동시에 참여하는 사람이 있다면 데이터베이스에 제대로 반영이 안될 수 있다는 점이다.
수많은 고민 끝에 Redis를 적용하기로 하였고, 구조는 다음과 같다.
이전에는 방송 기록을 남기고 싶어서 데이터베이스에 해당 테이블을 만들어 저장하려는 생각이였는데, 친구와의 토론으로 Shout out to mclub4 현재 라이브 방송에 대해선 Redis를 적용, 방송 기록에 대해서는 log를 생성해 저장하면 되겠다는 결론을 내렸다.
따라서 기존의 Broadcast 테이블을 제거하고 방송을 생성하는 코드가 이렇게 변경되었다.
// create a broadcast
const broadcast: Broadcast = {
title,
roomName: channel.id,
ownerId: user.id,
};
// set broadcast metadata in redis
await redisConnection.HSET(channelKey, {
viewers: 0,
broadcast: JSON.stringify(broadcast),
});
HSET
을 사용하여 시청자 수를 나눈 이유는 Redis의 HINCRBY
를 이용해 시청자수를 간편하게 올리고 내릴 수 있기 때문이다.
await redisConnection.HINCRBY(channelKey, "viewers", 1);
이렇게 구현하면서 위에서 얘기했던 동시성 문제는 싱글쓰레드인 Redis의 사용으로 정확한 시청자 수 값을 저장할 수 있게 되었다.
또한 이후에 동시 아이디 접속을 제한하고 싶으면 Redis lists를 이용하여 구현 방식을 바꿔도 좋다고 생각한다.
해당 채널이 온라인인지 확인하는 과정도 간단해졌다.
단순히 키가 존재하는지 EXISTS
를 이용하면 된다.
const channelKey = getChannelKey(channelId);
const isOnLive = await redisConnection.EXISTS(channelKey);
if (!isOnLive) {
return done({ success: false, message: "This channel is offline" });
}