Project/Optimization

[프로젝트] 2. API Rate Limiter 도입 - Spring Cloud Gateway

_은선_ 2024. 8. 26. 02:57
728x90
SMALL

트래픽 제한을 고려하게 된 이유

오늘은 지난번에 소개 드린 "올봄" 프로젝트의 2. API Rate Limiter 도입을 진행해보려고 한다.

이번에 개선할 프로젝트인 AI 기반 장년층 라이프 케어 서비스 "올봄"에서는 챗봇, 지도, ToDo, 게임, 일자리 총 5가지 기능이 있다.

이 중에서도 챗봇 기능에선  Langchain을 사용하여 LLM의 한 종류인 GPT-4 기반의 챗봇을 제공한다.

사용자들은 LLM 기반의 챗봇을 통해 일상 대화, 노인 편의 기능 및 유용 정보를 제공 받을 수 있다.

 

 

 

올봄은 장년층의 삶에 활기를 불어넣자는 목적으로 기획된 공익 서비스로, 현재까진 별도의 비즈니스 모델이 없다.

서비스 규모가 좀 더 커진다면 장년층을 대상으로 한 수익화 방안을 모색할 계획이 있지만, 지금까지는 수익화 전략이 따로 마련되어 있지 않은 상황이다.

 

챗봇 기능 운영을 위해서는 GPT-4 API 호출이 필요한데, 호출 한 건당 약 50-60원의 비용이 발생한다.

현재 올봄은 초기 사용자 확보를 위해 당분간 이 비용을 운영 측에서 부담하는 방식으로 서비스를 제공하고 있다.

 

그러나 악의적인 사용자가 나타나 챗봇에 지속적으로 요청을 보낼 경우, 예상치 못한 높은 GPT 사용 비용이 발생할 수 있다.

따라서, 사용자들의 요청 횟수에 제한을 두는 것이 필요하다고 판단했다.


API Rate Limiter

스프링에서 API Rate Limiter를 구현할 수 있는 방법은 여러가지가 있다.

그 중에서도, 나는 추후 확장성을 고려하여 메인 스프링 부트 서버 앞단에 Spring Cloud Gateway를 두어 API Rate Limiter를 구현하기로 하였다.


API Rate Limiter 코드

RateLimiterConfig.java

@Configuration
public class RateLimiterConfig {

    private static final String MEMBER_ID_ATTRIBUTE = "MEMBER_ID";

    @Bean
    public KeyResolver memberIdKeyResolver() {
        return exchange -> {
            Long memberId = exchange.getAttribute(MEMBER_ID_ATTRIBUTE);

            if (memberId != null) {
                return Mono.just(memberId.toString()); // MemberId를 기반으로 Rate Limiting 적용
            } else {
                return Mono.just("anonymous"); // MemberId가 없는 경우 기본값 사용
            }
        };
    }

    @Bean
    public RedisRateLimiter redisRateLimiter() {
        return new RedisRateLimiter(1, 1, 1);
    }
}

 

RequestRateLimitFilter.java

@Component
@Slf4j
public class RequestRateLimitFilter extends AbstractGatewayFilterFactory<RequestRateLimitFilter.Config> {

    private final KeyResolver memberIdKeyResolver;
    private final RedisRateLimiter defaultRateLimiter;

    public RequestRateLimitFilter(KeyResolver memberIdKeyResolver, RedisRateLimiter redisRateLimiter) {
        super(Config.class);
        this.memberIdKeyResolver = memberIdKeyResolver;
        this.defaultRateLimiter = redisRateLimiter;
    }

    @Override
    public GatewayFilter apply(Config config) {
        GatewayFilter filter = (exchange, chain) -> {
            KeyResolver keyResolver = getOrDefault(config.keyResolver, memberIdKeyResolver);
            RedisRateLimiter rateLimiter = getOrDefault(config.rateLimiter, defaultRateLimiter);
            String routeId = config.getRouteId();

            System.out.println("routeId = " + routeId);

            return keyResolver.resolve(exchange)
                    .flatMap(key -> rateLimiter.isAllowed(routeId, key))
                    .flatMap(rateLimitResponse -> {
                        if (rateLimitResponse.isAllowed()) {
                            return chain.filter(exchange);  // Rate limit이 허용된 경우
                        } else {
                            log.warn("Rate limit exceeded for key: ");
                            // TooManyRequestException 발생
                            return Mono.error(new TooManyRequestsException(AuthErrorCode.TOO_MANY_REQUESTS));
                        }
                    });
        };

        return filter;
    }

    private <T> T getOrDefault(T configValue, T defaultValue) {
        return configValue != null ? configValue : defaultValue;
    }

    @Getter
    @Setter
    public static class Config implements HasRouteId {
        private KeyResolver keyResolver;
        private RedisRateLimiter rateLimiter;
        private String routeId;
    }
}

 

TooManyException.java

public class TooManyRequestsException extends AllbomException{
    public TooManyRequestsException(ErrorCode errorCode) {
        super(errorCode);
    }
}

 

application.yml

server:
  port: 8081

