-
Notifications
You must be signed in to change notification settings - Fork 248
[π λ°©νμΆ μμ½ μΈλΆ API μ°λ 3λ¨κ³] ν°λ΄ λ―Έμ μ μΆν©λλ€. #699
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+628
β1
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
36dc28a
feat: ν ν° λ²ν· κΈ°λ° Rate Limiterλ₯Ό μ§μ ꡬν
JohnPrk 325181b
feat: κ²°μ Β·μμ½ μλν¬μΈνΈμ ν ν° λ²ν· Rate Limitμ μ μ©ν΄ μ΄κ³Ό μμ²μ κ±°λΆ
JohnPrk 64f3e1b
feat: ν μ€μ 429 μλ΅μ Retry-After κΈ°λ°μΌλ‘ λ°±μ€ν μ¬μλ
JohnPrk efb7105
feat: ν μ€λ‘ λκ°λ νΈμΆμ ν ν° λ²ν· Rate Limitμ μ μ©ν΄ νλ μ΄κ³Ό μ κ±°λΆ
JohnPrk 56bb6d3
test: ν ν° λ²ν· λμμ± κ²μ¦μ κ°μ μ€λ λ νλ‘ λ³κ²½
JohnPrk File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
15 changes: 15 additions & 0 deletions
15
src/main/java/roomescape/exception/OutboundRateLimitException.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package roomescape.exception; | ||
|
|
||
| public class OutboundRateLimitException extends RoomescapeException { | ||
|
|
||
| private final long retryAfterSeconds; | ||
|
|
||
| public OutboundRateLimitException(long retryAfterSeconds) { | ||
| super("μΈλΆ κ²°μ API νΈμΆ νλλ₯Ό μ΄κ³Όνμ΅λλ€. retryAfterSeconds=" + retryAfterSeconds); | ||
| this.retryAfterSeconds = retryAfterSeconds; | ||
| } | ||
|
|
||
| public long retryAfterSeconds() { | ||
| return retryAfterSeconds; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
28 changes: 28 additions & 0 deletions
28
src/main/java/roomescape/payment/toss/OutboundRateLimitInterceptor.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| package roomescape.payment.toss; | ||
|
|
||
| import org.springframework.http.HttpRequest; | ||
| import org.springframework.http.client.ClientHttpRequestExecution; | ||
| import org.springframework.http.client.ClientHttpRequestInterceptor; | ||
| import org.springframework.http.client.ClientHttpResponse; | ||
| import roomescape.exception.OutboundRateLimitException; | ||
| import roomescape.ratelimit.TokenBucketRateLimiter; | ||
|
|
||
| import java.io.IOException; | ||
|
|
||
| public class OutboundRateLimitInterceptor implements ClientHttpRequestInterceptor { | ||
|
|
||
| private final TokenBucketRateLimiter rateLimiter; | ||
|
|
||
| public OutboundRateLimitInterceptor(TokenBucketRateLimiter outboundRateLimiter) { | ||
| this.rateLimiter = outboundRateLimiter; | ||
| } | ||
|
|
||
| @Override | ||
| public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) | ||
| throws IOException { | ||
| if (!rateLimiter.tryConsume()) { | ||
| throw new OutboundRateLimitException(rateLimiter.retryAfterSeconds()); | ||
| } | ||
| return execution.execute(request, body); | ||
| } | ||
| } |
58 changes: 58 additions & 0 deletions
58
src/main/java/roomescape/payment/toss/RetryAfterInterceptor.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| package roomescape.payment.toss; | ||
|
|
||
| import org.springframework.http.HttpHeaders; | ||
| import org.springframework.http.HttpRequest; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.http.client.ClientHttpRequestExecution; | ||
| import org.springframework.http.client.ClientHttpRequestInterceptor; | ||
| import org.springframework.http.client.ClientHttpResponse; | ||
|
|
||
| import java.io.IOException; | ||
| import java.time.Duration; | ||
| import java.util.Optional; | ||
|
|
||
| public class RetryAfterInterceptor implements ClientHttpRequestInterceptor { | ||
|
|
||
| private final int maxAttempts; | ||
| private final Duration defaultBackoff; | ||
| private final Sleeper sleeper; | ||
|
|
||
| public RetryAfterInterceptor(int maxAttempts, Duration defaultBackoff, Sleeper sleeper) { | ||
| if (maxAttempts < 1) { | ||
| throw new IllegalArgumentException("maxAttemptsλ 1 μ΄μμ΄μ΄μΌ ν©λλ€. maxAttempts=" + maxAttempts); | ||
| } | ||
| this.maxAttempts = maxAttempts; | ||
| this.defaultBackoff = defaultBackoff; | ||
| this.sleeper = sleeper; | ||
| } | ||
|
|
||
| @Override | ||
| public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) | ||
| throws IOException { | ||
| for (int attempt = 1; ; attempt++) { | ||
| ClientHttpResponse response = execution.execute(request, body); | ||
| if (response.getStatusCode().value() != HttpStatus.TOO_MANY_REQUESTS.value()) { | ||
| return response; | ||
| } | ||
| if (attempt >= maxAttempts) { | ||
| response.close(); | ||
| throw new TossRateLimitException("ν μ€ νΈμΆμ΄ μμ² νλ(429)λ₯Ό μ΄κ³Όνμ΅λλ€. attempts=" + attempt); | ||
| } | ||
| Duration backoff = retryAfter(response).orElse(defaultBackoff); | ||
| response.close(); | ||
| sleeper.sleep(backoff); | ||
| } | ||
| } | ||
|
|
||
| private Optional<Duration> retryAfter(ClientHttpResponse response) throws IOException { | ||
| String value = response.getHeaders().getFirst(HttpHeaders.RETRY_AFTER); | ||
| if (value == null || value.isBlank()) { | ||
| return Optional.empty(); | ||
| } | ||
| try { | ||
| return Optional.of(Duration.ofSeconds(Long.parseLong(value.trim()))); | ||
| } catch (NumberFormatException e) { | ||
| return Optional.empty(); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package roomescape.payment.toss; | ||
|
|
||
| import java.time.Duration; | ||
|
|
||
| @FunctionalInterface | ||
| public interface Sleeper { | ||
|
|
||
| void sleep(Duration duration); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package roomescape.payment.toss; | ||
|
|
||
| import java.time.Duration; | ||
|
|
||
| public class ThreadSleeper implements Sleeper { | ||
|
|
||
| @Override | ||
| public void sleep(Duration duration) { | ||
| long millis = duration.toMillis(); | ||
| if (millis <= 0) { | ||
| return; | ||
| } | ||
| try { | ||
| Thread.sleep(millis); | ||
| } catch (InterruptedException e) { | ||
| Thread.currentThread().interrupt(); | ||
| throw new IllegalStateException("μ¬μλ λκΈ° μ€ μΈν°λ½νΈλμμ΅λλ€.", e); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,13 @@ | ||
| package roomescape.payment.toss; | ||
|
|
||
| import org.springframework.beans.factory.annotation.Qualifier; | ||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.http.HttpHeaders; | ||
| import org.springframework.http.client.SimpleClientHttpRequestFactory; | ||
| import org.springframework.web.client.RestClient; | ||
| import roomescape.ratelimit.TokenBucketRateLimiter; | ||
|
|
||
| import java.nio.charset.StandardCharsets; | ||
| import java.time.Duration; | ||
|
|
@@ -14,12 +16,35 @@ | |
| @Configuration | ||
| public class TossClientConfig { | ||
|
|
||
| @Bean | ||
| public Sleeper tossBackoffSleeper() { | ||
| return new ThreadSleeper(); | ||
| } | ||
|
|
||
| @Bean | ||
| public RetryAfterInterceptor tossRetryAfterInterceptor( | ||
| @Value("${toss.retry.max-attempts}") int maxAttempts, | ||
| @Value("${toss.retry.default-backoff}") Duration defaultBackoff, | ||
| Sleeper tossBackoffSleeper | ||
| ) { | ||
| return new RetryAfterInterceptor(maxAttempts, defaultBackoff, tossBackoffSleeper); | ||
| } | ||
|
|
||
| @Bean | ||
| public OutboundRateLimitInterceptor tossOutboundRateLimitInterceptor( | ||
| @Qualifier("outboundRateLimiter") TokenBucketRateLimiter outboundRateLimiter | ||
| ) { | ||
| return new OutboundRateLimitInterceptor(outboundRateLimiter); | ||
| } | ||
|
|
||
| @Bean | ||
| public RestClient tossRestClient( | ||
| @Value("${toss.base-url}") String baseUrl, | ||
| @Value("${toss.secret-key}") String secretKey, | ||
| @Value("${toss.connect-timeout}") Duration connectTimeout, | ||
| @Value("${toss.read-timeout}") Duration readTimeout | ||
| @Value("${toss.read-timeout}") Duration readTimeout, | ||
| RetryAfterInterceptor tossRetryAfterInterceptor, | ||
| OutboundRateLimitInterceptor tossOutboundRateLimitInterceptor | ||
| ) { | ||
| String basic = Base64.getEncoder() | ||
| .encodeToString((secretKey + ":").getBytes(StandardCharsets.UTF_8)); | ||
|
|
@@ -32,6 +57,8 @@ public RestClient tossRestClient( | |
| .baseUrl(baseUrl) | ||
| .requestFactory(requestFactory) | ||
| .defaultHeader(HttpHeaders.AUTHORIZATION, "Basic " + basic) | ||
| .requestInterceptor(tossRetryAfterInterceptor) | ||
| .requestInterceptor(tossOutboundRateLimitInterceptor) | ||
|
Comment on lines
+60
to
+61
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. νμ¬ μΈν°μ ν°λ₯Ό Retry -> Outbound μμΌλ‘ λ±λ‘νμ μ μ λν΄μ μμμ λν μ ν μ§μ μ΄ μμΌμ ¨λμ§ κΆκΈν©λλ€! |
||
| .build(); | ||
| } | ||
| } | ||
8 changes: 8 additions & 0 deletions
8
src/main/java/roomescape/payment/toss/TossRateLimitException.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package roomescape.payment.toss; | ||
|
|
||
| public class TossRateLimitException extends TossPaymentException { | ||
|
|
||
| public TossRateLimitException(String message) { | ||
| super(message); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| package roomescape.ratelimit; | ||
|
|
||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
|
|
||
| @Configuration | ||
| public class RateLimitConfig { | ||
|
|
||
| @Bean | ||
| public TokenBucketRateLimiter inboundRateLimiter( | ||
| @Value("${rate-limit.capacity}") long capacity, | ||
| @Value("${rate-limit.refill-per-sec}") double refillPerSec | ||
| ) { | ||
| return new TokenBucketRateLimiter(capacity, refillPerSec, System::nanoTime); | ||
| } | ||
|
|
||
| @Bean | ||
| public TokenBucketRateLimiter outboundRateLimiter( | ||
| @Value("${outbound-rate-limit.capacity}") long capacity, | ||
| @Value("${outbound-rate-limit.refill-per-sec}") double refillPerSec | ||
| ) { | ||
| return new TokenBucketRateLimiter(capacity, refillPerSec, System::nanoTime); | ||
| } | ||
| } |
29 changes: 29 additions & 0 deletions
29
src/main/java/roomescape/ratelimit/RateLimitInterceptor.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| package roomescape.ratelimit; | ||
|
|
||
| import jakarta.servlet.http.HttpServletRequest; | ||
| import jakarta.servlet.http.HttpServletResponse; | ||
| import org.springframework.beans.factory.annotation.Qualifier; | ||
| import org.springframework.http.HttpHeaders; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.web.servlet.HandlerInterceptor; | ||
|
|
||
| @Component | ||
| public class RateLimitInterceptor implements HandlerInterceptor { | ||
|
|
||
| private final TokenBucketRateLimiter rateLimiter; | ||
|
|
||
| public RateLimitInterceptor(@Qualifier("inboundRateLimiter") TokenBucketRateLimiter inboundRateLimiter) { | ||
| this.rateLimiter = inboundRateLimiter; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { | ||
| if (rateLimiter.tryConsume()) { | ||
| return true; | ||
| } | ||
| response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); | ||
| response.setHeader(HttpHeaders.RETRY_AFTER, String.valueOf(rateLimiter.retryAfterSeconds())); | ||
| return false; | ||
| } | ||
| } |
58 changes: 58 additions & 0 deletions
58
src/main/java/roomescape/ratelimit/TokenBucketRateLimiter.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| package roomescape.ratelimit; | ||
|
|
||
| import java.util.function.LongSupplier; | ||
|
|
||
| public class TokenBucketRateLimiter { | ||
|
|
||
| private static final double NANOS_PER_SECOND = 1_000_000_000.0; | ||
|
|
||
| private final long capacity; | ||
| private final double refillPerSec; | ||
| private final LongSupplier nanoTimeSupplier; | ||
|
|
||
| private double availableTokens; | ||
| private long lastRefillNanos; | ||
|
|
||
| public TokenBucketRateLimiter(long capacity, double refillPerSec, LongSupplier nanoTimeSupplier) { | ||
| if (capacity < 1) { | ||
| throw new IllegalArgumentException("capacityλ 1 μ΄μμ΄μ΄μΌ ν©λλ€. capacity=" + capacity); | ||
| } | ||
| if (refillPerSec <= 0) { | ||
| throw new IllegalArgumentException("refillPerSecλ 0λ³΄λ€ μ»€μΌ ν©λλ€. refillPerSec=" + refillPerSec); | ||
| } | ||
| this.capacity = capacity; | ||
| this.refillPerSec = refillPerSec; | ||
| this.nanoTimeSupplier = nanoTimeSupplier; | ||
| this.availableTokens = capacity; | ||
| this.lastRefillNanos = nanoTimeSupplier.getAsLong(); | ||
| } | ||
|
|
||
| public synchronized boolean tryConsume() { | ||
| refill(); | ||
| if (availableTokens >= 1.0) { | ||
| availableTokens -= 1.0; | ||
| return true; | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| public synchronized long retryAfterSeconds() { | ||
| refill(); | ||
| if (availableTokens >= 1.0) { | ||
| return 0L; | ||
| } | ||
| double shortage = 1.0 - availableTokens; | ||
| return (long) Math.ceil(shortage / refillPerSec); | ||
| } | ||
|
|
||
| private void refill() { | ||
| long now = nanoTimeSupplier.getAsLong(); | ||
| long elapsedNanos = now - lastRefillNanos; | ||
| if (elapsedNanos <= 0) { | ||
| return; | ||
| } | ||
| double refilled = (elapsedNanos / NANOS_PER_SECOND) * refillPerSec; | ||
| availableTokens = Math.min(capacity, availableTokens + refilled); | ||
| lastRefillNanos = now; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
μμ½ μλν¬μΈνΈμ νλλ₯Ό μ μ©νμ€ λ μ ν κ²½λ‘λ₯Ό μ¬μ©νμ κ² κ°μ΅λλ€.
νΉμ μ νν '/reservation'μμ½κ³Ό κ΄λ ¨ν κ²½λ‘λ§ μ²λ¦¬νμλ €λ μλμ ¨λμ§ κΆκΈν©λλ€