Skip to content

[Feature] WebSocket 이벤트에 대해 throttler guard를 통해 rate limit을 추가한다 - #460

Draft
HoonDongKang wants to merge 7 commits into
devfrom
feature/#459-socket-rate-limit
Draft

[Feature] WebSocket 이벤트에 대해 throttler guard를 통해 rate limit을 추가한다#460
HoonDongKang wants to merge 7 commits into
devfrom
feature/#459-socket-rate-limit

Conversation

@HoonDongKang

Copy link
Copy Markdown
Collaborator

🔗 관련 이슈

Closes #459

✅ 작업 내용

💡개선 사항

Backend

  • @nestjs/throttler 기반 WebSocket rate limit 처리를 추가했습니다.
  • WsThrottlerGuard를 추가해 battle:chat, battle:attack, battle:defense 이벤트별 throttle을 적용했습니다.
    • battle:chat: 배틀 채팅 이벤트 (1초 3
    • battle:attack: 배틀 이의제기 이벤트
    • battle:defense: 배틀 반박 이벤트
  { name: BATTLE_CLIENT_EVENTS.CHAT, ttl: 1000, limit: 5 },
  { name: BATTLE_CLIENT_EVENTS.ATTACK, ttl: 1000, limit: 1 },
  { name: BATTLE_CLIENT_EVENTS.DEFENSE, ttl: 1000, limit: 1 },
  • Throttler Key: throttler:${event}:${tracker}` 와 같이 구성하였습니다.
    • throttler:battle:chat:019ef8d0-5161-7408-adc5-fa9c637f866f:019f0283-b681-7301-b17a-92122e690e55
  • Redis 기반 ThrottlerStorage adapter를 추가해 요청 카운터와 TTL을 Redis에서 관리하도록 변경했습니다.
  • Redis throttler 처리 중 발생하는 오류를 별도 에러 타입으로 분리했습니다.
  • throttle limit 초과 시 WsThrottleException을 발생시키고, 전용 filter에서 클라이언트로 THROTTLE_ERROR 이벤트를 emit하도록 처리했습니다.

Frontend

  • FE에서 THROTTLE_ERROR 이벤트를 수신해 toast로 사용자에게 안내하도록 추가했습니다.
  • 기존 채팅은 낙관적 UI로 서버로 이벤트를 보내기 전 UI에 먼저 보여주었는데, 이 부분을 제거하고 서버로부터 CHATTED 이벤트를 수신 후 메시지를 반영하도록 변경하였습니다.

📸 참고 사항

  • 주요 확인 파일

    • backend/src/battles/guards/WsThrottler.guard.ts
    • backend/src/battles/adapters/out/throttler/redisThrottlerStorage.adapter.ts
    • backend/src/battles/filters/wsThrottleException.filter.ts
    • frontend/src/pages/battlePage/hooks/useBattleSocketErrorHandling.ts
    • frontend/src/features/battle/hooks/useBattleChat.ts
  • rate limit은 스팸 방지 목적이며, “라운드당 한 번만 제출 가능” 같은 게임 규칙 검증은 도메인 로직에서 별도로 처리해야 합니다.

  • guard에서는 throttle 여부만 판단하고, block 상황은 WsThrottleException으로 넘겨 filter에서 클라이언트 이벤트로 변환합니다.

  • chat은 기존 optimistic UI 때문에 throttle block 이후에도 화면에 메시지가 남는 문제가 있어 서버 응답 기반 반영 방식으로 변경했습니다.

  • Redis counter 증가는 Lua script를 사용해 INCR와 TTL 설정을 원자적으로 처리합니다.


💬 질문사항 (고민했던 부분)

  • FE 낙관적 UI 제거 여부
  • Throttle 기준 여부

- WebSocket 이벤트에 적용할 WsThrottlerGuard 추가
- 현재 처리 중인 socket event와 throttler name을 비교해 대상 이벤트만 제한하도록 구현
- battleId, userId, eventName 기반으로 throttle key 생성
- payload에 battleId가 없을 경우 socket data의 battleId를 fallback으로 사용
- throttle limit 초과 시 WsThrottleException을 발생시키도록 처리
- Redis throttler storage 에러 발생 시 WebSocket throttle 예외로 변환
- WsThrottlerGuard의 이벤트 매칭, key 생성, block 처리, storage 에러 변환 테스트 작성
- @nestjs/throttler에서 사용할 Redis 기반 ThrottlerStorage 구현
- Redis Lua script로 counter 증가와 TTL 설정을 원자적으로 처리
- Redis 응답을 ThrottlerStorageRecord 형식으로 변환
- limit 초과 여부를 기준으로 isBlocked, timeToExpire, timeToBlockExpire 계산
- Redis eval 실패, 잘못된 응답 형식, 숫자가 아닌 응답 값을 RedisThrottlerStorageError로 변환
- RedisThrottlerStorageAdapter의 정상 카운트, block 상태, TTL fallback, 에러 처리 테스트 작성

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

WebSocket 기반 BattlesGateway의 주요 사용자 입력 이벤트(CHAT, ATTACK, DEFENSE)에 대해 스팸 방지 목적의 rate limit을 추가하고, 제한 발생 시 클라이언트가 별도 에러 이벤트를 수신해 UI/로깅 처리할 수 있도록 하는 변경입니다.

Changes:

  • Backend: @nestjs/throttler 기반의 WebSocket 전용 WsThrottlerGuard 및 Redis storage adapter/Lua script 기반 카운팅을 추가하고, throttle 예외를 전용 WS 이벤트로 변환하는 filter를 도입
  • Backend: 채팅 이벤트를 sender 포함 전체 room 브로드캐스트로 변경하여(optimistic UI 제거에 맞춰) 서버 응답 기반 반영이 가능하도록 조정
  • Frontend: THROTTLE_ERROR 수신 처리 및 채팅 optimistic UI 제거(서버 CHATTED 이벤트 수신 후 반영)

Reviewed changes

Copilot reviewed 15 out of 18 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
pnpm-lock.yaml @nestjs/throttler 의존성 lock 반영
backend/package.json @nestjs/throttler 의존성 추가
backend/src/app.module.ts ThrottlerModule + Redis 기반 storage 적용 및 이벤트별 정책 설정
backend/src/battles/battles.module.ts WsThrottlerGuard provider 등록
backend/src/battles/guards/WsThrottler.guard.ts Socket.IO 이벤트명 기반 key 생성 및 throttle 판단/예외 변환 로직 추가
backend/src/battles/guards/WsThrottler.guard.spec.ts WS throttler guard 단위 테스트 추가
backend/src/battles/adapters/out/throttler/redisThrottlerStorage.adapter.ts Redis + Lua 기반 throttler storage adapter 추가
backend/src/battles/adapters/out/throttler/redisThrottlerStorage.adapter.spec.ts Redis throttler storage adapter 단위 테스트 추가
backend/src/battles/exceptions/wsThrottleException.ts throttle 전용 WS 예외(payload 포함) 추가
backend/src/battles/errors/redisThrottlerStorage.error.ts Redis throttler storage 오류 타입 분리
backend/src/battles/filters/wsThrottleException.filter.ts throttle 예외를 THROTTLE_ERROR 이벤트로 emit
backend/src/battles/filters/wsThrottleException.filter.spec.ts throttle filter 단위 테스트 추가
backend/src/battles/adapters/in/battles.gateway.ts CHAT/ATTACK/DEFENSE에 guard 적용 + throttle filter 적용 + 채팅 브로드캐스트 방식 변경
backend/src/battles/adapters/in/battles.gateway.spec.ts 테스트 모듈에 throttler 설정 추가(정책 값 포함)
backend/src/battles/dto/battleResult.dto.spec.ts PrismaBattle mock 필드 변경 및 타입 캐스팅 방식 변경
packages/types/src/socket-events.ts BATTLE_SERVER_EVENTS.THROTTLE_ERROR 이벤트 상수 추가
frontend/src/pages/battlePage/hooks/useBattleSocketErrorHandling.ts THROTTLE_ERROR 수신 시 toast + Sentry 로깅 추가
frontend/src/features/battle/hooks/useBattleChat.ts 채팅 optimistic UI 제거 및 서버 CHATTED 기반 반영 유지
Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +73 to +75
// throttler:battle:chat:019ef8d0-5161-7408-adc5-fa9c637f866f:019f0283-b681-7301-b17a-92122e690e55
return `throttler:${event}:${tracker}`
}
Comment thread backend/src/app.module.ts
Comment on lines +23 to +27
throttlers: [
{ name: BATTLE_CLIENT_EVENTS.CHAT, ttl: 1000, limit: 5 },
{ name: BATTLE_CLIENT_EVENTS.ATTACK, ttl: 1000, limit: 1 },
{ name: BATTLE_CLIENT_EVENTS.DEFENSE, ttl: 1000, limit: 1 },
],
Comment on lines +11 to +14
const error = exception.getError() as WsThrottleExceptionPayload

client.emit(BATTLE_SERVER_EVENTS.THROTTLE_ERROR, error)
}
Comment on lines 213 to 235
@@ -219,7 +219,7 @@ describe('BattleResultResponseDto', () => {
codeB: 'const prismaB = 2;',
language: 'TS',
category: 'REFACTORING',
type: 'PUBLIC',
isPrivate: false,
playTime: 'FIFTEEN_MIN',
topics: ['prisma-topic1', 'prisma-topic2'],
status: 'CLOSED',
@@ -231,7 +231,7 @@ describe('BattleResultResponseDto', () => {
createdAt: now,
updatedAt: now,
finishedAt: finishedAt,
}
} as PrismaBattle

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] WebSocket 이벤트에 대해 throttler guard를 통해 rate limit을 추가한다.

2 participants