README
WireMock์ ๋ํ ์ค๋ช ์ด ๋ถ์กฑํ๋ฉฐ, WireMock์ ์๊ณ ์๋ ๊ฐ์ ํ์ ์งํ๋ฉ๋๋ค.
WireMock์ ๋ํ ์ค๋ช ์ ์๋๋ฅผ ์ฐธ๊ณ ํด์ฃผ์ธ์.
- Spring Cloud Contract WireMock Docs
- Baeldung ์ Introduction WireMock
- ์ฐ์ํ๊ธฐ์ ๋ธ๋ก๊ทธ : ์ผ, ๋๋ WireMock ์ผ๋ก ํ
์คํธํ ์ ์์ด
Cloud OpenFeign ์ ์ฌ์๋๋ฅผ ์ํด Retryer.Default ๋ฅผ ๋น์ผ๋ก ๋ฑ๋กํ์ ๋ ์๋ 2๊ฐ์ง ๊ฒฝ์ฐ ์ ์ฌ์๋๋ฅผ ํ๋ ๊ฒ์ ํ์ธํ๊ณ ํ
์คํธ ํ๋ ๊ณผ์ ์ ์๊ฐํฉ๋๋ค.
1. IOException ๋ฐ์ ์, ์ฌ์๋
2. ์๋ต์ Retry-After ํค๋๊ฐ ์์ด์ RetryableException์ด ๋ฐ์ํ ๋, ์ฌ์๋
์๋ก : Feign vs OpenFeign์ ์ฌ์๋ ์ฐจ์ด
Feign ์ ์ฌ์๋ ์กฐ๊ฑด
1. IOException ๋ฐ์ ์, ์ผ์์ ์ธ ๋คํธ์ํฌ ์ค๋ฅ๋ก ๊ฐ์ฃผํด ์ฌ์๋๋ฅผ ํฉ๋๋ค.
2. ์๋ต์ Retry-After ํค๋๊ฐ ์์ ๊ฒฝ์ฐ ์ฌ์๋๋ฅผ ํฉ๋๋ค.
Cloud OpenFeign ์ ์ฌ์๋ ์กฐ๊ฑด
ํ์ง๋ง, Cloud OpenFeign ์ ๋ณ๋์ Feign.Retryer ๋น์ ๋ฑ๋กํ์ง ์์ผ๋ฉด ์ฌ์๋๋ฅผ ํ์ง ์์ต๋๋ค. ๊ธฐ๋ณธ ์ค์ ์ Retryer.NEVER_RETRY ๋ฅผ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ๋๋ค.
A bean of Retryer.NEVER_RETRY with the type Retryer is created by default, which will disable retrying.
Notice this retrying behavior is different from the Feign default one, where it will automatically retry IOExceptions, treating them as transient network related exceptions, and any RetryableException thrown from an ErrorDecoder.
๊ทธ๋์, OpenFeign์์ ์ฌ์๋๋ฅผ ์ฌ์ฉํ๋ ค๋ฉด Feign ์ Retryer ๋น์ ์ฌ์ ์ํด์ผ ํฉ๋๋ค.
์๋ฅผ ๋ค์ด, ์๋ ์ฝ๋๋ ๋ชจ๋ @FeignClient์ ์ฌ์๋๋ฅผ ์ถ๊ฐํ๋ ์ ์ญ์ ์ธ ์ค์ ํด๋์ค์
๋๋ค.
@Configuration // ํด๋น ์ด๋
ธํ
์ด์
์ด ์์ผ๋ฉด ๋ชจ๋ @FeignClient ์ ์ ์ญ์ ์ค์
@EnableFeignClients
public class GlobalFeignClientConfig {
/**
* 0.1์ด์ ๊ฐ๊ฒฉ์ผ๋ก ์์ํด ์ต๋ 1์ด์ ๊ฐ๊ฒฉ์ผ๋ก ์ ์ ์ฆ๊ฐํ๋ฉฐ, ์ต๋ 5๋ฒ ์ฌ์๋ํ๋ค.
*/
@Bean
Retryer.Default retryer() {
return new Retryer.Default(100L, TimeUnit.SECONDS.toMillis(1L), 5);
}
}
@FeignClient(name =, url=)
public interface GithubClient {
@GetMapping(path = "...", headers = "...")
List<DockerImage> getAllDockerImages();
}
๊ทธ๋ ๋ค๋ฉด ์ฌ์ ์ํ Retryer๊ฐ IOException ๊ณผ RetryableException ์ด ๋ฐ์ํ์ ๋ ์ฌ์๋๋ฅผ ํ๋์ง ํ
์คํธ๋ฅผ ํด๋ณด๋ ค๊ณ ํฉ๋๋ค.
Mock์ด ์๋ WireMock์ ์ฌ์ฉํด์ผ ํ๋ ์ด์
Mock์ ํ๊ณ
@FeignClient์ ์ฌ์๋ ๋ก์ง์ ํ
์คํธํ๋ ค๋ฉด @Mock์ด๋ @MockBean์ ์ฌ์ฉํ ์ ์์ต๋๋ค.
@FeignClient ์์ฒด๋ฅผ ๋ชจํนํ๋ฉด ์๋์ ๊ฐ์ ๋ค์ํ ์ปดํฌ๋ํธ๋ค์ด ๋์ํ์ง ์๊ฒ ๋ฉ๋๋ค.
- Retryer - ์ฌ์๋ ๋ด๋น
- ErrorDecoder - 200๋ฒ๋ ์ธ ์๋ต ์ฒ๋ฆฌ
- Encoder/Decoder - ์ง๋ ฌํ/์ญ์ง๋ ฌํ
| @Autowired FeignClient | @Mock / @MockBean FeignClient | |
| ์์ฑ ๋ฐฉ์ | Spring์ด ๋์ ํ๋ก์ ์์ฑ | Mockito๊ฐ ๊ฐ์ง ๊ฐ์ฒด ์์ฑ |
| ๋ด๋ถ ์ปดํฌ๋ํธ | Retryer, ErrorDecoder ๋ฑ ์ค์ ๋์ | ๋ชจ๋ ๋ด๋ถ ๋ก์ง ๋ฌด์ |
| ๋ฉ์๋ ํธ์ถ ์ | ์ค์ HTTP ํต์ + Feign ํ์ดํ๋ผ์ธ ์คํ | Mockito ์ค์ ๋ ๋์๋ง ๋ฐํ (๊ธฐ๋ณธ: null) |
๋ฐ๋ผ์ Feign Client์ ์ฌ์๋ ๋ก์ง์ ์ ๋๋ก ํ
์คํธํ๋ ค๋ฉด ์ค์ FeignClient ๋น์ ์ฃผ์
๋ฐ์์ผ ํฉ๋๋ค.
์ค์ FeignClient ํ๋ก์ ํ์ธํ๊ธฐ
@Autowired๋ก FeignClient๋ฅผ ์ฃผ์ ๋ฐ๊ณ ๋๋ฒ๊น ํด๋ณด๋ฉด ํ๋ก์ ๊ฐ์ฒด ๋ด๋ถ์ Retryer๊ฐ ์ค์ ๋์ด ์๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
@Autowired
private GithubClient githubClient;
@Test
void test() {
System.out.println(githubClient.getClass().getSimpleName()); // <-- ๋ธ๋ ์ดํน ํฌ์ธํธ
}
๋๋ฒ๊ฑฐ ๊ฒฐ๊ณผ

