@Component public class RateLimiter { private static final long RATE_LIMIT_TIME_MS = 10_000; // 요청을 카운팅하는 시간 단위 private static final long MAX_REQUESTS = 5; // 10초 동안 최대 5개의 요청만 허용 private final RedisTemplate<String, String> redisTemplate; public RateLimiter(RedisTemplate<String, String> redisTemplate) { this.redisTemplate = redisTemplate; } public boolean allow(String userId) { long now = System.currentTimeMillis(); // user 단위로 API 요청 횟수를 제한하기 위해, userId를 기반으로 Key를 생성 // (IP 단위로 API 요청 횟수를 제한하고 싶다면, userId 대신 IP를 파라미터로 받으면 된다.) String redisKey = "rate_limit:" + userId; // 서로 다른 요청을 구분하기 위해 UUID로 requestId 값을 생성 String requestId = UUID.randomUUID().toString(); // 1. Soted Set에서(redisKey)에서 score가 0 이상이고 (now - 10_000) 이하인 모든 member를 삭제 // = 최근 10초 내에 저장한 member만 남겨두고, 나머지 member는 전부 삭제 redisTemplate .opsForZSet() .removeRangeByScore(redisKey, 0, now - RATE_LIMIT_TIME_MS); // 2. 현재 들어온 API 요청을 Sorted Set에 저장. // member를 UUID 기반으로 생성된 고유의 requestId로 저정하고, // score를 현재 시간값(ex. 1735972805123)으로 저장한다. // SortedSet에 저장하는 것이기 때문에 score를 기준으로 자동 정렬돼서 저장된다. redisTemplate .opsForZSet() .add(redisKey, requestId, now); // 3. Sorted Set에 저장되어 있는 데이터 개수 조회 // = 최근 10초 내에 요청을 보낸 횟수 Long count = redisTemplate .opsForZSet() .size(redisKey); // 4. 만료 시간(TTL)을 10초로 설정함으로써 // Redis 공간을 불필요하게 많이 점유하는 것을 방지 redisTemplate.expire( redisKey, 10_000, TimeUnit.MILLISECONDS ); // 최대 요청 수보다 작거나 같으면 true를 리턴 // 최대 요청 수를 초과하면 false를 리턴 return count <= MAX_REQUESTS; } }
// OncePerRequestFilter : HTTP 요청이 들어올 때마다 단 한 번 실행되는 필터 로직 @Component public class RateLimitFilter extends OncePerRequestFilter { private final RateLimiter rateLimiter; public RateLimitFilter(RateLimiter rateLimiter) { this.rateLimiter = rateLimiter; } @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain ) throws ServletException, IOException { // 어떤 사용자인지 식별하기 위한 키 // (실무에서는 IP 값을 활용해 식별하는 편) String userId = request.getHeader("USER-ID"); // userId가 null일 땐 RateLimiter가 작동하지 않게 바로 return하기 if (userId == null) { return; } // rateLimiter.allow()가 false라는 건 API 요청 횟수를 초과했다는 뜻이다. // API 요청 횟수가 초과할 경우 429(TOO MANY REQUESTS)로 응답하게 된다. if (!rateLimiter.allow(userId)) { response.setStatus(429); return; } filterChain.doFilter(request, response); } }


