728x90
1.어노테이션 만들기
컨트롤러에서 요청 제한 로직을 직접 작성하는것은 코드가 너무 중복되고 비효율적이라 생각하기때문에 어노테이션으로 만들어서 사용하기로 하였다. 다음은 메서드에 적용하여 런타임동안 유지되고 파라미터로 value와 timeWindow를 받게 하였다.
@Target(ElementType.METHOD) // 메서드에만 적용
@Retention(RetentionPolicy.RUNTIME) // 런타임까지 유지
public @interface RateLimit {
long value() default 5; // 초당 허용 요청 수
long timeWindow() default 1; // 제한 시간 (초)
}
다음과같은 형태로 어노테이션을 사용할수있다.
@RateLimit(value = 5, timeWindow = 1)
1-1.@Target()
어노테이션을 어디에 적용할 수 있는지 지정하는 어노테이션이다.
- ElementType.FIELD → 필드(멤버 변수)에 적용
- ElementType.TYPE → 클래스, 인터페이스, enum 등에 적용
- ElementType.PARAMETER → 메서드 매개변수에 적용
1-2.@Retention()
어노테이션을 언제까지 유지할 것인지 지정하는 어노테이션이다.
- RUNTIME → 실행 중에도 유지
- SOURCE → 컴파일할 때만 유지
- CLASS → 바이트코드까지 유지되지만 실행 중에는 접근 불가
1-3.public @interface RateLimit
@RateLimit 형태로 사용 가능하게 만든다.
2.RateLimiterService
서비스로는 Redis를 통해 초당 요청하는 횟수를 증가하고 검증하는 로직을 포함한 메소드를 만든다.
IP를 기준으로 엔드포인트마다 체크한다.
엔드포인트마다 최초에 timeWindow 만큼 만료기간을주고 Redis에 이 값이 만료되기전에 requestLimit를 넘으면 false를 반환해준다.
@Service
@RequiredArgsConstructor
public class RateLimiterService {
private final RedisTemplate<String, Object> redisTemplate;
public boolean isAllowed(String clientIp, String endpoint, long requestLimit, long timeWindow) {
String key = "rate_limit:" + clientIp + ":" + endpoint;
//요청 횟수 증가
Long currentCount = redisTemplate.opsForValue().increment(key, 1);
if (currentCount == 1) {
// 최초 요청 시 TTL 설정
redisTemplate.expire(key, timeWindow, TimeUnit.SECONDS);
}
return currentCount <= requestLimit;
}
}
3.AOP(Aspect-Oriented Programming)
3-1.AOP(Aspect-Oriented Programming)
프로그램의 주요 비즈니스 로직과는 독립적으로 공통적인 관심사를 모듈화하여 코드의 중복을 줄이고, 유지보수를 쉽게하기 위한 방법론 , 공통 관심사를 메인 로직과 분리하여, 코드 중복을 줄이고 관리가 용이하도록한다.
3-2.Aspect , @Aspect
AOP의 Aspect를 정의하는 클래스에 적용된다.
(Aspect는 특정 비즈니스 로직에 대해 공통 관심사를 처리하는 모듈을 의미)
3-3.Advice
AOP에서 공통 관심사를 실행하는 구체적인 동작을 의미한다.
- @Before: 메서드 실행 전에 동작하는 Advice
- @After: 메서드 실행 후에 동작하는 Advice
- @Around: 메서드 실행 전후에 동작하는 Advice
- @Pointcut: 어떤 메서드에 Advice를 적용할지를 지정
3-4.Join Point
실제 코드의 실행 흐름에서 AOP가 적용되는 지점을 나타낸다.
4.RateLimitAspect
AOP를 통해 API 요청 Rate Limiting 기능을 구현한 클래스다.
@Around를 통해 @RateLimit 어노테이션에 지정된 값을 받을 수있게 하였다.
@Aspect
@Component
@RequiredArgsConstructor
public class RateLimitAspect {
private final RateLimiterService rateLimiterService;
@Around("@annotation(rateLimit)") // @RateLimit이 적용된 메서드만 적용
public Object checkRateLimit(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
throw new IllegalStateException("No request context available");
}
HttpServletRequest request = attributes.getRequest();
String clientIp = request.getRemoteAddr();
String endpoint = request.getRequestURI();
// 요청 제한 확인
if (!rateLimiterService.isAllowed(clientIp, endpoint, rateLimit.value(), rateLimit.timeWindow())) {
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("Too many requests");
}
return joinPoint.proceed();
}
}
joinPoint는 실제 메서드 호출을 진행할 수 있게 해주는 객체고 , rateLimit 매개변수는 @RateLimit 어노테이션의 값들이다.
public Object checkRateLimit(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
RequestContextHolder를 사용하여 현재 요청의 HttpServletRequest 객체를 얻는다.
request.getRemoteAddr()로 클라이언트의 IP 주소를 가져오고,
request.getRequestURI()로 요청한 URI를 가져온다.
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
throw new IllegalStateException("No request context available");
}
HttpServletRequest request = attributes.getRequest();
String clientIp = request.getRemoteAddr();
String endpoint = request.getRequestURI();
서비스에서 만든 로직을 통해 Rate limit을 초과하지 않았다면, joinPoint.proceed()를 호출하여 원래의 메서드를 실행한다.
// 요청 제한 확인
if (!rateLimiterService.isAllowed(clientIp, endpoint, rateLimit.value(), rateLimit.timeWindow())) {
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("Too many requests");
}
return joinPoint.proceed();
5.RedisConfig
Spring에서 Redis를 사용할 때, Redis 연결 및 데이터를 직렬화하는 방법 등을 설정하는 config 파일을 생성한다.
키는 StringRedisSerializer를 사용하여 문자열로 직렬화하고, 값은 GenericJackson2JsonRedisSerializer를 사용하여 JSON 형식으로 직렬화하여 Redis에 저장한다.
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
}
6.RateLimit 어노테이션 테스트하기
우선 테스트로 시간을 길게해서 5회 제한을 둬서 테스트해본다.
//상품 상세조회
@GetMapping("/{productId}")
@RateLimit(value = 5, timeWindow = 1000000)
public ResponseEntity<ProductResponseDTO> getProductItemDetail(@PathVariable Long productId){
ProductResponseDTO responseDTO = productService.getProductDetail(productId);
return ResponseEntity.status(HttpStatus.OK).body(responseDTO);
}
5회이상 api를 요청했더니 다음처럼 정상적으로 Too many requests 라고 응답을 받았다.
728x90
'BackEnd > SpringBoot' 카테고리의 다른 글
[SpringBoot] redis로 실시간 인기 제품 (0) | 2025.03.13 |
---|---|
[SpringBoot] Redis 로 Spring Cache 추가 (0) | 2025.03.11 |
[SpringBoot] Redis 추가, Redis로 JWT 블랙리스트 구현 (0) | 2025.03.10 |