개요
KOALA 서비스 회원가입에서는 총 2 가지의 인증이 요구된다.
- 이메일 인증
- 문자 인증
문자 인증은 1 건당 대략 20원 정도 청구된다.
만약 누군가 의도적으로 많은 요청을 보내면, 불필요한 요청이 쌓이고 비용이 지불된다.
이를 예방하기 위한 처리율 제한 기능을 추가해보자!
처리율 제한(Rate Limit)
일정 시간 내에 요청되는 최대 횟수를 제한하는 기술이다.
- 과도한 요청으로 인한 서비스 장애 및 피해를 방지한다.
예시
- 사용자가 5회 이상 잘못된 비밀번호를 입력할 경우, 15분 동안 해당 계정의 로그인 시도를 차단
- 초당 최대 1000명의 사용자가 접속할 수 있도록 제한하며, 초과 시 대기열에 넣거나 "사이트가 과부하 상태입니다"라는 메시지를 표시
- 한 사용자가 특정 API에 1초에 100번의 요청을 보내고자 할 때, 1분 동안 60회의 요청만 가능하도록 제한
처리율 제한 알고리즘
이를 구현하는 방법은 3가지가 있다.
1. 토큰 버킷 알고리즘(Tocken Bucket Algorithm) ✔️
- 버킷과 토큰으로 요청을 관리한다.
- 일정 속도로 버킷에 토큰이 채워지며, 요청이 발생할 때마다 토큰이 소비된다.
- 버킷 안의 토큰이 모두 소진되면 요청이 제한된다.
- 짧은 시간에 집중되는 트래픽(burst of traffic)도 처리 가능하다. ➡️ 버킷에 토큰이 남아있기만 하다면 시스템에 요청이 전달된다.
2. 누출 버킷 알고리즘(Leaky Bucket Algorithm)
- 바구니에 물이 들어오고 구멍을 통해 일정한 속도로 물이 빠져나간다. 양동이의 물높이는 일정하게 유지되면서 물이 넘치지 않도록 설계되어 있다. ➡️ 양동이에 물이 가득차면 더 이상 물을 붓지 못하듯이, 시스템의 처리 능력을 초과하는 요청이 들어오면 새로운 요청은 거부 및 대기한다.
- 요청은 큐(Queue)에 저장되고 FIFO으로 요청이 처리된다.
- 큐 크기를 제한하여 메모리 사용량을 조정할 수 있다.
- 단시간에 요청이 몰리면 큐에 요청이 쌓이고, 제때 처리하지 못하면 최신 요청들은 버려지게 된다.
3. 슬라이딩 윈도우 알고리즘(Sliding Window Algorithm)
- time-based! 요청이 들어오는 시간을 "윈도우"로 설정한다. ➡️ 윈도우는 고정값이 아닌 동적으로 변한다.
- 이 시간 범위 내에서 허용된 요청을 카운트하여 처리한다.
- 일정 시간 내에(슬라이딩 윈도우 안에) 요청이 초과되면 더 이상 요청을 받지 않는다.
토큰 버킷 알고리즘을 선택한 이유
① 토큰 버킷 알고리즘은 정해진 기간 동안 사용자마다 개별적인 토큰을 갖게 되고, 이를 통해 처리율을 제한한다. 리딩 버킷은 다수의 사용자를 대상으로 처리율을 제한하므로 토큰 버킷 알고리즘이 더 적합하다.
② 슬라이팅 윈도우도 마찬가지로 특정 시간 동안 모든 사용자로부터의 요청 수를 카운트하여 제한한다.
③ 유연성있는 처리율 제한을 지원한다.
// Param: (버킷에 담을 최대 토큰 개수, 버킷에 토큰을 채우는 주기)
Refill.intervally(MAX_REQUESTS_PER_DAY, Duration.ofDays(1))
이러한 구현 방식으로 토큰 생성 주기나 소비 속도를 유연하게 조정할 수 있다!
토큰 버킷 알고리즘 라이브러리
토큰 버킷을 구현하기 위한 라이브러리는 3 가지가 있다.
- Bucket4j
- Resilence4j
- Guava LateLimiter
1. Bucket4j
- Java 기반으로 구현된 처리율 제한 라이브러리다.
- thread- safe하도록 구현되어 있다.
- 원자적 변수: AtomicInteger, AtomicLong과 같은 클래스를 사용해서 스레드 간의 경쟁 조건을 방지하였다.
- CAS(Compare-And-Swap) 연산: 토큰을 소비할 때 CAS 연산을 통해 현재 토큰 수를 확인하고 새로운 수로 업데이트 한다.
2. Resilence4j
- MSA 등과 같은 분산 환경에서 사용하기 좋다.
Circuit Breaker, Fallback 기술을 통해 다른 모듈로 장애가 퍼지지 않도록 방지하고, 대체 처리를 하는 등 분산 환경 운영에 도움된다. - 분산 환경에서의 사용을 위해 Hazelcast, Consul 등의 분산 저장소와 연동 가능
3. Guava
- 구글에서 제공하는 라이브러리로, 동기식 API만 지원한다.
Bucket4j를 선택한 이유
- float, double 자료형을 사용하지 않고 오직 정수형으로만 연산을 처리한다.
부동 소수점 연산은 과학/공학 계산용으로 설계되어 있다. 넓은 범위의 수를 빠르게 정밀한 ‘근사치’로 계산하는 것에는 유용하지만 정확도를 요구하는 연산에서는 사용하면 안 된다. ( 정수형의 안정성에 대한 상세한 내용 )
- 이 라이브러리는 기본적으로 lock-free, thread-safe하도록 설계되었다. 멀티 스레딩 환경에서의 확장성이 높고, 다양한 동시성에 대응 가능하다.
해당 깃허브에서 Atomic 구현 & CAS에 대해 자세히 확인할 수 있다.
- GC(Gabage Collector) 부담 최소화를 위해 boxing type 대신 primitive type을 사용한다.
(Boxing vs. Primitive에 대한 상세한 내용)
- spring boot의 Interceptor 기능을 통해서 요청 처리 전후 Rate Limit 적용이 가능하다.
- Interceptor - MVC Handler
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { bu registry.addInterceptor(new MyInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/login", "/logout"); } }
- Interceptor - MVC Handler
Bucket4j로 처리율 제한하기
1. 라이브러리 의존성 추가
implementation 'com.github.vladimir-bukhtoyarov:bucket4j-core:7.6.0'
2. Refill, Bandwidth, Bucket class 사용
Refill refill = Refill.intervally(10, Duration.ofMinutes(1));
Bandwidth limit = Bandwidth.classic(10, refill);
Bucket bucket = Bucket.builder()
.addLimit(limit)
.build();
for (int i = 1; i <= 10; i++) {
assertTrue(bucket.tryConsume(1));
}
assertFalse(bucket.tryConsume(1));
✶ 프로젝트 보안을 위해 상세 구현은 Baeldung에서 제공하는 예시 코드로 대체하도록 한다.
- Refill: 토큰 리필 방식을 정의한다.
- intervally(토큰 리필 개수, 토큰 리필 주기) 메서드로 버킷을 컨트롤한다.
- Bandwidth: 버킷이 허용하는 요청수와 리필 전략을 정의한다.
- Bucket: 실제 요청을 처리한다.
- addLimit: 처리율 제한을 정의한다.
- tryConsume: 요청에 따른 토큰 소비를 시도한다.
실제 적용하며 발생한 문제
개선 전
요청 IP를 HttpServletRequest의 getRemoteAddr()를 통해 받았다. 이 메서드를 사용하면 클라이언트의 공인 IP 주소를 받게되는데 그게 문제 발생의 원인이었다..!
왜냐하면 해당 서비스를 이용하는 학생은 주로 항공대 학생으로,
같은 IP에 접속할 가능성이 높기 때문이다.
따라서 다음과 같이 개선하였다.
개선 후
String userRandomId = RandomUtils.getRandomString(36);
이런식으로 요청이 들어오면 랜덤값을 생성하고, 이러한 randomId를 통해 버킷을 생성함으로써 해결하였다.
'Dev > Spring & JPA' 카테고리의 다른 글
[Spring] HTTP Request Client(webclient, feignclient) (10) | 2024.11.10 |
---|---|
[spring] null 처리를 위한 spring의 Stringutils (2) | 2023.12.21 |
[spring] @Async와 SimpleAsyncTaskExecutor, TaskExecutor 그리고 thread pool (0) | 2023.10.11 |
[Backend] 객체 지향 특징 | 다형성 | 좋은 객체 지향 설계 5 가지 원칙(SOLID) | EJB (0) | 2023.07.19 |