Dev

과도한 트래픽에 대한 방어하기

khjoon 2024. 12. 4. 22:08

고민한 부분

서비스를 배포하게 되면 한 사용자가 악의적으로 트래픽을 많이 보내게 될 경우 서버가 다운될 수도 있고 특정 기능은 외부 api를 사용하는 기능도 있어서 금전적으로 손해를 볼 수도 있겠다고 생각했습니다.
그래서 특정 위치에서 발생하는 과도한 트래픽을 제한해야겠다고 생각했습니다.

구현 방법

 

Bucket4j

토큰 버킷(Token Bucket) 알고리즘을 사용하여 API 요청률을 제한하는 데 사용됩니다.

 

동작원리

각 사용자 또는 API 키에 대한 버킷을 생성한다. 버킷으로 토큰의 개수를 제한할 수 있습니다.

일정한 속도로 버킷에 토큰이 추가됩니다.

API가 호출이 될 때 마다 버킷에 있는 토큰을 사용하게 되고 토큰이 부족하면 요청을 거부하거나 지연시킵니다.

 

구현

HTTP 요청에 대한 Rate Limiting을 구현했습니다.

  1. HTTP 요청 수신: 클라이언트가 서버로 HTTP 요청을 보냅니다.
  2. 인터셉터 작동: HttpInterceptor가 요청을 가로챕니다.
  3. 버킷 조회: RateLimitService에서 클라이언트별로 버킷을 조회하거나 생성합니다.
  4. 토큰 소비: 요청을 처리하기 전에 버킷에서 토큰을 소비합니다.
    토큰이 남아 있으면 요청을 처리합니다.
    토큰이 부족하면 요청을 거부하고, HTTP 429 상태 코드(Too Many Requests)를 반환합니다.
  5. 요청 처리: 요청이 허용되면 컨트롤러 메서드가 실행되고, 응답이 반환됩니다.

의존성 추가

implementation 'com.giffing.bucket4j.spring.boot.starter:bucket4j-spring-boot-starter:0.2.0'

 

사용자별 버킷을 생성하고 제한하는 서비스 로직을 작성합니다.

@Service
public class RateLimitService {

    private final Map<String, Bucket> cache = new ConcurrentHashMap<>();

    private String getHost(HttpServletRequest httpServletRequest){
        return httpServletRequest.getHeader("Host");
    }

    //접속 제한하기
    public Bucket resolveBucket(HttpServletRequest httpServletRequest) {
        return cache.computeIfAbsent(getHost(httpServletRequest), this::newBucket);
    }

    private Bucket newBucket(String apiKey) {
        return Bucket4j.builder()
                // 10개의 클라이언트가 10초에 1000개씩 보낼 수 있는 대역폭
                //.addLimit(Bandwidth.classic(1000, Refill.intervally(10, Duration.ofSeconds(10))))
                //10개의 클라이언트가 10초에 1000개씩 보낼 수 있는 대역폭
                .addLimit(Bandwidth.classic(10, Refill.intervally(10, Duration.ofSeconds(10))))
                .build();
    }



}

 

HttpInterceptor 클래스는 Spring MVC 인터셉터로, HTTP 요청을 가로채고 Rate Limiting을 적용합니다.

@Component
@Log4j2
public class HttpInterceptor extends HandlerInterceptorAdapter {
    RateLimitService rateLimitService = new RateLimitService();
    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) {
        Bucket bucket = rateLimitService.resolveBucket(request);
        log.info("================ Before Method");
        log.info("접속 ip 주소 '{}'", request.getRemoteAddr());
        log.info(request.getRemoteAddr());

        if (bucket.tryConsume(1)) { // 1개 사용 요청
            // 초과하지 않음
            log.info("초과 안함");
            return true;
        } else {
            // 제한 초과
            log.info("{} 트래픽 초과!!!", request.getRemoteAddr());
            return false;
        }
    }
    @Override
    public void postHandle( HttpServletRequest request,
                            HttpServletResponse response,
                            Object handler,
                            ModelAndView modelAndView) {
        log.info("================ Method Executed");
    }
    @Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler,
                                Exception ex) {
        log.info("================ Method Completed");
    }
}

 

WebConfig 클래스는 Spring의 WebMvcConfigurer를 구현하여 인터셉터를 설정합니다.

@RequiredArgsConstructor
@Configuration
public class WebConfig implements WebMvcConfigurer {


    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        HttpInterceptor httpInterceptor = new HttpInterceptor();
        registry.addInterceptor(httpInterceptor);
    }
}

테스트

연속으로 api 요청을 했을때 트래픽이 제한되는 것을 확인할 수 있습니다.

참고: https://koogood.tistory.com/32