spring:
  application:
    name: allbom-gateway
  cloud:
    gateway:
      routes:
        - id: api-server
          uri: http://localhost:8080
          predicates:
            - Path=/**
          filters:
            - name: RequestRateLimitFilter
  data:
    redis:
      host: localhost
      port: 6379

실행 결과

spring cloud gateway 서버의 port번호인 8081을 통해 API 엔드포인트로 요청을 보냈다.

위에서 봤듯이, RedisRateLimiter(1, 1, 1)은 초당 1개의 요청을 허용하고, 그 이상은 429 에러를 발생시킨다. 즉, 주어진 시간 내에 요청 수가 해당 설정을 초과했기 때문에, 429 에러가 난 것이다.

 

위에서 Error를 Custom한대로, 너무 많은 요청이 들어오면 클라이언트에게 429에러를 반환하고, 적절한 메시지를 전달하는 것을 확인할 수 있다.


개선된 아키텍처 

 

API Rate Limiter을 구현하기 위해 메인 애플리케이션 앞단에 Spring Cloud Gateway 서버를 추가하였다.

즉, Spring Cloud Gateway는 이 아키텍처에서 클라이언트 요청의 진입점이다. 

RedisRateLimiter는 사용자 또는 클라이언트별로 개별적인 버킷이 주어진다. 즉, 각 사용자마다 별도의 버킷이 할당되며, 이 버킷을 통해 요청 제한이 관리된다. 이를 통해 동일한 API에 대한 요청이라 하더라도, 각 사용자의 요청 빈도가 별도로 제한된다.

 

Spring Cloud Gateway와 RedisRateLimiter를 함께 사용할 때, 요청을 보낸 사용자의 고유한 식별자(예: IP 주소, API 키, 사용자 ID)를 기준으로 각 사용자에게 별도의 버킷이 할당된다. 이 과정에서 HTTP 헤더의 Bearer JWT 토큰을 디코딩하여 사용자 고유의 멤버 ID를 추출하고, 이를 기준으로 요청 제한을 적용하였다.

Gateway 서버에서 이미 JWT 디코딩을 통해 멤버 ID를 추출하므로, 이 값을 메인 서버에 헤더로 전달하여 추가적인 처리를 간소화하고, 효율적으로 활용할 수 있도록 변경하였다.

 

이를 통해 사용자의 요청 수를 효과적으로 제한하고, 서비스의 안정성을 유지하면서 악의적인 요청으로 인한 서버 과부하를 방지할 수 있게 되었다. 이 Gateway 서버는 요청의 분배, 필터링, 그리고 인증과 같은 추가적인 기능도 수행하여 전체 아키텍처의 유연성과 확장성을 높여준다.


참고 문헌

https://pgmjun.tistory.com/m/153

 

Bucket4j를 사용해서 스프링부트 트래픽 제한하기

트래픽 제한을 고려하게 된 이유트래픽 제한에 대한 고민은 gpt-3.5-turbo 모델을 Fine-Tuning하여 사용하는 졸업작품 코리를 개발하면서 시작되었다.코리는 초기 유저들을 모으기 위해 한동안은 유료

pgmjun.tistory.com

https://dkswnkk.tistory.com/m/732

 

Bucket4j로 트래픽 제한하기(Redis & MariaDB)

개요 최근 업무 프로젝트에서 특정(요금이 부가되는) 로직에 대해 월별 사용량을 제한하는 기능이 추가되어야 했습니다. 이와 관련하여 처리율 제한 기술을 알아보았는데 Bucket4j, Guava, RateLimitj,

dkswnkk.tistory.com

 

https://bossm0n5t3r.github.io/posts/spring-boot-bucket4j/

 

Bucket4j 로 API Rate Limiting 를 구현해보자 | The Archive

1 2 3 4 5 6 enum class UserRole { ADMIN, PREMIUM, USER, ANONYMOUS, }

bossm0n5t3r.github.io

https://dkswnkk.tistory.com/m/732

 

Bucket4j로 트래픽 제한하기(Redis & MariaDB)

개요 최근 업무 프로젝트에서 특정(요금이 부가되는) 로직에 대해 월별 사용량을 제한하는 기능이 추가되어야 했습니다. 이와 관련하여 처리율 제한 기술을 알아보았는데 Bucket4j, Guava, RateLimitj,

dkswnkk.tistory.com


https://devs0n.tistory.com/70?category=865955

 

Request Rate Limiting with Spring Cloud Gateway - 2. RequestRateLimiter 필터 적용

앞서 개요에서 Spring Cloud Gateway에서 기본적으로 Request Rate Limiting 기능을 제공한다고 하였다. 이번 포스팅에서는 Spring Cloud Gateway에서 기본적으로 제공하는 Request Rate Limiting 기능과 이를 사용하는

devs0n.tistory.com

https://woooongs.tistory.com/56

 

Spring Cloud Gateway RateLimiter 적용

Local 환경에서 RedisRateLimiter 를 적용한 과정. 1. docker 를 이용해서 redis 띄우기 docker run -d -p 6379:6379 --name rate_limiter_redis redis - port 를 mapping 하지 않아서 spring cloud gateway 에서 redis 로 접속이 안되는

woooongs.tistory.com

https://velog.io/@on5949/Api-Gateway%EC%97%90%EC%84%9C-Rate-Limiter-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0Throttling

 

Api Gateway에서 Rate Limiter 구현하기(Throttling, Spring Cloud Gateway)

Rate Limiter 무엇인가? Rate Limiter는 간단하게 말해서 서버가 클라이언트의 시간당 요청횟수를 제한하는 기술을 의미한다. 보통 Api를 사용할 때 분당, 혹은 시간당 몇회 요청이 제한되어 있는 것을

velog.io

https://dgle.dev/RateLimiter1/

 

Request Rate Limiter를 만들어보자! 1편 | Dongle

Make Custom Rate Limiter With Spring Cloud Gateway 1

dgle.dev

https://velog.io/@whcksdud8/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Rate-limit-%ED%95%B8%EB%93%A4%EB%A7%81-%EB%B0%8F-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81

 

[프로젝트] Rate limit 핸들링을 통한 무분별한 API 호출 방지하기

들어가기 앞서 프로젝트를 진행하면서 구현하게 되는 대부분은 특정 서비스를 수행하기 위한 api 설계가 대부분이었던 것 같습니다. (개발 레벨) 하지만 현업에 계신 개발자님들의 이야기를 종

velog.io

 

 

728x90
LIST