โ๋ถ๋ช ๋ก์ปฌ์์๋ ์ ๋๋๋ฐ, ์ ๋ฐ์์๋ ์ค์๊ฐ ๋ฐ์ดํฐ๊ฐ ๋ณ๊ฒฝ์ด ์ ๋๋์?โ
์ค์๊ฐ ์๋น์ค๋ฅผ ๋ฐฐํฌํ๊ณ ๋๋ฉด ๊ฐ์ฅ ๋ง์ด ๋ฃ๋ CS ์ค ํ๋์ ๋๋ค. ์ค์ ๋ก ์ ํฌ ์๋น์ค์์๋ WebSocket ์ ์ฌ์ฉํ์ฌ ์ค์๊ฐ ๋ฐ์ดํฐ๋ฅผ ์๋นํ๊ธฐ ๋๋ฌธ์, ์ค์๊ฐ ๋ฐ์ดํฐ๊ฐ ๋ณ๊ฒฝ๋์ง ์๋ ๊ฒ์๋งค์ฐ ์น๋ช ์ ์ธ ๋ฌธ์ ์ ๋๋ค.
์ฌ๋ฌด์ค์ ์์ ์ ์ธ ๊ธฐ๊ฐ ์์ดํ์ด ํ๊ฒฝ์์๋ new WebSocket() ํ ์ค์ด๋ฉด ๋ชจ๋ ๊ฒ ์๋ฒฝํด ๋ณด์
๋๋ค.
ํ์ง๋ง ์ฌ์ฉ์๊ฐ ์งํ์ฒ ํฐ๋์ ์ง๋๊ฑฐ๋, ์๋ฆฌ๋ฒ ์ดํฐ๋ฅผ ํ์ LTE์ ์์ดํ์ด๋ฅผ ์ค๊ฐ๋ ์๊ฐ, ์ฐ๋ฆฌ๊ฐ ๊ณต๋ค์ฌ ๋ง๋
์ค์๊ฐ ์ฐ๊ฒฐ์ ์๋ฆฌ ์์ด ์ฃฝ์ด๋ฒ๋ฆฝ๋๋ค.
๋จ์ํ ์ฐ๊ฒฐ์ด ๋๊ธฐ๋ ๊ฒ ๋ฌธ์ ๊ฐ ์๋๋๋ค. ์ง์ง ๋ฌธ์ ๋ โ์ฐ๊ฒฐ์ด ๋๊ฒผ๋ค๋ ์ฌ์ค์กฐ์ฐจ ๋ชจ๋ฅด๋ ์ํโ๊ฐ ์กด์ฌํ๋ค๋ ๊ฒ์ด์์ฃ .
TCP๋ ์ด์์์ง๋ง ๋ฐ์ดํฐ๋ ํ๋ฅด์ง ์๋ โ์ข๋น ์ปค๋ฅ์ โ
๋ธ๋ผ์ฐ์ ์ WebSocket API๋ ์๊ฐ๋ณด๋ค ๋ถ์น์ ํฉ๋๋ค. ๋คํธ์ํฌ๊ฐ ๋ฌผ๋ฆฌ์ ์ผ๋ก ์ฐจ๋จ๋์ด๋ onclose ์ด๋ฒคํธ๊ฐ ์ฆ์ ๋ฐ์ํ์ง ์๋ ๊ฒฝ์ฐ๊ฐ ํ๋คํ์ฃ . OS ๋ ๋ฒจ์ TCP Keep-alive๊ฐ ๋์ํ๊ธฐ๊น์ง๋ ๋๋ฌด ๊ธด ์๊ฐ์ด ๊ฑธ๋ฆฌ๊ณ , ๊ทธ๋์ ์ฌ์ฉ์๋ ๋ฉ์์ง๊ฐ ์ค์ง ์๋ ๋น ํ๋ฉด๋ง ๋ฐ๋ผ๋ณด๊ฒ ๋ฉ๋๋ค.
์ฐ๋ฆฌ๋ ์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ์ ํ๋ฆฌ์ผ์ด์ ๋ ๋ฒจ์ ํํธ๋นํธ(Heartbeat)๋ฅผ ์ง์ ๊ตฌํํด์ผ ํ์ต๋๋ค. ๋จ์ํ ์๋ฒ๊ฐ ์ด์์๋์ง ํ์ธํ๋ ์์ค์ ๋์ด, ์ผ์ ์๊ฐ ๋์ ์๋ต์ด ์์ผ๋ฉด ํด๋ผ์ด์ธํธ๊ฐ ๋จผ์ ์ฐ๊ฒฐ์ โ์ดํดโํ๊ณ ์ฌ์ฐ๊ฒฐ์ ์๋ํ๊ฒ ๋ง๋๋ ๊ฒ์ด ํต์ฌ์ ๋๋ค.
// ๋จ์ํ ์ด์์์์ ํ์ธํ๋ ๊ฒ ์๋๋ผ, '์ฃฝ์์ ๋ ํ์คํ ์ฃฝ์ด๋' ๋ก์ง
const HEARTBEAT_INTERVAL = 30000
const PONG_TIMEOUT = 5000
function setupHeartbeat(ws) {
let pingTimeout
const sendPing = () => {
// ์๋ฒ์ PING ์ ์ก
ws.send(JSON.stringify({ type: 'PING' }))
// 5์ด ์์ PONG์ด ์ ์ค๋ฉด ์ด ์ฐ๊ฒฐ์ ๊ฐ๋ง์ด ์๋ค๊ณ ํ๋จ
pingTimeout = setTimeout(() => {
console.warn('โ ๏ธ ์๋ต ์ง์ฐ. ์ฐ๊ฒฐ์ ๊ฐ์ ๋ก ์ข
๋ฃํ๊ณ ์ฌ์ฐ๊ฒฐ ๋ก์ง์ ํ์๋๋ค.')
ws.close()
}, PONG_TIMEOUT)
}
ws.onmessage = (event) => {
const data = JSON.parse(event.data)
if (data.type === 'PONG') {
// ์ ์ ์๋ต์ด ์ค๋ฉด ํ์์์ ํด์
clearTimeout(pingTimeout)
}
}
const intervalId = setInterval(sendPing, HEARTBEAT_INTERVAL)
ws.onclose = () => {
clearInterval(intervalId)
clearTimeout(pingTimeout)
}
}
์ฌ์ฐ๊ฒฐ ํญํ์ด ์๋ฒ๋ฅผ ๋ฎ์น ๋
์๋ฒ ๋ฐฐํฌ๋ฅผ ์ํด ์ธ์คํด์ค๋ฅผ ์ฌ์์ํ๋ ์๊ฐ, ์๋ง ๋ช
์ ํด๋ผ์ด์ธํธ๊ฐ ๋์์ onclose๋ฅผ ๊ฐ์งํฉ๋๋ค. ์ด๋ ๋ชจ๋ ํด๋ผ์ด์ธํธ๊ฐ setTimeout(() => connect(), 1000) ์ฒ๋ผ ์ ์งํ๊ฒ 1์ด ๋ค์ ์ฌ์ฐ๊ฒฐ์ ์๋ํ๋ฉด ์ด๋ป๊ฒ ๋ ๊น์? ์๋ฒ๋ ์ด์๋์๋ง์ ์๋ง ๊ฐ์ ์ปค๋ฅ์
์์ฒญ์ ํ๊บผ๋ฒ์ ๋ฐ๊ฒ ๋๊ณ , ๋ค์ ๋ถํ๋ฅผ ๊ฒฌ๋์ง ๋ชปํด ๋ป์ด๋ฒ๋ฆฌ๋ โThundering Herdโ ํ์์ด ๋ฐ์ํฉ๋๋ค.
์ด๋ ํ์ํ ๊ฒ์ด **์ง์ ๋ฐฑ์คํ(Exponential Backoff)**์ **์งํฐ(Jitter)**์ ๋๋ค. ์ฌ์ฐ๊ฒฐ ๋๊ธฐ ์๊ฐ์ 2๋ฐฐ์ฉ ๋๋ฆฌ๋, ์ฌ๊ธฐ์ ๋ฌด์์์ฑ์ ์์ด ์์ฒญ ์์ ์ ๋ถ์ฐ์ํค๋ ๊ฒ์ด์ฃ .
function connectWithRetry(attempt = 0) {
// 1์ด, 2์ด, 4์ด... ์ต๋ 30์ด๊น์ง ๋๊ธฐ ์๊ฐ ์ฆ๊ฐ
const baseDelay = Math.min(1000 * Math.pow(2, attempt), 30000)
// ์์ฒญ์ด ๊ฒน์น์ง ์๋๋ก 0~1์ด ์ฌ์ด์ ๋ฌด์์ ๊ฐ(Jitter) ์ถ๊ฐ
const jitter = Math.random() * 1000
setTimeout(() => {
const ws = new WebSocket(WS_URL)
ws.onopen = () => {
console.log('โ
์ฐ๊ฒฐ ์ฑ๊ณต!')
attempt = 0 // ์ฑ๊ณต ์ ์๋ ํ์ ์ด๊ธฐํ
}
ws.onclose = () => {
connectWithRetry(attempt + 1)
}
}, baseDelay + jitter)
}
๋๊ฒผ๋ ์ฐฐ๋์ ๋ฉ์์ง๋ฅผ ๋ณต๊ตฌํ๋ ๋ฒ
์ฌ์ฐ๊ฒฐ์ ์ฑ๊ณตํ๋ค๊ณ ํด์ ๋์ด ์๋๋๋ค. ์ฐ๊ฒฐ์ด ๋๊ฒจ์๋ 3์ด ๋์ ์๋ฒ์์ ๋ฐํ๋ ๋ฉ์์ง๋ ์ด๋๋ก ๊ฐ์๊น์? WebSocket์ ๊ธฐ๋ณธ์ ์ผ๋ก ๋ฉ์์ง ์ ๋ฌ์ ๋ณด์ฅํ์ง ์์ต๋๋ค.
์ฐ๋ฆฌ๋ ์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ชจ๋ ๋ฉ์์ง์ **์ํ์ค ๋ฒํธ(Sequence Number)**๋ฅผ ๋ถ์ฌํ์ต๋๋ค. ํด๋ผ์ด์ธํธ๊ฐ ์ฌ์ฐ๊ฒฐ๋ ๋ โ๋ ๋ง์ง๋ง์ผ๋ก 105๋ฒ ๋ฉ์์ง๊น์ง ๋ฐ์์ดโ๋ผ๊ณ ์๋ฒ์ ์๋ ค์ฃผ๋ฉด, ์๋ฒ๋ Redis ๋ฑ์ ์์ ์ ์ฅํด๋ ๋ฉ์์ง ํ์์ 106๋ฒ๋ถํฐ์ ๋ฐ์ดํฐ๋ฅผ ๋ค์ ๋ฐ์ด๋ฃ์ด ์ค๋๋ค.
์ด ๊ตฌ์กฐ๊ฐ ๊ฐ์ถฐ์ง๊ณ ๋์์ผ ๋น๋ก์ ์ฌ์ฉ์๋ ํฐ๋์ ์ง๋์จ ๋ค์๋ ๋ํ์ ํ๋ฆ์ ๋์น์ง ์๊ฒ ๋์์ต๋๋ค.
๊ฒฐ๊ตญ ์ค์๊ฐ์ โ์ฐ๊ฒฐโ๋ณด๋ค โ๋ณต๊ตฌโ์ ์์ ์ ๋๋ค
WebSocket์ ๋ค๋ฃจ๋ค ๋ณด๋ฉด ๊นจ๋ซ๊ฒ ๋๋ ์ง๋ฆฌ๊ฐ ์์ต๋๋ค. ์๋ฒฝํ๊ฒ ์ ์ง๋๋ ์ฐ๊ฒฐ์ ํ์์ ๊ฐ๊น๋ค๋ ๊ฒ์ ๋๋ค. ์ง์ ํ ๊ณ ์์ ์ค๋ ฅ์ ์ฐ๊ฒฐ์ด ์ผ๋ง๋ ์ค๋ ์ ์ง๋๋๋๊ฐ ์๋๋ผ, ์ฐ๊ฒฐ์ด ๋๊ฒผ์ ๋ ์ผ๋ง๋ ์ฐ์ํ๊ณ ๋น ๋ฅด๊ฒ, ๊ทธ๋ฆฌ๊ณ ๋ฐ์ดํฐ ์์ค ์์ด ๋ณต๊ตฌํ๋๋์์ ๊ฒฐ์ ๋ฉ๋๋ค.
์ฌ๋ฌ๋ถ์ ์๋น์ค๋ ์ง๊ธ ์ด ์๊ฐ์๋ ์์์ด ๋๊ธฐ๊ณ ์์์ง ๋ชจ๋ฆ ๋๋ค. ํ์ง๋ง ์ฌ์ฉ์๊ฐ ๊ทธ ์ฌ์ค์ ์ ํ ๋์น์ฑ์ง ๋ชปํ๊ณ ์๋ค๋ฉด, ์ฌ๋ฌ๋ถ์ ์ด๋ฏธ ํ๋ฅญํ ์ค์๊ฐ ์์คํ ์ ๊ตฌ์ถํ ์ ์ ๋๋ค.