Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package com.loopers.config;

import io.github.resilience4j.retry.RetryConfig;
import io.github.resilience4j.retry.RetryRegistry;
import io.github.resilience4j.core.IntervalFunction;
import lombok.extern.slf4j.Slf4j;
import feign.FeignException;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.Duration;
import java.util.concurrent.TimeoutException;
import java.net.SocketTimeoutException;

/**
* Resilience4j Retry ์„ค์ • ์ปค์Šคํ„ฐ๋งˆ์ด์ง•.
* <p>
* ์‹ค๋ฌด ๊ถŒ์žฅ ํŒจํ„ด์— ๋”ฐ๋ผ ๋ฉ”์„œ๋“œ๋ณ„๋กœ ๋‹ค๋ฅธ Retry ์ •์ฑ…์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค:
* </p>
* <p>
* <b>Retry ์ •์ฑ…:</b>
* <ul>
* <li><b>๊ฒฐ์ œ ์š”์ฒญ API (requestPayment):</b> Retry ์—†์Œ (์œ ์ € ์š”์ฒญ ๊ฒฝ๋กœ - ๋น ๋ฅธ ์‹คํŒจ)</li>
* <li><b>์กฐํšŒ API (getTransactionsByOrder, getTransaction):</b> Exponential Backoff ์ ์šฉ (์Šค์ผ€์ค„๋Ÿฌ - ์•ˆ์ „)</li>
* </ul>
* </p>
* <p>
* <b>Exponential Backoff ์ „๋žต (์กฐํšŒ API์šฉ):</b>
* <ul>
* <li><b>์ดˆ๊ธฐ ๋Œ€๊ธฐ ์‹œ๊ฐ„:</b> 500ms</li>
* <li><b>๋ฐฐ์ˆ˜(multiplier):</b> 2 (๊ฐ ์žฌ์‹œ๋„๋งˆ๋‹ค 2๋ฐฐ์”ฉ ์ฆ๊ฐ€)</li>
* <li><b>์ตœ๋Œ€ ๋Œ€๊ธฐ ์‹œ๊ฐ„:</b> 5์ดˆ (๋„ˆ๋ฌด ๊ธธ์–ด์ง€์ง€ ์•Š๋„๋ก ์ œํ•œ)</li>
* <li><b>๋žœ๋ค jitter:</b> ํ™œ์„ฑํ™” (thundering herd ๋ฌธ์ œ ๋ฐฉ์ง€)</li>
* </ul>
* </p>
* <p>
* <b>์žฌ์‹œ๋„ ์‹œํ€€์Šค ์˜ˆ์‹œ (์กฐํšŒ API):</b>
* <ol>
* <li>1์ฐจ ์‹œ๋„: ์ฆ‰์‹œ ์‹คํ–‰</li>
* <li>2์ฐจ ์‹œ๋„: 500ms ํ›„ (500ms * 2^0)</li>
* <li>3์ฐจ ์‹œ๋„: 1000ms ํ›„ (500ms * 2^1)</li>
* </ol>
* </p>
* <p>
* <b>์„ค๊ณ„ ๊ทผ๊ฑฐ:</b>
* <ul>
* <li><b>์œ ์ € ์š”์ฒญ ๊ฒฝ๋กœ:</b> ๊ธด Retry๋Š” ์Šค๋ ˆ๋“œ ์ ์œ  ๋น„์šฉ์ด ํฌ๋ฏ€๋กœ Retry ์—†์ด ๋น ๋ฅด๊ฒŒ ์‹คํŒจ</li>
* <li><b>์Šค์ผ€์ค„๋Ÿฌ ๊ฒฝ๋กœ:</b> ๋น„๋™๊ธฐ/๋ฐฐ์น˜ ๊ธฐ๋ฐ˜์ด๋ฏ€๋กœ Retry๊ฐ€ ์•ˆ์ „ํ•˜๊ฒŒ ์ ์šฉ ๊ฐ€๋Šฅ (Nice-to-Have ์š”๊ตฌ์‚ฌํ•ญ ์ถฉ์กฑ)</li>
* <li><b>Eventually Consistent:</b> ์‹คํŒจ ์‹œ ์ฃผ๋ฌธ์€ PENDING ์ƒํƒœ๋กœ ์œ ์ง€๋˜์–ด ์Šค์ผ€์ค„๋Ÿฌ์—์„œ ๋ณต๊ตฌ</li>
* <li><b>์ผ์‹œ์  ์˜ค๋ฅ˜ ๋ณต๊ตฌ:</b> ๋„คํŠธ์›Œํฌ ์ผ์‹œ์  ์˜ค๋ฅ˜๋‚˜ PG ์„œ๋ฒ„ ์ผ์‹œ์  ์žฅ์•  ์‹œ ์ž๋™ ๋ณต๊ตฌ</li>
* </ul>
* </p>
*
* @author Loopers
* @version 2.0
*/
@Slf4j
@Configuration
public class Resilience4jRetryConfig {

/**
* PaymentGatewayClient์šฉ Retry ์„ค์ •์„ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•ฉ๋‹ˆ๋‹ค.
* <p>
* Exponential Backoff ์ „๋žต์„ ์ ์šฉํ•˜์—ฌ ์žฌ์‹œ๋„ ๊ฐ„๊ฒฉ์„ ์ ์ง„์ ์œผ๋กœ ์ฆ๊ฐ€์‹œํ‚ต๋‹ˆ๋‹ค.
* </p>
*
* @return RetryRegistry (์ปค์Šคํ„ฐ๋งˆ์ด์ง•๋œ ์„ค์ •์ด ์ ์šฉ๋จ)
*/
@Bean
public RetryRegistry retryRegistry() {
RetryRegistry retryRegistry = io.github.resilience4j.retry.RetryRegistry.ofDefaults();
// Exponential Backoff ์„ค์ •
// - ์ดˆ๊ธฐ ๋Œ€๊ธฐ ์‹œ๊ฐ„: 500ms
// - ๋ฐฐ์ˆ˜: 2 (๊ฐ ์žฌ์‹œ๋„๋งˆ๋‹ค 2๋ฐฐ์”ฉ ์ฆ๊ฐ€)
// - ์ตœ๋Œ€ ๋Œ€๊ธฐ ์‹œ๊ฐ„: 5์ดˆ
// - ๋žœ๋ค jitter: ํ™œ์„ฑํ™” (thundering herd ๋ฌธ์ œ ๋ฐฉ์ง€)
IntervalFunction intervalFunction = IntervalFunction
.ofExponentialRandomBackoff(
Duration.ofMillis(500), // ์ดˆ๊ธฐ ๋Œ€๊ธฐ ์‹œ๊ฐ„
2.0, // ๋ฐฐ์ˆ˜ (exponential multiplier)
Duration.ofSeconds(5) // ์ตœ๋Œ€ ๋Œ€๊ธฐ ์‹œ๊ฐ„
);

// RetryConfig ์ปค์Šคํ„ฐ๋งˆ์ด์ง•
RetryConfig retryConfig = RetryConfig.custom()
.maxAttempts(3) // ์ตœ๋Œ€ ์žฌ์‹œ๋„ ํšŸ์ˆ˜ (์ดˆ๊ธฐ ์‹œ๋„ ํฌํ•จ)
.intervalFunction(intervalFunction) // Exponential Backoff ์ ์šฉ
.retryOnException(throwable -> {
// ์ผ์‹œ์  ์˜ค๋ฅ˜๋งŒ ์žฌ์‹œ๋„: 5xx ์„œ๋ฒ„ ์˜ค๋ฅ˜, ํƒ€์ž„์•„์›ƒ, ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜
if (throwable instanceof FeignException feignException) {
int status = feignException.status();
// 5xx ์„œ๋ฒ„ ์˜ค๋ฅ˜๋งŒ ์žฌ์‹œ๋„
if (status >= 500 && status < 600) {
log.debug("์žฌ์‹œ๋„ ๋Œ€์ƒ ์˜ˆ์™ธ: FeignException (status: {})", status);
return true;
}
return false;
}
if (throwable instanceof SocketTimeoutException ||
throwable instanceof TimeoutException) {
log.debug("์žฌ์‹œ๋„ ๋Œ€์ƒ ์˜ˆ์™ธ: {}", throwable.getClass().getSimpleName());
return true;
}
return false;
})
// ignoreExceptions๋Š” ์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ
// retryOnException์—์„œ 5xx๋งŒ ์žฌ์‹œ๋„ํ•˜๊ณ  4xx๋Š” ์ œ์™ธํ•˜๋ฏ€๋กœ,
// ๋ณ„๋„๋กœ ignoreExceptions๋ฅผ ์„ค์ •ํ•  ํ•„์š”๊ฐ€ ์—†์Œ
.build();

// ๊ฒฐ์ œ ์š”์ฒญ API: ์œ ์ € ์š”์ฒญ ๊ฒฝ๋กœ์—์„œ ์‚ฌ์šฉ๋˜๋ฏ€๋กœ Retry ๋น„ํ™œ์„ฑํ™” (๋น ๋ฅธ ์‹คํŒจ)
// ์‹คํŒจ ์‹œ ์ฃผ๋ฌธ์€ PENDING ์ƒํƒœ๋กœ ์œ ์ง€๋˜์–ด ์Šค์ผ€์ค„๋Ÿฌ์—์„œ ๋ณต๊ตฌ๋จ
RetryConfig noRetryConfig = RetryConfig.custom()
.maxAttempts(1) // ์žฌ์‹œ๋„ ์—†์Œ (์ดˆ๊ธฐ ์‹œ๋„๋งŒ)
.build();
retryRegistry.addConfiguration("paymentGatewayClient", noRetryConfig);

// ์Šค์ผ€์ค„๋Ÿฌ ์ „์šฉ ํด๋ผ์ด์–ธํŠธ: ๋น„๋™๊ธฐ/๋ฐฐ์น˜ ๊ธฐ๋ฐ˜์œผ๋กœ Retry ์ ์šฉ
// Exponential Backoff ์ ์šฉํ•˜์—ฌ ์ผ์‹œ์  ์˜ค๋ฅ˜ ์ž๋™ ๋ณต๊ตฌ
retryRegistry.addConfiguration("paymentGatewaySchedulerClient", retryConfig);

log.info("Resilience4j Retry ์„ค์ • ์™„๋ฃŒ:");
log.info(" - ๊ฒฐ์ œ ์š”์ฒญ API (paymentGatewayClient): Retry ์—†์Œ (์œ ์ € ์š”์ฒญ ๊ฒฝ๋กœ - ๋น ๋ฅธ ์‹คํŒจ)");
log.info(" - ์กฐํšŒ API (paymentGatewaySchedulerClient): Exponential Backoff ์ ์šฉ (์Šค์ผ€์ค„๋Ÿฌ - ๋น„๋™๊ธฐ/๋ฐฐ์น˜ ๊ธฐ๋ฐ˜ Retry)");

return retryRegistry;
}
}

