<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>zzung.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Sun, 08 Dec 2024 18:12:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>zzung.log</title>
            <url>https://velog.velcdn.com/images/fire_dev/profile/fa123f0c-1632-4cad-9688-946f2cef4788/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. zzung.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/fire_dev" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[카프카 List<LinkedHashMap>를 List<Object> 형태로 파싱하기]]></title>
            <link>https://velog.io/@fire_dev/%EC%B9%B4%ED%94%84%EC%B9%B4-ListLinkedHashMap%EB%A5%BC-ListObject-%ED%98%95%ED%83%9C%EB%A1%9C-%ED%8C%8C%EC%8B%B1%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@fire_dev/%EC%B9%B4%ED%94%84%EC%B9%B4-ListLinkedHashMap%EB%A5%BC-ListObject-%ED%98%95%ED%83%9C%EB%A1%9C-%ED%8C%8C%EC%8B%B1%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 08 Dec 2024 18:12:00 GMT</pubDate>
            <description><![CDATA[<p>카프카 컨슈머에서 List 형태의 메시지를 컨슘할 때, 역직렬화시 LinkedHashMap으로 파싱하여 List&lt;LinkedHashMap&lt;key, value&gt;&gt; 형태로 메시지가 들어오게 된다.</p>
<p>그래서 List로 생각하고 작성한 코드에서 LinkedHashMap 을 캐스트 할 수 없다는 오류가 발생하였다.</p>
<pre><code>LinkedHashMap cannot be cast to class...  </code></pre><p>List형태로 변경하려면 아래와 같이 추가하면 간단하게 해결 가능하다</p>
<h2 id="방법1-convertvalue로-형변환">방법1) convertValue로 형변환</h2>
<pre><code class="language-java">List&lt;TestDto&gt; dataList = objectMapper.convertValue(record.value(), new TypeReference&lt;&gt;() { });</code></pre>
<h2 id="방법2-jsondeserializer-커스텀">방법2) JsonDeserializer 커스텀</h2>
<h3 id="1-testdtodeserializer-생성하는-메서드">1 TestDtoDeserializer 생성하는 메서드</h3>
<pre><code class="language-java">protected JsonDeserializer&lt;TestDto&gt; testDtoDeserializer() {
    ObjectMapper om = new ObjectMapper();
    JavaType javaType = om.getTypeFactory().constructParametricType(List.class, TestDto.class); // LinkedListHashMap =&gt; TestDto
    return new JsonDeserializer&lt;&gt;(javaType, om, false);
}</code></pre>
<h3 id="2-value_deserializer_class_config-설정">2 VALUE_DESERIALIZER_CLASS_CONFIG 설정</h3>
<p>JsonDeserializer&lt; TestDto&gt;를 반환하는 testDtoDeserializer 호출하여 객체를 생성하고 VALUE_DESERIALIZER_CLASS_CONFIG로 설정한다.</p>
<pre><code class="language-java">@Bean
public ConsumerFactory&lt;String, TestDto&gt; testConsumerFactory() {
    JsonDeserializer&lt;TestDto&gt; deserializer = testDtoDeserializer(); // 1 JsonDeserializer&lt;TestDto&gt; 가져오기
    Map&lt;String, Object&gt; properties = new HashMap&lt;&gt;();
    properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress);
    properties.put(ConsumerConfig.GROUP_ID_CONFIG, TEST_COLLECT_GROUP_ID);
    properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
    properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, deserializer); // 2 VALUE_DESERIALIZER_CLASS_CONFIG 로 설정
    properties.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 100);
    properties.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, 600000);

    return new DefaultKafkaConsumerFactory&lt;&gt;(properties, new StringDeserializer(), new ErrorHandlingDeserializer&lt;&gt;(deserializer));
}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[스프링배치 JobParametersValidator 사용하여 필수 파라미터 지정하기]]></title>
            <link>https://velog.io/@fire_dev/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B0%B0%EC%B9%98-JobParametersValidator-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC</link>
            <guid>https://velog.io/@fire_dev/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B0%B0%EC%B9%98-JobParametersValidator-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC</guid>
            <pubDate>Sun, 08 Dec 2024 17:47:09 GMT</pubDate>
            <description><![CDATA[<p>스프링 배치 jobParameter 중 필수값을 지정해야하는 파라미터가 존재하였습니다.
null이 입력되면 배치를 종료처리 해야할지 고민하던 찰나에 이런 경우를 위한 Validator가 있다는 것을 알게 되었습니다</p>
<p>방법은 다음과 같습니다.</p>
<h2 id="방법1-jobparametersvalidator-인터페이스-구현">방법1) JobParametersValidator 인터페이스 구현</h2>
<p>JobParametersValidator를 구현한 CustomJobParametersValidator를 생성합니다.</p>
<pre><code class="language-java">public class CustomJobParametersValidator implements JobParametersValidator {

    @Override
    public void validate(JobParameters jobParameters) throws JobParametersInvalidException {

        if (jobParameters.getString(&quot;name&quot;) == null) { // name이란 parameter가 없으면 예외 발생
            throw new JobParametersInvalidException(&quot;name parameter is not found.&quot;);
        }
    }
}</code></pre>
<p>이후 스프링 job 설정에서 validator로 위에서 생성한 CustomJobParametersValidator을 설정합니다.</p>
<pre><code class="language-java">@RequiredArgsConstructor
@Configuration
public class ValidatorConfiguration {

    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;

    @Bean
    public Job parametersValidatorJob() {
        return this.jobBuilderFactory.get(&quot;parametersValidatorJob&quot;)
                .validator(new CustomJobParametersValidator()) // Validator 설정
                .start(parametersValidatorStep1())
                .next(parametersValidatorStep2())
                .next(parametersValidatorStep3())
                .build();
    }

    ... 
}</code></pre>
<h3 id="방법2-defaultjobparametersvalidator-구현체-사용">방법2) DefaultJobParametersValidator 구현체 사용 </h3>
<p>DefaultJobParametersValidator 구현체에 requiredKeys, optionalKeys 값을 넘겨 필수 파라미터와 옵션 파라미터를 구분할 수 있도록 합니다.</p>
<pre><code class="language-java">@Bean(name = JOB_NAME)
public Job job() throws Exception {
    String[] requiredKeys = {&quot;chnlId&quot;, &quot;critnDts&quot;}; // 필수 파라미터명
    String[] optionalKeys = {&quot;run.id&quot;, &quot;stdCtgId&quot;, &quot;strtDt&quot;, &quot;endDt&quot;}; // 옵션 파라미터명 run.id는 incrementer로 생성되는 값임

    return jobBuilderFactory.get(JOB_NAME)
            .validator(new DefaultJobParametersValidator(requiredKeys, optionalKeys))
            .incrementer(new UniqueRunIdIncrementer())
            .listener(jobExecutionListener())
            .start(startStep())
            .build();
}</code></pre>
<p>jobBuilderFactory로 job을 생성할때 validator로 DefaultJobParametersValidator을 지정하면 다음과 같은 validate함수가 호출됩니다.</p>
<pre><code class="language-java">@Override
public void validate(@Nullable JobParameters parameters) throws JobParametersInvalidException {

    if (parameters == null) {
        throw new JobParametersInvalidException(&quot;The JobParameters can not be null&quot;);
    }

    Set&lt;String&gt; keys = parameters.getParameters().keySet();

    // If there are explicit optional keys then all keys must be in that
    // group, or in the required group.
    if (!optionalKeys.isEmpty()) {

        Collection&lt;String&gt; missingKeys = new HashSet&lt;&gt;();
        for (String key : keys) {
            if (!optionalKeys.contains(key) &amp;&amp; !requiredKeys.contains(key)) { // 옵션키에도 존재하지 않고 필수키에도 존재하지 않으면 -&gt; 지정된 키가 아니면 예외 발생
                missingKeys.add(key);
            }
        }
        if (!missingKeys.isEmpty()) {
            throw new JobParametersInvalidException(
                    &quot;The JobParameters contains keys that are not explicitly optional or required: &quot; + missingKeys);
        }

    }

    Collection&lt;String&gt; missingKeys = new HashSet&lt;&gt;();
    for (String key : requiredKeys) {
        if (!keys.contains(key)) { // 필수키가 존재하지 않으면 예외 발생
            missingKeys.add(key);
        }
    }
    if (!missingKeys.isEmpty()) { 
        throw new JobParametersInvalidException(&quot;The JobParameters do not contain required keys: &quot; + missingKeys);
    }

}</code></pre>
<p><img src="https://velog.velcdn.com/images/fire_dev/post/5b42f3b5-395f-4ce5-9f9a-f856fd43da4b/image.png" alt="">
필수키를 인자로 넘겨주지 않으면 예외를 발생시키고 배치가 종료되는 것을 확인할 수 있었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Resilience4j Circuit Breaker 적용하여 장애 대응하기 (+ Custom Predicate구현, 네이밍 규칙 변경)]]></title>
            <link>https://velog.io/@fire_dev/resilience4j-Circuit-Breaker-%EC%A0%81%EC%9A%A9</link>
            <guid>https://velog.io/@fire_dev/resilience4j-Circuit-Breaker-%EC%A0%81%EC%9A%A9</guid>
            <pubDate>Sun, 08 Dec 2024 17:04:26 GMT</pubDate>
            <description><![CDATA[<h2 id="resilience4j-란">Resilience4j 란?</h2>
<p>함수형 프로그래밍으로 설계된 경량(lightweight) 장애 허용(fault tolerance) 라이브러리이며 기존 Netflix Hystrix를 대체하는 라이브러리입니다.</p>
<p><strong>Circuit Breaker, Rate Limiter, Retry, Bulkhead</strong> 등의 방식을 사용하고
이 방식들을 _데코레이터 패턴_으로 사용할 수 있도록 제공하여 필요한 방식만 직접 선택하여 사용할 수 있습니다.</p>
<h2 id="resilience4j-핵심-모듈">Resilience4j 핵심 모듈</h2>
<h3 id="1-circuit-breaker">1 Circuit Breaker</h3>
<p>호출이 실패하거나 타임아웃 상황에 <strong>Circuit Breaker</strong> 를 열어서 차단하는 방식
<img src="https://velog.velcdn.com/images/fire_dev/post/6d1a7b5d-603a-476c-9f71-03d683deddb9/image.png" alt=""></p>
<p><strong>circuit breaker 상태</strong></p>
<ul>
<li>CLOSED : 서킷브레이커가 닫혀 있는 상태로 서킷브레이커가 감싼 내부의 프로세스로 요청을 보내고 응답을 받을 수 있다.</li>
<li>OPEN : 서킷브레이커가 열려 있는 상태로 서킷브레이커는 내부의 프로세스로 요청을 보내지 않는다.</li>
<li>HALF_OPEN : 서킷브레이커가 열려 있는 상태지만 내부의 프로세스로 요청을 보내고 실패율을 측정해 상태를 CLOSED 혹은 OPEN 상태로 변경한다.<br>

</li>
</ul>
<p>호출 결과를 저장하고 집계할 때 <strong>슬라이딩 윈도우 방식</strong>을 사용한다</p>
<ul>
<li>개수 기반 슬라이딩 윈도우(count-based sliding window)</li>
<li>시간 기반 슬라이딩 윈도우(time-based sliding window)</li>
</ul>
<br>

<h4 id="동작-시나리오">동작 시나리오</h4>
<ol>
<li><p>실패 비율(failure rate) 또는 느린 호출(slow call) 이 설정한 임계치보다 크거나 같으면 CircuitBreaker의 상태는 Closed → Open 으로 변경된다</p>
<ul>
<li>기본적으로 모든 예외를 실패로 간주하고 실패로 간주할 예외 리스트를 정의할 수도 있다</li>
<li>실패 비율/느린 호출 비율을 계산하기 위해 호출 결과를 최소치는 기록한 상태여야 한다.</li>
<li>CircuitBreaker가 Open 상태이면 CallNotPermittedException 을 던져 호출을 반려한다.</li>
</ul>
</li>
<li><p>대기 시간이 경과하고 나면 OPEN → HALF_OPEN 상태로 변경되며 설정한 횟수만큼 호출을 허용해 이 백엔드가 아직도 이용 불가능한지, 아니면 사용 가능한 상태로 돌아왔는지 확인한다.</p>
<ul>
<li>허용한 호출을 모두 완료할때 까지는 그 이상의 호출은 CallNotPermittedException으로 거부된다.</li>
</ul>
</li>
<li><p>실패 비율이나 느린호출 비율이 설정한 임계치보다 크거나 같으면 상태는 다시 OPEN으로 변경되고 둘 모두 임계치 미만이며 CLOSED 상태로 돌아간다.</p>
</li>
</ol>
<h3 id="2-rate-limiter">2 Rate Limiter</h3>
<p>제한치를 넘어간 것을 감지했을 때의 동작이나 제한할 요청 타입과 관련된 광범위한 옵션을 제공한다.
간단히 제한치를 넘어선 요청을 거부하거나 큐를 만들어서 나중에 실행할 수도 있고, 어떤 방식으로든 두 정책을 조합해도 된다.</p>
<h3 id="3-retry">3 Retry</h3>
<p>실패한 실행을 짧은 지연을 가진 후 재시도하는 매커니즘이다</p>
<h3 id="4-bulkhead">4 Bulkhead</h3>
<p>동시 실행 횟수를 제한하는데 활용할 수 있는 패턴이다</p>
<ol>
<li>세마 포어를 사용하는 SemaphoreBulkhead<ul>
<li>동시 요청 수를 제한을 두고 요청 수에 도달한 이후 요청에 대해서 BulkheadFullException 발생</li>
</ul>
</li>
<li>유한 큐와 고정 스레드 풀을 사용하는 FixedThreadPoolBulkhead<ul>
<li>시스템 자원과 별도로 thread pool을 설정하고 설정된 thread pool은 서비스를 제공하기 위한 용도로만 사용</li>
<li>thread pool과 별도로 waiting queue를 설정할 수 있다. 만약 thread pool과 waiting queue 가 full 인 경우 BulkheadFullException이 발생</li>
</ul>
</li>
</ol>
<br>



<h2 id="resilience4j-적용하기">Resilience4j 적용하기</h2>
<p><del>feign client에 circuit breaker 방식을 적용하였습니다</del></p>
<h3 id="📌-dependency추가-pomxml-사용-기준">📌 dependency추가 (pom.xml 사용 기준)</h3>
<p>아래 spring-cloud-starter-circuitbreaker-resilience4j 를 pom.xml에 추가합니다</p>
<pre><code class="language-xml">&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;
    &lt;artifactId&gt;spring-cloud-starter-circuitbreaker-resilience4j&lt;/artifactId&gt;
&lt;/dependency&gt;</code></pre>
<br>

<p>그 외에 기능을 사용하고 싶으면 아래 디펜던시를 선택해서 추가하면 됩니다</p>
<pre><code class="language-xml">&lt;dependency&gt;
    &lt;groupId&gt;io.github.resilience4j&lt;/groupId&gt;
    &lt;artifactId&gt;resilience4j-circuitbreaker&lt;/artifactId&gt;
    &lt;version&gt;${resilience4jVersion}&lt;/version&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
    &lt;groupId&gt;io.github.resilience4j&lt;/groupId&gt;
    &lt;artifactId&gt;resilience4j-ratelimiter&lt;/artifactId&gt;
    &lt;version&gt;${resilience4jVersion}&lt;/version&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
    &lt;groupId&gt;io.github.resilience4j&lt;/groupId&gt;
    &lt;artifactId&gt;resilience4j-retry&lt;/artifactId&gt;
    &lt;version&gt;${resilience4jVersion}&lt;/version&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
    &lt;groupId&gt;io.github.resilience4j&lt;/groupId&gt;
    &lt;artifactId&gt;resilience4j-bulkhead&lt;/artifactId&gt;
    &lt;version&gt;${resilience4jVersion}&lt;/version&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
    &lt;groupId&gt;io.github.resilience4j&lt;/groupId&gt;
    &lt;artifactId&gt;resilience4j-cache&lt;/artifactId&gt;
    &lt;version&gt;${resilience4jVersion}&lt;/version&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
    &lt;groupId&gt;io.github.resilience4j&lt;/groupId&gt;
    &lt;artifactId&gt;resilience4j-timelimiter&lt;/artifactId&gt;
    &lt;version&gt;${resilience4jVersion}&lt;/version&gt;
&lt;/dependency&gt;</code></pre>
<br>

<h3 id="📌-yml-파일-설정-추가">📌 yml 파일 설정 추가</h3>
<p>임의로 설정한 값들입니다.</p>
<pre><code class="language-yml">resilience4j:
  circuitbreaker:
    configs:
      default:
        failureRateThreshold: 20 # 실패율 임계값 해당 값 이상이면 closed -&gt; open으로 상태변경
        permittedNumberOfCallsInHalfOpenState: 10 # half_open 상태일 때 허용할 call 개수
        maxWaitDurationInHalfOpenState: 10000 # half_open 상태에서 open 상태로 변경되기 전 최대 유지시간 10초
        slidingWindowType: COUNT_BASED # 슬라이딩 윈도우 타입 (COUNT_BASED / TIME_BASED)
        slidingWindowSize: 50 # count_based - 개수 time_based - 초
        minimumNumberOfCalls: 10  # 최초 failureRate, slowCallRate를 계산하기 위한 최소 Call 개수
        waitDurationInOpenState: 60000 # open -&gt; half open 상태로 변경 대기 시간
        record-failure-predicate: com.test.config.resilience4j.TimeOutExceptionRecordFailurePredicate</code></pre>
<br>

<table>
<thead>
<tr>
<th>property</th>
<th>default value</th>
<th>description</th>
</tr>
</thead>
<tbody><tr>
<td>failureRateThreshold</td>
<td>50</td>
<td>실패한 호출에 대한 임계값으로 이 값을 초과하면 서킷이 열린다.</td>
</tr>
<tr>
<td>slowCallDurationThreshold</td>
<td>60000 [ms]</td>
<td>호출 시간이 설정한 값보다 길면 slow call로 판단한다.</td>
</tr>
<tr>
<td>slowCallRateThreshold</td>
<td>100</td>
<td>slow call 비율이 설정한 값보다 크거나 같으면 OPEN 상태로 바뀌고 호출을 차단한다.</td>
</tr>
<tr>
<td>minimumNumberOfCalls</td>
<td>100</td>
<td>failure/slow rate을 계산하기 위한 최소 호출 수</td>
</tr>
<tr>
<td>waitDurationInOpenState</td>
<td>60000 [ms]</td>
<td>OPEN → HALF_OPEN 으로 변경 되기까지 대기 시간</td>
</tr>
<tr>
<td>recordExceptions</td>
<td>empty</td>
<td>서킷 작동에 영향을 주는 예외 리스트</td>
</tr>
<tr>
<td></td>
<td></td>
<td>리스트에 등록된 예외 중에서만 서킷 작동에 영향을 준다.</td>
</tr>
<tr>
<td>ignoreExceptions</td>
<td>empty</td>
<td>서킷 작동에 영향을 주지 않는 예외 리스트</td>
</tr>
<tr>
<td>slidingWindowType</td>
<td>COUNT_BASED</td>
<td>서킷브레이커가 닫힐 때 호출 결과를 기록하는데 사용하는 슬라이딩 윈도우의 유형</td>
</tr>
</tbody></table>
<h3 id="maxwaitdurationinhalfopenstate-설정-주의-사항">maxWaitDurationInHalfOpenState 설정 주의 사항</h3>
<p><strong>waitDurationInOpenState, permittedNumberOfCallsInHalfOpenState 작동 방식</strong></p>
<ol>
<li>요청 50개중에 20%이상 실패나면 써킷 작동(open)한다.</li>
<li>써킷 작동되면 <strong>waitDurationInOpenState</strong>(1분, default) 소요되면 half open으로 상태 변경된다.</li>
<li>half open 상태에서 <strong>permittedNumberOfCallsInHalfOpenState</strong>(10)개 만큼 수행후 성공하면 써킷 미작동(close)한다.</li>
<li>Half open 상태에서  <strong>permittedNumberOfCallsInHalfOpenState</strong>(10)개 만큼 수행후 실패하면 써킷 작동(open)한다.</li>
</ol>
<p>❗️❗️ <strong>만약 Half open을 허용하는 최대 유지시간(maxWaitDurationInHalfOpenState)이 지나면 permittedNumberOfCallsInHalfOpenState(10개)를 수행하기 전에 open 상태로 변경됩니다.</strong></p>
<p>최대 유지시간보다 API 응답 대기 시간이 길다면 circuit이 무한으로 open 상태를 유지하는 상황에 빠질 수 있으므로 참고하여 maxWaitDurationInHalfOpenState을 선정해야 합니다.</p>
<p>maxWaitDurationInHalfOpenState값을 0(default)으로 지정하면permittedNumberOfCallsInHalfOpenState 수만큼 api 호출하여 판단하므로 default값을 유지하도록 하였습니다.</p>
<blockquote>
<p>maxWaitDurationInHalfOpenState 설정 값만큼 대기한 이후
<img src="https://velog.velcdn.com/images/fire_dev/post/c45ecf5b-3699-4f3f-a0f7-a631c18b16f2/image.png" alt="">
최소 호출 수만큼 요청을 받기 전에 maxWaitDurationInHalfOpenState에 설정된 값만큼 시간이 지나면 Circuit Breaker는 OPEN 상태로 전환합니다.
해당 시간 동안 받은 응답의 결과와 상관없이 무조건 OPEN 상태로 전환합니다.
출처) <a href="https://meetup.nhncloud.com/posts/385">https://meetup.nhncloud.com/posts/385</a></p>
</blockquote>
<br>

<h3 id="📌-custom-predicate-설정">📌 Custom Predicate 설정</h3>
<p>특정 exception이 실패로 측정되도록 하는 Custom Predicate를 설정할 수 있습니다.
Predicate은 실패로 측정되고자 하는 exception은 true로, 성공으로 측정되고자 하는 경우는 false를 리턴해야 한다. 
기본값에서는 모든 exception이 실패로 기록되지만 현 프로젝트에선 time-out이 발생하는 케이스만 오류로 측정하도록 하는 요구가 있었습니다.</p>
<pre><code class="language-java">public class TimeOutExceptionRecordFailurePredicate implements Predicate&lt;Throwable&gt; {

    @Override
    public boolean test(Throwable t) {
        // RetryableException 예외이고, timed out 발생시에만 에러로 인식
        if (t instanceof RetryableException &amp;&amp; t.getMessage().startsWith(&quot;Read timed out&quot;)) {
            return true;
        } else if (t instanceof FeignException.GatewayTimeout) {
            return true;
        }
        return false;
    }
}</code></pre>
<p>이와 같은 <code>Predicate&lt;Throwable&gt;</code>을 구현한 클래스를 생성하고 원하는 특정 예외인 경우 true값을 반환하도록 <code>test 메서드</code>를 오버라이드 하면 됨!</p>
<pre><code class="language-yml">record-failure-predicate: com.test.config.resilience4j.TimeOutExceptionRecordFailurePredicate</code></pre>
<p>위의 설정 yml에서 볼 수 있듯이 생성한 클래스를 record-failure-predicate 속성에 넣어주면 설정 완료 됩니다.</p>
<h3 id="📌-feign-client에-circuitbreaker-어노테이션-추가">📌 feign client에 CircuitBreaker 어노테이션 추가</h3>
<pre><code class="language-java">@FeignClient(name = &quot;testClient&quot;, url = &quot;tesetUrl&quot;)
public interface CommandClient {
    @CircuitBreaker(name = &quot;CommandClient#saveItem&quot;)
    @PostMapping(&quot;/item&quot;)
    ApiResponseDto&lt;List&lt;ItemResDto&gt;&gt; saveItem(@RequestBody List&lt;ItemReqDto&gt; itemReqDtos) {
    ...
}</code></pre>
<br>


<pre><code>@CircuitBreaker(name = &quot;{circuitBreaker명}&quot;)

@CircuitBreaker(name = &quot;{circuitBreaker명}&quot;, fallbackMethod = &quot;{fallbackMethod 명}&quot;)</code></pre><p>fallback Method를 지정할수도 있고 생략할 수도 있습니다.</p>
<ul>
<li>fallback method를 사용하면 circuitBreaker가 OPEN 상태로 변경되어 CallNotPermittedException 발생시 fallback 함수가 호출됩니다.</li>
<li>fallback method를 설정하지 않으면 CallNotPermittedException가 발생시 처리되는 로직이 없거나(단순 예외 발생)</li>
<li>callNotPermittedException을 받는 ExceptionHandler가  있으면 여기서 에러를 캐치합니다.</li>
</ul>
<br>

<h3 id="fallback-구현-관련-참고-사항">fallback 구현 관련 참고 사항</h3>
<ul>
<li>fallback 메서드는 어노테이션 적용 위치와 동일한 위치에 있어야 합니다.</li>
<li>그래서 feign client같은 interface에서는 default 메서드를 구현하거나</li>
<li>interface를 감싸는 wrapper class를 만들어서 구현하는 방식이 있고 또는 <a href="https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/#spring-cloud-feign-circuitbreaker-fallback">fallback class를 구현</a>해서 적용하는 방식도 있습니다.
 <br>



</li>
</ul>
<h3 id="circuitbreaker-이름-관련-참고-사항">circuitBreaker 이름 관련 참고 사항</h3>
<p>circuitBreaker명은 <strong>클래스명#메서드명</strong>으로 설정했습니다.</p>
<p>원래 feign client에 circuitBreaker를 사용한다는 설정을 yml에 넣어두면 feign에서 자동으로 인식하여 메서드 별로 이름을 자동 매핑합니다. (DefaultCircuitBreakerNameResolver가 사용됨)</p>
<pre><code>feign.client.circuitbreker.enabled = true 설정

Spring-Cloud-OpenFeign 4.0.0-SNAPSHOT 버전부터는 
spring.cloud.openfeign.circuitbreaker.enabled</code></pre><p>설정을 넣으면** @CircuitBreaker** 어노테이션을 사용하지 않아도 
<strong>feignClientClassName#calledMethod(parameterTypes)</strong> 패턴으로 자동 네임이 생성됩니다.</p>
<br>
그러나.. 기존 Prometheus와 출동이 발생하였고 네이밍 규칙을 변경하기로 하였습니다.

<pre><code>java.lang.IllegalArgumentException: Prometheus requires that all meters with the same name have the same set of tag keys.

There is already an existing meter named &#39;resilience4j_circuitbreaker_state&#39; containing tag keys [name, state]. The meter you are attempting to register has keys [group, name, state].</code></pre><br>

<h3 id="circuitbreaker-네이밍-규칙을-변경하는-방법">CircuitBreaker 네이밍 규칙을 변경하는 방법</h3>
<p><a href="https://github.com/spring-cloud/spring-cloud-openfeign/blob/main/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/CircuitBreakerNameResolver.java">CircuitBreakerNameResolver 인터페이스</a>를 feign에서 가져와서 네이밍 규칙을 변경할 수 있습니다.
<a href="https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/#spring-cloud-feign-circuitbreaker">spring-docs 문서 참고</a>하면 네이밍 규칙을 feignClientName_calledMethod의 형태로 다음과 같이 변경할 수 있습니다.</p>
<pre><code class="language-java">@Configuration
public class FooConfiguration {
    @Bean
    public CircuitBreakerNameResolver circuitBreakerNameResolver() {
        return (String feignClientName, Target&lt;?&gt; target, Method method) -&gt; feignClientName + &quot;_&quot; + method.getName();
    }
}</code></pre>
<br>




<br>

<h3 id="📌-test용-controller-추가">📌 test용 controller 추가</h3>
<p>CircuitBreakerRegistry는 설정값을 저장하는 인메모리 저장소라고 생각하면 됩니다 </p>
<pre><code class="language-java">@RestController
@RequiredArgsConstructor
@Slf4j
public class CircuitBreakerTestController {
    private final MarketCollectCommandClient feign;
    private final CircuitBreakerRegistry circuitBreakerRegistry;

    @GetMapping(&quot;/circuit/call&quot;) // feign client 호출
    public ResponseEntity&lt;List&lt;ItemResDto&gt;&gt; call(@RequestBody List&lt;ItemDto&gt; itemDtos) {
        List&lt;ItemResDto&gt; response = feign.saveItem(itemDtos).getData();
        return ResponseEntity.ok(response);
    }

    @GetMapping(&quot;/circuit/close&quot;) // circuitBreaker close로 상태 변경
    public ResponseEntity&lt;Void&gt; close(@RequestParam String name) {
        circuitBreakerRegistry.circuitBreaker(name)
                .transitionToClosedState();
        return ResponseEntity.ok().build();
    }

    @GetMapping(&quot;/circuit/open&quot;) // circuitBreaker open으로 상태 변경
    public ResponseEntity&lt;Void&gt; open(@RequestParam String name) {
        circuitBreakerRegistry.circuitBreaker(name)
                .transitionToOpenState();
        return ResponseEntity.ok().build();
    }

    @GetMapping(&quot;/circuit/status&quot;) // circuitBreaker 상태 확인
    public ResponseEntity&lt;CircuitBreaker.State&gt; status(@RequestParam String name) {
        CircuitBreaker.State state = circuitBreakerRegistry.circuitBreaker(name)
                .getState();
        return ResponseEntity.ok(state);
    }

    @GetMapping(&quot;/circuit/all&quot;) // circuitBreaker 상태 확인
    public ResponseEntity&lt;Void&gt; all() {
        Seq&lt;CircuitBreaker&gt; circuitBreakers = circuitBreakerRegistry.getAllCircuitBreakers();
        for (CircuitBreaker circuitBreaker : circuitBreakers) {
            log.error(&quot;circuitName={}, state={}&quot;, circuitBreaker.getName(), circuitBreaker.getState());
        }
        return ResponseEntity.ok().build();
    }

    @ExceptionHandler(FeignException.class)
    public ResponseEntity&lt;?&gt; handleFeignException(FeignException e) {
        return ResponseEntity.badRequest()
                .body(Collections.singletonMap(&quot;code&quot;, &quot;FeignException&quot;));
    }

    @ExceptionHandler(NoFallbackAvailableException.class)
    public ResponseEntity&lt;?&gt; handleNoFallbackAvailableException(NoFallbackAvailableException e) {
        return ResponseEntity.badRequest()
                .body(Collections.singletonMap(&quot;code&quot;, &quot;NoFallbackAvailableException&quot;));
    }

    @ExceptionHandler(CallNotPermittedException.class)
    public ResponseEntity&lt;?&gt; handleCallNotPermittedException(CallNotPermittedException e) {
        return ResponseEntity.badRequest()
                .body(Collections.singletonMap(&quot;code&quot;, &quot;CallNotPermittedException&quot;));
    }

}</code></pre>
<br>
<br>

<h3 id="참고">참고</h3>
<p><a href="https://resilience4j.readme.io/">https://resilience4j.readme.io/</a> 
<a href="https://godekdls.github.io/Resilience4j/contents/">https://godekdls.github.io/Resilience4j/contents/</a> 
<a href="https://spring.io/projects/spring-cloud-circuitbreaker">https://spring.io/projects/spring-cloud-circuitbreaker</a> 
<a href="https://docs.spring.io/spring-cloud-circuitbreaker/docs/current/reference/html/">https://docs.spring.io/spring-cloud-circuitbreaker/docs/current/reference/html/</a> 
<a href="https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/#spring-cloud-feign-circuitbreaker">https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/#spring-cloud-feign-circuitbreaker</a>
<a href="https://arnoldgalovics.com/spring-cloud-feign-resilience4j-testing/">https://arnoldgalovics.com/spring-cloud-feign-resilience4j-testing/</a>
<a href="https://mangkyu.tistory.com/289">https://mangkyu.tistory.com/289</a>
<a href="https://godekdls.github.io/Resilience4j/introduction/">https://godekdls.github.io/Resilience4j/introduction/</a> 
<a href="https://meetup.nhncloud.com/posts/385">https://meetup.nhncloud.com/posts/385</a></p>
]]></description>
        </item>
    </channel>
</rss>