Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit b672a12

Browse files
committedApr 25, 2024
feat: 이동 윈도 로깅 알고리즘 생성, 조회 API 구현
1 parent 2bb9afd commit b672a12

File tree

7 files changed

+180
-1
lines changed

7 files changed

+180
-1
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.systemdesign.slidingwindowlog.controller;
2+
3+
import com.systemdesign.slidingwindowlog.dto.response.SlidingWindowLogProfileResponse;
4+
import com.systemdesign.slidingwindowlog.dto.response.SlidingWindowLogResponse;
5+
import com.systemdesign.slidingwindowlog.exception.RateLimitExceededException;
6+
import com.systemdesign.slidingwindowlog.service.SlidingWindowLogService;
7+
import lombok.RequiredArgsConstructor;
8+
import org.springframework.http.ResponseEntity;
9+
import org.springframework.web.bind.annotation.GetMapping;
10+
import org.springframework.web.bind.annotation.PostMapping;
11+
import org.springframework.web.bind.annotation.RequestMapping;
12+
import org.springframework.web.bind.annotation.RestController;
13+
import reactor.core.publisher.Flux;
14+
import reactor.core.publisher.Mono;
15+
16+
import static org.springframework.http.HttpStatus.*;
17+
18+
@RestController
19+
@RequiredArgsConstructor
20+
@RequestMapping("sliding-window-log")
21+
public class SlidingWindowLogController {
22+
23+
private final SlidingWindowLogService slidingWindowLogService;
24+
25+
@GetMapping
26+
public Mono<ResponseEntity<Flux<SlidingWindowLogResponse>>> findAllSlidingWindowLog() {
27+
return Mono.just(
28+
ResponseEntity.ok()
29+
.body(slidingWindowLogService.findAllSlidingWindowLog())
30+
);
31+
}
32+
33+
@PostMapping
34+
public Mono<ResponseEntity<SlidingWindowLogProfileResponse>> createSlidingWindowLog() {
35+
return slidingWindowLogService.createSlidingWindowLog()
36+
.map(response -> ResponseEntity.status(CREATED).body(response))
37+
.onErrorResume(RateLimitExceededException.class, e ->
38+
Mono.just(ResponseEntity.status(TOO_MANY_REQUESTS).build())
39+
);
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.systemdesign.slidingwindowlog.dto.response;
2+
3+
import lombok.Builder;
4+
import lombok.Getter;
5+
6+
import java.util.List;
7+
8+
@Getter
9+
@Builder
10+
public class SlidingWindowLogProfileResponse {
11+
12+
private List<SlidingWindowLogResponse> counters;
13+
14+
public static SlidingWindowLogProfileResponse from(List<SlidingWindowLogResponse> counters) {
15+
return SlidingWindowLogProfileResponse.builder()
16+
.counters(counters)
17+
.build();
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.systemdesign.slidingwindowlog.dto.response;
2+
3+
import lombok.Builder;
4+
import lombok.Getter;
5+
6+
@Getter
7+
@Builder
8+
public class SlidingWindowLogResponse {
9+
10+
private String key;
11+
private Long requestCount;
12+
13+
public static SlidingWindowLogResponse from(String key, Long requestCount) {
14+
return SlidingWindowLogResponse.builder()
15+
.key(key)
16+
.requestCount(requestCount)
17+
.build();
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.systemdesign.slidingwindowlog.exception;
2+
3+
import com.systemdesign.slidingwindowlog.common.exception.ExceptionCode;
4+
import lombok.Getter;
5+
import lombok.RequiredArgsConstructor;
6+
import org.springframework.http.HttpStatus;
7+
8+
import static org.springframework.http.HttpStatus.TOO_MANY_REQUESTS;
9+
10+
@Getter
11+
@RequiredArgsConstructor
12+
public enum RateExceptionCode implements ExceptionCode {
13+
14+
COMMON_TOO_MANY_REQUESTS(TOO_MANY_REQUESTS, "RAT-001", "사용자 요청 횟수 초과"),
15+
;
16+
17+
private final HttpStatus status;
18+
private final String code;
19+
private final String message;
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.systemdesign.slidingwindowlog.exception;
2+
3+
import com.systemdesign.slidingwindowlog.common.exception.BusinessException;
4+
import com.systemdesign.slidingwindowlog.common.exception.ExceptionCode;
5+
6+
public class RateLimitExceededException extends BusinessException {
7+
8+
public RateLimitExceededException(ExceptionCode exceptionCode, Object... rejectedValues) {
9+
super(exceptionCode, rejectedValues);
10+
}
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package com.systemdesign.slidingwindowlog.service;
2+
3+
import com.systemdesign.slidingwindowlog.dto.response.SlidingWindowLogProfileResponse;
4+
import com.systemdesign.slidingwindowlog.dto.response.SlidingWindowLogResponse;
5+
import com.systemdesign.slidingwindowlog.exception.RateExceptionCode;
6+
import com.systemdesign.slidingwindowlog.exception.RateLimitExceededException;
7+
import lombok.RequiredArgsConstructor;
8+
import lombok.extern.slf4j.Slf4j;
9+
import org.springframework.data.domain.Range;
10+
import org.springframework.data.redis.core.ReactiveRedisTemplate;
11+
import org.springframework.stereotype.Service;
12+
import reactor.core.publisher.Flux;
13+
import reactor.core.publisher.Mono;
14+
15+
import java.util.List;
16+
17+
@Slf4j
18+
@Service
19+
@RequiredArgsConstructor
20+
public class SlidingWindowLogService {
21+
22+
private final ReactiveRedisTemplate<String, Object> redisTemplate;
23+
private final static String SLIDING_WINDOW_KEY = "SlidingWindow:"; // 키
24+
private final static long SLIDING_WINDOW_MAX_REQUEST = 1000; // 최대 요청 허용 수
25+
private final static long SLIDING_WINDOW_DURATION = 60; // 60초
26+
27+
public Mono<SlidingWindowLogProfileResponse> createSlidingWindowLog() {
28+
long currentTimestamp = System.currentTimeMillis();
29+
String redisKey = generateRedisKey("requests");
30+
log.info("Sliding Window Counter created. key: {}", redisKey);
31+
32+
return redisTemplate.opsForZSet()
33+
.add(redisKey, String.valueOf(currentTimestamp), currentTimestamp)
34+
.flatMap(added -> redisTemplate.opsForZSet().removeRangeByScore(redisKey, Range.closed(0D,
35+
calculateTimeRange())))
36+
.flatMap(removed -> redisTemplate.opsForZSet().count(redisKey,
37+
Range.closed(calculateTimeRange(), (double) currentTimestamp)))
38+
.flatMap(count -> {
39+
log.info("Sliding Window Counter count: {}", count);
40+
41+
if (count >= SLIDING_WINDOW_MAX_REQUEST) {
42+
log.error("Rate limit exceeded. key: {}", redisKey);
43+
return Mono.error(new RateLimitExceededException(RateExceptionCode.COMMON_TOO_MANY_REQUESTS, count));
44+
}
45+
return Mono.just(SlidingWindowLogProfileResponse.from(List.of(SlidingWindowLogResponse.from(redisKey, count + 1))));
46+
});
47+
}
48+
49+
public Flux<SlidingWindowLogResponse> findAllSlidingWindowLog() {
50+
String redisKey = generateRedisKey("requests");
51+
long currentTimestamp = System.currentTimeMillis();
52+
log.info("Sliding Window Counter find all. key: {}", redisKey);
53+
54+
return redisTemplate.opsForZSet().rangeByScore(redisKey,
55+
Range.closed(calculateTimeRange(), (double) currentTimestamp))
56+
.map(value -> {
57+
log.info("Sliding Window Counter value: {}", value);
58+
return SlidingWindowLogResponse.from(redisKey, Long.parseLong((String) value));
59+
});
60+
}
61+
62+
private double calculateTimeRange() {
63+
long currentTimestamp = System.currentTimeMillis();
64+
return currentTimestamp - SLIDING_WINDOW_DURATION * 1000;
65+
}
66+
67+
private String generateRedisKey(String requestType) {
68+
return SLIDING_WINDOW_KEY + requestType;
69+
}
70+
}

‎src/main/resources/application.properties

-1
This file was deleted.

0 commit comments

Comments
 (0)
Please sign in to comment.