81 changes: 81 additions & 0 deletions apps/commerce-api/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,87 @@ spring:
job:
enabled: false # ์Šค์ผ€์ค„๋Ÿฌ์—์„œ ์ˆ˜๋™ ์‹คํ–‰ํ•˜๋ฏ€๋กœ ์ž๋™ ์‹คํ–‰ ๋น„ํ™œ์„ฑํ™”

circuitbreaker:
enabled: false # FeignClient ์ž๋™ Circuit Breaker ๋น„ํ™œ์„ฑํ™” (์–ด๋Œ‘ํ„ฐ ๋ ˆ๋ฒจ์—์„œ pgCircuit ์‚ฌ์šฉ)
resilience4j:
enabled: true # Resilience4j ํ™œ์„ฑํ™”

resilience4j:
circuitbreaker:
configs:
default:
registerHealthIndicator: true
slidingWindowSize: 20 # ์Šฌ๋ผ์ด๋”ฉ ์œˆ๋„์šฐ ํฌ๊ธฐ (Building Resilient Distributed Systems ๊ถŒ์žฅ: 20~50)
minimumNumberOfCalls: 1 # ์ตœ์†Œ ํ˜ธ์ถœ ํšŸ์ˆ˜ (์ฒซ ํ˜ธ์ถœ๋ถ€ํ„ฐ ํ†ต๊ณ„ ์ˆ˜์ง‘ํ•˜์—ฌ ๋ฉ”ํŠธ๋ฆญ ์ฆ‰์‹œ ๋…ธ์ถœ)
permittedNumberOfCallsInHalfOpenState: 3 # Half-Open ์ƒํƒœ์—์„œ ํ—ˆ์šฉ๋˜๋Š” ํ˜ธ์ถœ ์ˆ˜
automaticTransitionFromOpenToHalfOpenEnabled: true # ์ž๋™์œผ๋กœ Half-Open์œผ๋กœ ์ „ํ™˜
waitDurationInOpenState: 10s # Open ์ƒํƒœ ์œ ์ง€ ์‹œ๊ฐ„ (10์ดˆ ํ›„ Half-Open์œผ๋กœ ์ „ํ™˜)
failureRateThreshold: 50 # ์‹คํŒจ์œจ ์ž„๊ณ„๊ฐ’ (50% ์ด์ƒ ์‹คํŒจ ์‹œ Open)
slowCallRateThreshold: 50 # ๋А๋ฆฐ ํ˜ธ์ถœ ๋น„์œจ ์ž„๊ณ„๊ฐ’ (50% ์ด์ƒ ๋А๋ฆฌ๋ฉด Open) - Release It! ๊ถŒ์žฅ: 50~70%
slowCallDurationThreshold: ${RESILIENCE4J_CIRCUITBREAKER_SCHEDULER_SLOW_CALL_DURATION_THRESHOLD:2s} # ๋А๋ฆฐ ํ˜ธ์ถœ ๊ธฐ์ค€ ์‹œ๊ฐ„ (2์ดˆ ์ด์ƒ) - Building Resilient Distributed Systems ๊ถŒ์žฅ: 2s (ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ ๋™์  ์„ค์ • ๊ฐ€๋Šฅ)
recordExceptions:
- feign.FeignException
- feign.FeignException$InternalServerError
- feign.FeignException$ServiceUnavailable
- feign.FeignException$GatewayTimeout
- feign.FeignException$BadGateway
- java.net.SocketTimeoutException
- java.util.concurrent.TimeoutException
ignoreExceptions: [] # ๋ชจ๋“  ์˜ˆ์™ธ๋ฅผ ๊ธฐ๋ก (๋ฌด์‹œํ•  ์˜ˆ์™ธ ์—†์Œ)
instances:
pgCircuit:
baseConfig: default
slidingWindowSize: 20 # Building Resilient Distributed Systems ๊ถŒ์žฅ: 20 (๊ณผ์ œ ๊ถŒ์žฅ๊ฐ’)
minimumNumberOfCalls: 1 # ์ฒซ ํ˜ธ์ถœ๋ถ€ํ„ฐ ํ†ต๊ณ„ ์ˆ˜์ง‘ํ•˜์—ฌ ๋ฉ”ํŠธ๋ฆญ ์ฆ‰์‹œ ๋…ธ์ถœ
waitDurationInOpenState: 10s
failureRateThreshold: 50
slowCallRateThreshold: 50 # ๋А๋ฆฐ ํ˜ธ์ถœ ๋น„์œจ ์ž„๊ณ„๊ฐ’ (50% ์ด์ƒ ๋А๋ฆฌ๋ฉด Open) - Release It! ๊ถŒ์žฅ: 50~70%
slowCallDurationThreshold: ${RESILIENCE4J_CIRCUITBREAKER_PAYMENT_GATEWAY_SLOW_CALL_DURATION_THRESHOLD:2s} # ๋А๋ฆฐ ํ˜ธ์ถœ ๊ธฐ์ค€ ์‹œ๊ฐ„ (2์ดˆ ์ด์ƒ) - Building Resilient Distributed Systems ๊ถŒ์žฅ: 2s (ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ ๋™์  ์„ค์ • ๊ฐ€๋Šฅ)
retry:
configs:
default:
maxAttempts: 3 # ์ตœ๋Œ€ ์žฌ์‹œ๋„ ํšŸ์ˆ˜ (์ดˆ๊ธฐ ์‹œ๋„ ํฌํ•จ)
waitDuration: 500ms # ์žฌ์‹œ๋„ ๋Œ€๊ธฐ ์‹œ๊ฐ„ (๊ธฐ๋ณธ๊ฐ’, paymentGatewayClient๋Š” Java Config์—์„œ Exponential Backoff ์ ์šฉ)
retryExceptions:
# ์ผ์‹œ์  ์˜ค๋ฅ˜๋งŒ ์žฌ์‹œ๋„: 5xx ์„œ๋ฒ„ ์˜ค๋ฅ˜, ํƒ€์ž„์•„์›ƒ, ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜
- feign.FeignException$InternalServerError # 500 ์—๋Ÿฌ
- feign.FeignException$ServiceUnavailable # 503 ์—๋Ÿฌ
- feign.FeignException$GatewayTimeout # 504 ์—๋Ÿฌ
- java.net.SocketTimeoutException
- java.util.concurrent.TimeoutException
ignoreExceptions:
# ํด๋ผ์ด์–ธํŠธ ์˜ค๋ฅ˜(4xx)๋Š” ์žฌ์‹œ๋„ํ•˜์ง€ ์•Š์Œ: ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์˜ค๋ฅ˜์ด๋ฏ€๋กœ ์žฌ์‹œ๋„ํ•ด๋„ ์„ฑ๊ณตํ•˜์ง€ ์•Š์Œ
- feign.FeignException$BadRequest # 400 ์—๋Ÿฌ
- feign.FeignException$Unauthorized # 401 ์—๋Ÿฌ
- feign.FeignException$Forbidden # 403 ์—๋Ÿฌ
- feign.FeignException$NotFound # 404 ์—๋Ÿฌ
timelimiter:
configs:
default:
timeoutDuration: 6s # ํƒ€์ž„์•„์›ƒ ์‹œ๊ฐ„ (Feign readTimeout๊ณผ ๋™์ผ)
cancelRunningFuture: true # ์‹คํ–‰ ์ค‘์ธ Future ์ทจ์†Œ
instances:
paymentGatewayClient:
baseConfig: default
timeoutDuration: 6s
paymentGatewaySchedulerClient:
baseConfig: default
timeoutDuration: 6s
bulkhead:
configs:
default:
maxConcurrentCalls: 20 # ๋™์‹œ ํ˜ธ์ถœ ์ตœ๋Œ€ ์ˆ˜ (Building Resilient Distributed Systems: ๊ฒฉ๋ฒฝ ํŒจํ„ด)
maxWaitDuration: 5s # ๋Œ€๊ธฐ ์‹œ๊ฐ„ (5์ดˆ ์ดˆ๊ณผ ์‹œ BulkheadFullException ๋ฐœ์ƒ)
instances:
paymentGatewayClient:
baseConfig: default
maxConcurrentCalls: 20 # PG ํ˜ธ์ถœ์šฉ ์ „์šฉ ๊ฒฉ๋ฒฝ: ๋™์‹œ ํ˜ธ์ถœ ์ตœ๋Œ€ 20๊ฐœ๋กœ ์ œํ•œ
maxWaitDuration: 5s
paymentGatewaySchedulerClient:
baseConfig: default
maxConcurrentCalls: 10 # ์Šค์ผ€์ค„๋Ÿฌ์šฉ ๊ฒฉ๋ฒฝ: ๋™์‹œ ํ˜ธ์ถœ ์ตœ๋Œ€ 10๊ฐœ๋กœ ์ œํ•œ (๋ฐฐ์น˜ ์ž‘์—…์ด๋ฏ€๋กœ ๋” ๋ณด์ˆ˜์ )
maxWaitDuration: 5s

springdoc:
use-fqn: true
swagger-ui:
Expand Down
Loading