4 min read

๋Š๊ธฐ์ง€ ์•Š๋Š” ์—ฐ๊ฒฐ์ด ์•„๋‹ˆ๋ผ ์šฐ์•„ํ•˜๊ฒŒ ๋ณต๊ตฌ๋˜๋Š” ์—ฐ๊ฒฐ์„ ์œ„ํ•˜์—ฌ

Table of Contents

โ€œ๋ถ„๋ช… ๋กœ์ปฌ์—์„œ๋Š” ์ž˜ ๋๋Š”๋ฐ, ์™œ ๋ฐ–์—์„œ๋Š” ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ์ด ์•ˆ ๋˜๋‚˜์š”?โ€

์‹ค์‹œ๊ฐ„ ์„œ๋น„์Šค๋ฅผ ๋ฐฐํฌํ•˜๊ณ  ๋‚˜๋ฉด ๊ฐ€์žฅ ๋งŽ์ด ๋“ฃ๋Š” 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์„ ๋‹ค๋ฃจ๋‹ค ๋ณด๋ฉด ๊นจ๋‹ซ๊ฒŒ ๋˜๋Š” ์ง„๋ฆฌ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์™„๋ฒฝํ•˜๊ฒŒ ์œ ์ง€๋˜๋Š” ์—ฐ๊ฒฐ์€ ํ™˜์ƒ์— ๊ฐ€๊น๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ง„์ •ํ•œ ๊ณ ์ˆ˜์˜ ์‹ค๋ ฅ์€ ์—ฐ๊ฒฐ์ด ์–ผ๋งˆ๋‚˜ ์˜ค๋ž˜ ์œ ์ง€๋˜๋А๋ƒ๊ฐ€ ์•„๋‹ˆ๋ผ, ์—ฐ๊ฒฐ์ด ๋Š๊ฒผ์„ ๋•Œ ์–ผ๋งˆ๋‚˜ ์šฐ์•„ํ•˜๊ณ  ๋น ๋ฅด๊ฒŒ, ๊ทธ๋ฆฌ๊ณ  ๋ฐ์ดํ„ฐ ์†์‹ค ์—†์ด ๋ณต๊ตฌํ•˜๋А๋ƒ์—์„œ ๊ฒฐ์ •๋ฉ๋‹ˆ๋‹ค.

์—ฌ๋Ÿฌ๋ถ„์˜ ์„œ๋น„์Šค๋Š” ์ง€๊ธˆ ์ด ์ˆœ๊ฐ„์—๋„ ์ˆ˜์—†์ด ๋Š๊ธฐ๊ณ  ์žˆ์„์ง€ ๋ชจ๋ฆ…๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์‚ฌ์šฉ์ž๊ฐ€ ๊ทธ ์‚ฌ์‹ค์„ ์ „ํ˜€ ๋ˆˆ์น˜์ฑ„์ง€ ๋ชปํ•˜๊ณ  ์žˆ๋‹ค๋ฉด, ์—ฌ๋Ÿฌ๋ถ„์€ ์ด๋ฏธ ํ›Œ๋ฅญํ•œ ์‹ค์‹œ๊ฐ„ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•œ ์…ˆ์ž…๋‹ˆ๋‹ค.