WireMock์ผ๋ก HTTP ์๋ฒ ๋ชจํนํ๊ธฐ
์ค์ FeignClient๋ฅผ ํ
์คํธํ๋ ค๋ฉด HTTP ์์ฒญ์ ๋ฐ์ ์๋ฒ๊ฐ ํ์ํฉ๋๋ค. ์ด๋ WireMock์ ์ฌ์ฉํ์ฌ HTTP ์๋ฒ๋ฅผ ๋ชจํนํ ์ ์์ต๋๋ค.
์์กด์ฑ์ ์ถ๊ฐํฉ๋๋ค.
testImplementation 'org.springframework.cloud:spring-cloud-contract-wiremock'
Retryer.Default ์ ์ฌ์๋ ์กฐ๊ฑด ์์ธํ ์์๋ณด๊ธฐ
์ด๋ฒ ๋ด์ฉ์ Feign ์ ์ฌ์๋ ์กฐ๊ฑด์ธ IOException ๊ณผ Retry-After ํค๋๊ฐ ์์ ๋, ์ ๋ง ์ฌ์๋๋ฅผ ํ๋์ง ๋ด๋ถ ์ฝ๋๋ฅผ ๋ณด๋ฉฐ ํ์ธํ๋ ๋ด์ฉ์ ๋๋ค.
์ฌ์๋ ์กฐ๊ฑด 1. IOException ๋ฐ์
Retryer.Default์ ์ฝ๋๋ฅผ ํ์ธํ๋ฉด RetryableException์ด ๋ฐ์ํ์ ๋ ์ฌ์๋ํ๋ ๊ฒ์ ์ ์ ์์ต๋๋ค.

๊ทธ๋ ๋ค๋ฉด RetryableException ์ ์ธ์ ๋ง๋ค์ด์ง๊น์?
Feign์ SynchronousMethodHandler.executeAndDecode( ) ๋ฉ์๋๋ฅผ ์ดํด๋ณด๋ฉด RetryableException์ด ์ด๋ป๊ฒ ์์ฑ๋๋์ง ์ ์ ์์ต๋๋ค.
executeAndDecode ์์ RetryableException ์ด ๋ง๋ค์ด์ง๋, executeAndDecode ๋ก ๋ค์ด๊ฐ๋ด
๋๋ค.

client.execute์์ IOException์ด ๋ฐ์ํ๋ฉด errorExecuting() ๋ฉ์๋๊ฐ ํธ์ถ๋๊ณ , ์ด ๋ฉ์๋๊ฐ RetryableException์ ์์ฑํฉ๋๋ค.


์ ๋ฆฌํ๋ฉด, ์์ฒญ ์ค IOException ๋ฐ์ โ RetryableException ์์ฑ โ Retryer.Default๊ฐ ์ฌ์๋
์ฌ์๋ ์กฐ๊ฑด 2. Retry-After ํค๋๊ฐ ์์ ๋, ์ฌ์๋๋ฅผ ํฉ๋๋ค.
๋ ๋ฒ์งธ๊ฒฝ์ฐ๋ ErrorDecoder ์์ Retry-After ๋ ํค๋๋ฅผ ํ์ฑํด์, ํด๋น ํค๋๊ฐ ์์ผ๋ฉด new RetrableException ์ ๋ฐ์ํ๋๋ฐ์.

Retry-After ํค๋๋ ์๋ฒ๊ฐ ํด๋ผ์ด์ธํธ์๊ฒ "์ ์ ํ์ ๋ค์ ์๋ํด๋ฌ๋ผ"๊ณ ์๋ ค์ฃผ๋ HTTP ํค๋์
๋๋ค.
์ฃผ๋ก ๋ค์๊ณผ ๊ฐ์ ์ํ ์ฝ๋์ ํจ๊ป ์ฌ์ฉ๋ฉ๋๋ค.
- 503 Service Unavailable - ์๋น์ค ์ผ์์ ์ค๋จ
- 429 Too Many Requests - ์์ฒญ ์ ํ ์ด๊ณผ
HTTP/1.1 429 Too Many Requests
Retry-After: 60
--> "60์ด ํ์ ๋ค์ ์๋ํด์ฃผ์ธ์"
Retry-After vs ์ค์ ๊ฐ, ์ด๋ค ๊ฒ ์ฐ์ ์ผ๊น?
Retryer.Default๋ฅผ ์์ฑํ ๋ ์ต๋ ๋๊ธฐ ์๊ฐ(maxPeriod)์ ์ค์ ํ๋๋ฐ, Retry-After ํค๋ ๊ฐ๊ณผ ์ถฉ๋ํ๋ฉด ์ด๋ป๊ฒ ๋ ๊น์?
new Retryer.Default(100L, TimeUnit.SECONDS.toMillis(1L), 5);
// โ maxPeriod = 1์ด
์ด ์ญ์, Retryer.Default ์ ๋ด๋ถ ๋ก์ง์ ๋ณด๋ฉด ์ ์ ์์ต๋๋ค.
Retry-After ํค๋ ๊ฐ์ด ์ฐ์ ์ด์ง๋ง, ์ฌ์ฉ์๊ฐ ์ค์ ํ maxPeriod๋ฅผ ์ด๊ณผํ ์๋ ์์ต๋๋ค.
public void continueOrPropagate(RetryableException e) {
long interval;
if (e.retryAfter() != null) {
// Retry-After ๊ฐ์ด ์์ผ๋ฉด ์ฐ์ ์ฌ์ฉ
interval = e.retryAfter() - currentTimeMillis();
if (interval > maxPeriod) {
interval = maxPeriod; // ๋จ, maxPeriod๋ฅผ ์ด๊ณผํ๋ฉด ์ ํ
}
} else {
// Retry-After ์์ผ๋ฉด ์ค์ ๊ฐ ์ฌ์ฉ
interval = nextMaxInterval();
}
}
๊ทธ๋์, ์๋ฒ๊ฐ ์๋ตํ Retry-After ํค๋์ ๊ฐ์ ์๊ฒฉํ๊ฒ ์งํค๊ณ ์ถ๋ค๋ฉด, Retryer ์ continueOrPropagate( ) ๋ฅผ ์ค๋ฒ๋ผ์ด๋ฉํด์ผ ํฉ๋๋ค.

ํ ์คํธ ์ฝ๋ ์์ฑํ๊ธฐ
ํ
์คํธํ ๋ด์ฉ์ ๋ค์ 2๊ฐ์ง์
๋๋ค.
1. IOException ๋ฐ์ ์ ์ฌ์๋๊ฐ ๋๋์ง?
2. Retry-After ํค๋ ๊ฐ์ ๋ฐ๋ผ ์ฌ์๋๊ฐ ๋๋์ง?
ํ ์คํธ 1. IOException ๋ฐ์ ์ ์ฌ์๋
WireMock์ Fault.CONNECTION_RESET_BY_PEER๋ฅผ ์ฌ์ฉํ๋ฉด IOException์ ๋ฐ์์ํฌ ์ ์์ต๋๋ค.
@Test
@DisplayName("IOException ๋ฐ์ ์ ์ค์ ๋ ํ์๋งํผ ์ฌ์๋ํ๋ค")
void retryOnIOException() {
// Given: WireMock์ด CONNECTION_RESET_BY_PEER ์ค๋ฅ๋ฅผ ๋ฐํํ๋๋ก ์ค์
stubFor(get(GithubClient.DOCKER_REPO_URL)
.willReturn(aResponse()
.withFault(Fault.CONNECTION_RESET_BY_PEER)));
// When: FeignClient ํธ์ถ
assertThatThrownBy(() -> githubClient.getAllDockerImages())
.isInstanceOf(RetryableException.class)
.hasCauseInstanceOf(IOException.class);
// Then: ์ค์ ๋ ํ์๋งํผ ์ฌ์๋ ํ์ธ
/**
* ์ฃผ์: WireMock์ด ๋ด๋ถ์ ์ผ๋ก ์ฌ์ฉํ๋ ApacheHttpClient๊ฐ
* IOException ๋ฐ์ ์ ์๋์ผ๋ก 1ํ ์ฌ์๋ํ๊ธฐ ๋๋ฌธ์ ์ค์ ์์ฒญ ํ์๋ *2๊ฐ ๋ฉ๋๋ค.
*
* ์ฐธ๊ณ : https://github.com/wiremock/wiremock/issues/1789
*/
verify(FEIGN_RETRY_MAX_ATTEMPTS * 2,
getRequestedFor(urlEqualTo(GithubClient.DOCKER_REPO_URL)));
}
์ฃผ์์ฌํญ: WireMock์ ApacheClient ์ฌ์๋
๋๋ฒ๊ฑฐ๋ก retryer.continueOrPropagate(e) ๋ฉ์๋์ ๋ธ๋ ์ดํน ํฌ์ธํธ๋ฅผ ์ฐ์ด๋ณด๋ฉด ์ ํํ 5๋ฒ ํธ์ถ๋๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
๊ทธ๋ฐ๋ฐ WireMock์ ์์ฒญ ์นด์ดํธ๋ 10๋ฒ(5 ร 2)์ผ๋ก ๋์ต๋๋ค.
์ด์ ๋ WireMock์ด ๋ด๋ถ์ ์ผ๋ก ์ฌ์ฉํ๋ ApacheHttpClient๊ฐ IOException ๋ฐ์ ์ ์๋์ผ๋ก 1ํ ์ฌ์๋ํ๊ธฐ ๋๋ฌธ์
๋๋ค.
์ฐธ๊ณ ์๋ฃ
์ด๋ ๊ฒ ์ธ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋์์ ์์กดํ๋ ํ
์คํธ๋ ๋์ค์ ๊นจ์ง ์ฐ๋ ค๊ฐ ์์ต๋๋ค.
๋ฐ๋ผ์ ์ด ํ
์คํธ๋ ์ฝ๋๋ก ์์ฑํ๊ธฐ๋ณด๋ค๋ ๋๋ฒ๊ฑฐ๋ก retryer.continueOrPropagate(e)๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ํธ์ถ๋๋์ง ํ์ธํ๋ ์ ๋๋ฉด ์ถฉ๋ถํ๋ค๋ ์๊ฐ์ด ๋ญ๋๋ค.
ํ ์คํธ 2. Retry-After ํค๋ ์์ ๋ ์ฌ์๋
์ด๋ฒ์๋ ์ค์ง์ ์ผ๋ก ์ ์ฉํ ํ
์คํธ์
๋๋ค.
WireMock ์๋ต์ Retry-After ํค๋๋ฅผ ์ถ๊ฐํ์ฌ ์ฌ์๋ ํ์๋ฅผ ๊ฒ์ฆํฉ๋๋ค.
@ParameterizedTest
@DisplayName("Retry-After ํค๋๊ฐ ์์ผ๋ฉด ์ค์ ๋ ํ์๋งํผ ์ฌ์๋ํ๋ค")
@ValueSource(ints = {403, 429})
void retryWithRetryAfterHeader(int responseCode) {
// Given: HTTP ์๋ฌ ์๋ต + Retry-After ํค๋
stubFor(get(GithubClient.DOCKER_REPO_URL)
.willReturn(aResponse()
.withStatus(responseCode)
.withHeader(HttpHeaders.RETRY_AFTER, "1")));
// When: FeignClient ํธ์ถ
assertThatThrownBy(() -> githubClient.getAllDockerImages())
.isInstanceOf(RetryableException.class);
// Then: ์ฌ์๋ ํ์ ๊ฒ์ฆ
verify(FEIGN_RETRY_MAX_ATTEMPTS,
getRequestedFor(urlEqualTo(GithubClient.DOCKER_REPO_URL)));
}
ํ ์คํธ 3. Retry-After ์ฐ์ ์์ ๊ฒ์ฆ
๋ง์ง๋ง์ผ๋ก Retry-After ํค๋์ Retryer.Default ์ค์ ์๊ฐ ์ค, ๋ ์์ ๊ฐ์ด ์ฌ์๋ ๊ฐ๊ฒฉ์ ์ํ์ ์ ๊ฒฐ์ ํ๋์ง ๊ฒ์ฆํ ์๋ ์์ต๋๋ค.
@Test
@DisplayName("Retry-After ํค๋์ Retryer.Default ์ค์ ์๊ฐ ์ค, ๋ ์์ ๊ฐ์ด ์ฌ์๋ ๊ฐ๊ฒฉ์ ์ํ์ ์ ๊ฒฐ์ ํ๋ค")
void retryTime() {
Duration strictRetryAfterSeconds = Duration.ofSeconds(3);
stubFor(get(GithubClient.DOCKER_REPO_URL)
.willReturn(aResponse()
.withStatus(429)
.withHeader(HttpHeaders.RETRY_AFTER, String.valueOf(strictRetryAfterSeconds.getSeconds()))));
long start = System.currentTimeMillis();
assertThatThrownBy(() -> githubClient.getAllDockerImages())
.isInstanceOf(RetryableException.class);
Duration actualTime = Duration.ofMillis(System.currentTimeMillis() - start);
Duration expectedTime = strictRetryAfterSeconds.multipliedBy(FEIGN_RETRY_MAX_ATTEMPTS - 1);
assertThat(actualTime).isGreaterThanOrEqualTo(expectedTime);
}
๋ง์ฝ, ์๋ฒ๊ฐ ์๋ตํ Retry-After ํค๋์ ๊ฐ์ ์๊ฒฉํ๊ฒ ์งํค๊ณ ์ถ๋ค๋ฉด, Retryer ์ continueOrPropagate( ) ๋ฅผ ์ค๋ฒ๋ผ์ด๋ฉํด์ผ ํฉ๋๋ค.
๋ค์์ ์ ์ฒด ํ
์คํธ ์ฝ๋์
๋๋ค.
- FeignClient ์ค์
@Configuration
@EnableFeignClients
public class GlobalFeignClientConfig {
public static final int FEIGN_RETRY_MAX_ATTEMPTS = 3;
/**
* 1. Retry-After ํค๋ ์กด์ฌ ์, ํด๋น ํค๋ ์๊ฐ์ ์๊ฒฉํ๊ฒ ์กด์ค
* 2. Retry-After ํค๋ ์์ ์, 0.1์ด์ ๊ฐ๊ฒฉ์ผ๋ก ์์ํด ์ต๋ 3์ด์ ๊ฐ๊ฒฉ์ผ๋ก ์ ์ ์ฆ๊ฐํ๋ฉฐ, ์ต๋ 3๋ฒ ์๋ํ๋ค.
*/
@Bean
Retryer.Default retryer() {
return new Retryer.Default(100L, TimeUnit.SECONDS.toMillis(3L), FEIGN_RETRY_MAX_ATTEMPTS);
}
}
- FeignClient ์ค์ ์ฝ๋
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.http.Fault;
import feign.RetryableException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock;
import org.springframework.http.HttpHeaders;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.TestPropertySource;
import java.io.IOException;
import java.time.Duration;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static jonghyeok.onpremiseinstallsupporter.GlobalFeignClientConfig.FEIGN_RETRY_MAX_ATTEMPTS;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
@SpringBootTest
@ActiveProfiles(value = "test")
@AutoConfigureWireMock(port = GithubClientMockTest.RANDOM_PORT)
@TestPropertySource(properties = "github.url=http://localhost:${wiremock.server.port}")
public class GithubClientMockTest {
static final int RANDOM_PORT = 0;
@Autowired
GithubClient githubClient;
@BeforeEach
void setUp() {
WireMock.resetAllRequests();
}
@ParameterizedTest
@DisplayName("์๋ต์ Retry-After ํค๋๊ฐ ์์ผ๋ฉด ์ฌ์๋ ์ค์ ํ์๋งํผ ์ฌ์๋ ํ๋ค.")
@ValueSource(ints = {403, 429})
void retryFivesTimesByResponseCode(int responseCode) {
stubFor(get(GithubClient.DOCKER_REPO_URL)
.willReturn(aResponse()
.withStatus(responseCode)
.withHeader(HttpHeaders.RETRY_AFTER, "1")));
assertThatThrownBy(() -> githubClient.getAllDockerImages())
.isInstanceOf(RetryableException.class);
verify(FEIGN_RETRY_MAX_ATTEMPTS, getRequestedFor(urlEqualTo(GithubClient.DOCKER_REPO_URL)));
}
@Test
@DisplayName("Retry-After ํค๋์ Retryer.Default ์ค์ ์๊ฐ ์ค, ๋ ์์ ๊ฐ์ด ์ฌ์๋ ๊ฐ๊ฒฉ์ ์ํ์ ์ ๊ฒฐ์ ํ๋ค")
void strictRetry() {
Duration strictRetryAfterSeconds = Duration.ofSeconds(3);
stubFor(get(GithubClient.DOCKER_REPO_URL)
.willReturn(aResponse()
.withStatus(429)
.withHeader(HttpHeaders.RETRY_AFTER, String.valueOf(strictRetryAfterSeconds.getSeconds()))));
long start = System.currentTimeMillis();
assertThatThrownBy(() -> githubClient.getAllDockerImages())
.isInstanceOf(RetryableException.class);
Duration actualTime = Duration.ofMillis(System.currentTimeMillis() - start);
Duration expectedTime = strictRetryAfterSeconds.multipliedBy(FEIGN_RETRY_MAX_ATTEMPTS - 1);
assertThat(actualTime).isLessThanOrEqualTo(expectedTime);
}
@Test
@DisplayName("IOException ๋ฐ์ ์ ์ค์ ๋ ํ์๋งํผ ์ฌ์๋ํ๋ค.")
void retryOnIOException() {
stubFor(get(GithubClient.DOCKER_REPO_URL)
.willReturn(aResponse()
.withFault(Fault.CONNECTION_RESET_BY_PEER)));
assertThatThrownBy(() -> githubClient.getAllDockerImages())
.isInstanceOf(RetryableException.class)
.hasCauseInstanceOf(IOException.class);
/**
* https://github.com/wiremock/wiremock/issues/1789
* WireMock ์์ ์ฌ์ฉ์ค์ธ ApacheHttpClient ๊ฐ IOException ๋ฐ์ ์, ๊ธฐ๋ณธ์ ์ผ๋ก 1ํ ์ฌ์๋ ์๋ํ๊ธฐ ๋๋ฌธ์ *2 ๋ฅผ ํจ.
*/
verify(FEIGN_RETRY_MAX_ATTEMPTS * 2, getRequestedFor(urlEqualTo(GithubClient.DOCKER_REPO_URL)));
}
}