[Feature] WebSocket 이벤트에 대해 throttler guard를 통해 rate limit을 추가한다 - #460
Draft
HoonDongKang wants to merge 7 commits into
Draft
[Feature] WebSocket 이벤트에 대해 throttler guard를 통해 rate limit을 추가한다#460HoonDongKang wants to merge 7 commits into
HoonDongKang wants to merge 7 commits into
Conversation
- 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, 에러 처리 테스트 작성
There was a problem hiding this comment.
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 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 | |||
|
|
|||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🔗 관련 이슈
Closes #459
✅ 작업 내용
💡개선 사항
Backend
@nestjs/throttler기반 WebSocket rate limit 처리를 추가했습니다.WsThrottlerGuard를 추가해battle:chat,battle:attack,battle:defense이벤트별 throttle을 적용했습니다.battle:chat: 배틀 채팅 이벤트 (1초 3battle:attack: 배틀 이의제기 이벤트battle:defense: 배틀 반박 이벤트throttler:battle:chat:019ef8d0-5161-7408-adc5-fa9c637f866f:019f0283-b681-7301-b17a-92122e690e55ThrottlerStorageadapter를 추가해 요청 카운터와 TTL을 Redis에서 관리하도록 변경했습니다.WsThrottleException을 발생시키고, 전용 filter에서 클라이언트로THROTTLE_ERROR이벤트를 emit하도록 처리했습니다.Frontend
THROTTLE_ERROR이벤트를 수신해 toast로 사용자에게 안내하도록 추가했습니다.CHATTED이벤트를 수신 후 메시지를 반영하도록 변경하였습니다.📸 참고 사항
주요 확인 파일
backend/src/battles/guards/WsThrottler.guard.tsbackend/src/battles/adapters/out/throttler/redisThrottlerStorage.adapter.tsbackend/src/battles/filters/wsThrottleException.filter.tsfrontend/src/pages/battlePage/hooks/useBattleSocketErrorHandling.tsfrontend/src/features/battle/hooks/useBattleChat.tsrate limit은 스팸 방지 목적이며, “라운드당 한 번만 제출 가능” 같은 게임 규칙 검증은 도메인 로직에서 별도로 처리해야 합니다.
guard에서는 throttle 여부만 판단하고, block 상황은
WsThrottleException으로 넘겨 filter에서 클라이언트 이벤트로 변환합니다.chat은 기존 optimistic UI 때문에 throttle block 이후에도 화면에 메시지가 남는 문제가 있어 서버 응답 기반 반영 방식으로 변경했습니다.
Redis counter 증가는 Lua script를 사용해
INCR와 TTL 설정을 원자적으로 처리합니다.💬 질문사항 (고민했던 부분)