Skip to content

[4주차] Ratelimit 구현 및 Jacoco 적용#98

Open
sina-log wants to merge 25 commits intohanghae-skillup:reversalSpringfrom
sina-log:develop
Open

[4주차] Ratelimit 구현 및 Jacoco 적용#98
sina-log wants to merge 25 commits intohanghae-skillup:reversalSpringfrom
sina-log:develop

Conversation

@sina-log
Copy link

@sina-log sina-log commented Feb 4, 2025

제목(title)

Rate Limit Implementation


작업 내용

  • Rate Limit 적용
  • jacoco test 적용

발생했던 문제와 해결 과정을 남겨 주세요.

이번 주차에서 고민되었던 지점이나, 어려웠던 점을 알려 주세요.

  • jacoco 버전이 충돌하여 해결하는데 시간이 많이 걸림
  • 커버리지 올리는데 쉽지 않았음
  • 테스트 하는데 모킹하시 쉽지 않음

리뷰 포인트

기타 질문

…이동, RateLimitInterceptor를 API 모듈로 이동, 불필요한 테스트 파일 제거 및 테스트 코드 개선
@sina-log sina-log changed the title [ 4주차 ] Ratelimit 구현 및 테스트 케이스 작성 [4주차] Ratelimit 구현 및 jacoco 적용 Feb 4, 2025
@sina-log sina-log changed the title [4주차] Ratelimit 구현 및 jacoco 적용 [4주차] Ratelimit 구현 및 Jacoco 적용 Feb 4, 2025
Copy link

@youngxpepp youngxpepp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안녕하세요 준형님! 이건홍 코치입니다.

좋았던 점

  • guava의 Cache와 RateLimiter를 적재적소에 사용해서 rate limit을 잘 구현해주셨습니다.
  • 테스트 커버리지 훌륭하네요!
    제가 테스트에 대해 많은 인사이트를 얻었던 블로그 글들을 첨부할게요. 바로가기

아쉬운 점

  • 동시성을 조금만 더 신경 써주셨으면 좋겠어요!

Comment on lines +9 to +20
@Configuration
@RequiredArgsConstructor
public class RateLimitConfig implements WebMvcConfigurer {

private final RateLimitInterceptor rateLimitInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(rateLimitInterceptor)
.addPathPatterns("/api/v1/**"); // 모든 API에 적용
}
} No newline at end of file

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WebConfig가 있어서 중복 되네요!

Comment on lines +29 to +41
// 조회 API에 대한 IP 기반 rate limit 체크
if (isQueryRequest(request)) {
rateLimitService.checkIpRateLimit(clientIp);
}

// 예약 API에 대한 사용자 기반 rate limit 체크
if (isReservationRequest(request)) {
String scheduleTime = request.getParameter("scheduleTime");
Long userId = getUserIdFromRequest(request);
if (userId != null && scheduleTime != null) {
rateLimitService.checkUserReservationRateLimit(userId, scheduleTime);
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

인터셉터를 두개로 분리하면 어때요?
WebMvcConfigurer에 있는 url도 분리해서 각각 다른 인터셉터를 적용하면 로직이 더 간단해질거 같아요!

Comment on lines +54 to +72
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
private String getClientIp(HttpServletRequest request) {
return Stream.of(
"X-Forwarded-For",
"Proxy-Client-IP",
"WL-Proxy-Client-IP",
"HTTP_CLIENT_IP",
"HTTP_X_FORWARDED_FOR"
)
.map(request::getHeader)
.filter(this::isValidIp)
.findFirst()
.orElse(request.getRemoteAddr());
}
private boolean isValidIp(String ip) {
return ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip);
}

중복되는 코드들이 있어서 리팩토링 해봤어요!

Comment on lines +16 to +33
private final Cache<String, RateLimiter> rateLimiters;
private final Cache<String, Integer> requestCounts;
private final Cache<String, RateLimiter> reservationRateLimiters;
private final int maxRequestsPerMinute = 50;

public GuavaRateLimitService() {
this.rateLimiters = CacheBuilder.newBuilder()
.expireAfterWrite(1, TimeUnit.HOURS)
.build();

this.requestCounts = CacheBuilder.newBuilder()
.expireAfterWrite(1, TimeUnit.MINUTES)
.build();

this.reservationRateLimiters = CacheBuilder.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

guava의 Cache와 RateLimiter를 적절하게 잘 사용해주셨네요.
60분 이후에 캐시에 등록된 RateLimiter가 자연스레 만료되면서 초기화 되는군요!

}

if (!limiter.tryAcquire()) {
Integer newCount = count == null ? 1 : count + 1;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

동시에 사용자 요청이 들어온다면 count가 적절하게 1씩 증가할까요?

Comment on lines +42 to +46
RateLimiter limiter = rateLimiters.getIfPresent(ip);
if (limiter == null) {
limiter = RateLimiter.create(maxRequestsPerMinute / 60.0); // 초당 요청 수로 변환
rateLimiters.put(ip, limiter);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그냥 get을 사용하는건 어때요? 동시성 때문에 너도나도 새로운 RateLimiter를 만들 수도 있어요.

Suggested change
RateLimiter limiter = rateLimiters.getIfPresent(ip);
if (limiter == null) {
limiter = RateLimiter.create(maxRequestsPerMinute / 60.0); // 초당 요청 수로 변환
rateLimiters.put(ip, limiter);
}
RateLimiter limiter = rateLimiters.get(ip, () -> RateLimiter.create(maxRequestsPerMinute / 60.0));

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants