Circuit Breaker ์ ์ฉ
API ํต์ ์์ ์ํท ๋ธ๋ ์ด์ปค๋ฅผ ์ ์ฉํด์ผ ํ๋ค๋ ์๊ธฐ๋ฅผ ๋ค์๊ณ , ์ด ์ญ์ ์ฒ์ ๋ฃ๋ ์ฉ์ด๋ผ ์ผ๋จ ์ดํด๋ฅผ ์ํด..
Circuit Breaker ๋?
- ํ๋ก์ฐจ๋จ๊ธฐ๋ก ๋ถ๋ฆฌ๋ฉฐ, ์ ๊ธฐ์ ํ๋ก๋ฅผ ๊ณผ์ ์ ๋ฑ์ผ๋ก๋ถํฐ ๋ณดํธํ๊ธฐ ์ํ ์ฅ์น
- ์์คํ
์์ ์ฅ์ ์ ํ์ฐ์ ๋ง๊ธฐ์ํด (๊ฒฐํจ์ ๊ฒฉ๋ฆฌ) ์ฌ์ฉ๋๋ ํจํด์ ์ผ์ข
- Netflix ์ Hystrix๊ฐ ์์
์ฆ, ํต์ ์ ๋ฌธ์ ๊ฐ ์์ ๋, pool ์ ๊ณ์ ์ ์ ํ๋ ํ์์ ๋ง๊ธฐ ์ํด, ์ฐจ๋จ๊ธฐ๋ฅผ open ํ์ฌ ๊ฒฉ๋ฆฌํ๋ค๊ณ ์๊ฐํ๋ฉด ๋ ๊ฑฐ ๊ฐ๋ค.
๋ญ๊ฐ ๊ฐ๋
์ ์ผ๋ก๋ ์ดํด๊ฐ ๋๋ฉด์๋ ํท๊ฐ๋ฆฌ๋ ๊ฐ๋
์ด๋๊น๋.. (์ดํด์๋๋ค๋ ์๋ฆฌ…?)
์ฌํํผ ๋ณธ๋ก ์ผ๋ก ๋ค์ด๊ฐ์ ์ด์ ์ ์ ์ฉํ๋ Feign ์ ์ํท๋ธ๋ ์ด์ปค๋ฅผ ์ ์ฉ์์ผ๋ณด๋๋ก ํจ.
1. ์์กด์ฑ ์ถ๊ฐ
์ด์ ๊ณผ ๊ฐ์ด maven ์ ์ฌ์ฉํ๋ฏ๋ก pom.xml ์ ์ถ๊ฐํด์ค.
1
2
3
4
5
6
7
8
9
10
|
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-feign</artifactId>
<version>0.17.0</version>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-circuitbreaker</artifactId>
<version>0.17.0</version>
</dependency>
|
2. Config Decorating
- (1)์์ ์์ฑํ AAAClientConfig ๋ฅผ ๋ณ๊ฒฝ
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
@Configuration
public class AAAClientConfig {
@Value("${url}")
private String url;
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50) // ์คํจ ์๊ณ์น 50% ๋ก ์ค์
.ringBufferSizeInClosedState(20) // 20๋ฒ ์๋
.ringBufferSizeInHalfOpenState(10) // 10์ด ํ 10๋ฒ ์๋
.waitDurationInOpenState(Duration.ofSeconds(3L)) // Open ํ 3์ด ๊ธฐ๋ค๋ฆผ
.build();
CircuitBreaker circuitBreaker = CircuitBreaker.of("AAAClientCB", config);
FeignDecorators decorators =
FeignDecorators.builder().withCircuitBreaker(circuitBreaker).build();
@Bean
public AAAClient aAAClient() {
return Resilience4jFeign.builder(decorators)
.client(new OkHttpClient())
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.options(new Request.Options(3000, 5000))
.retryer(Retryer.NEVER_RETRY)
.logger(new Slf4jLogger(AAAClient.class))
.logLevel(Logger.Level.FULL)
.target(AAAClient.class, url);
}
}
|
3. Test ์ฝ๋ ์์ฑ
- ์ ์์ ์ธ ๊ฒฝ์ฐ
- ์ผ๋ถ๋ฌ ์คํจ๋ฅผ ํ์ ๋์ ์ํท๋ธ๋ ์ด์ปค open close ํ์ธ
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
|
public class CBTest {
@Rule
public WireMockRule wireMockRule = new WireMockRule();
private AAAClient client;
@Before
public void setUp() {
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50) // ์คํจ ์๊ณ์น 50% ๋ก ์ค์
.ringBufferSizeInClosedState(20) // 20๋ฒ ์๋
.ringBufferSizeInHalfOpenState(10) // 3์ด ํ 10๋ฒ ์๋
.waitDurationInOpenState(Duration.ofSeconds(3L)) // Open ํ 3์ด ๊ธฐ๋ค๋ฆผ
.build();
CircuitBreaker circuitBreaker = CircuitBreaker.of("AAAClientCB", config);
FeignDecorators decorators =
FeignDecorators.builder().withCircuitBreaker(circuitBreaker).build();
aAAClient = Resilience4jFeign.builder(decorators)
.client(new OkHttpClient())
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.options(new Request.Options(3000, 5000))
.retryer(Retryer.NEVER_RETRY)
.logger(new Slf4jLogger(AAAClient.class))
.logLevel(Logger.Level.FULL)
.target(AAAClient.class, "http://localhost:8080");
}
@Test
public void ์ ์์ธ๊ฒฝ์ฐ_Test() {
stubFor(
get(urlPathMatching("/1"))
.willReturn(
aResponse()
.withStatus(200)
.withHeader("content-type", "application/json")
.withBody("{}")
)
);
boolean allSuccess = IntStream.range(0, 40)
.mapToObj(i -> Try.of(() -> client.getDetail(1)))
.allMatch(t -> t.isSuccess());
assertThat(allSuccess).isTrue();
}
@Test
public void ์ํท๋ธ๋ ์ด์ปค_Open_Close_Test() {
stubFor(
get(urlPathMatching("/71"))
.willReturn(
aResponse()
.withStatus(200)
.withHeader("content-type", "application/json")
.withBody("{}")
)
);
stubFor(
get(urlPathMatching("/72"))
.willReturn(
aResponse()
.withStatus(500)
.withHeader("content-type", "application/json")
.withBody("{}")
)
);
// ์๋ฌ ๋น์จ 50% ์ด์
IntStream.range(0, 10).forEach(i -> Try.of(() -> client.getDetail(72)));
// 3์ด๊ฐ ์คํ ์ํ์์ ๋ฐ๋ก ์๋ฌ
sleep(1000);
Try<ApiResponse> failFast1 = Try.of(() -> client.getDetail(71));
sleep(1500);
Try<ApiResponse> failFast2 = Try.of(() -> client.getDetail(71));
// 3์ด ํ ๋ฐ ์คํ
sleep(1000);
Try.of(() -> client.getDetail(71));
Try<ApiResponse> realEx = Try.of(() -> client.getDetail(72));// 1๊ฐ ์คํจ๋ก๋ 50% ๋น์จ ์ ๋จ
// ๋ฐ ์คํ ํ ๋ค์ ์ด๋ฆฌ์ง ์์ผ๋ฉด ๋ชจ๋ ์ฑ๊ณต
boolean allSuccess = IntStream.range(0, 40)
.mapToObj(i -> Try.of(() -> client.getDetail(71)))
.allMatch(t -> t.isSuccess());
SoftAssertions.assertSoftly(softly -> {
softly.assertThat(failFast1.getCause()).isInstanceOf(CallNotPermittedException.class);
softly.assertThat(failFast2.getCause()).isInstanceOf(CallNotPermittedException.class);
softly.assertThat(realEx.getCause()).isNotInstanceOf(CallNotPermittedException.class);
softly.assertThat(allSuccess).isTrue();
});
}
private void sleep(int sleepTime) {
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
}
}
}
|
- SoftAssertions์ ๊ฐ assertThat ์ด ์คํจํด๋ ๊ฒ์ฆ์ ๊ณ์ ์คํํ๊ณ ๋ง์ง๋ง์ ๊ฒฐ๊ณผ๋ฅผ ํ๊บผ๋ฒ์ ๋ณด์ฌ์ค
- (๋ง์ง๋ง์ softly.assertAll() ์ถ๊ฐ)
- softly.assertAll() ์ ์ฌ์ฉํ์ง ์์ ๊ฒฝ์ฐ์ ๋ํด์ AssertJ ์ Collect all errors with soft assertions ๋ถ๋ถ ์ฐธ์กฐ