간단해보이는 줄로만 알았던 CircuitBreak 를 적용하는 과정에서 겪은 문제와 해결 방안을 기록하려 한다.
1. 문제의 발단
resilience4j 의존성을 추가해준다.
CircuitBreaker를 이용해서 FeignClient 를 호출할 것이기 때문에 위 설정 정보를 yml 파일에 추가해주었다.
Resilience 설정 클래스를 만들어 위와 같이 설정해주었다. 간단히 설명하자면
failureRateThreshold: 실패 비율을 말한다. (정해진 n개의 요청 중 이 비율 이상의 실패를 가지게되면 OPEN 상태로 전환한다.)
waitDurationInOpenState : OPEN 상태에서 HALF-OPEN 상태로 전환되는 딜레이를 나타낸다.
slidingWindowType: 실패를 측정할 때 사용되는 수치를 말한다. COUNT_BASED 로 하게되면 요청 횟수를 기준으로 실패 비율을 측정하게되고 TIME_BASED 설정을 넣게되면 특정 시간동안의 실패를 기록해두고 그에 따른 비율로 OPEN 여부를 판단한다.
예를 들어 TIME_BASED 로 해두고 시간을 10초로 설정해두었다고 한다면, 10초동안 들어온 실패 요청의 비율을 확인하고 그게 failureRateThreshold의 값을 넘어가게 된다면 OPEN 상태로 전환해두게 된다.
slidingWindowSize: slidingWindowType에 따른 크기 값, COUNT_BASED 로 해두면 횟수를 기준으로 비율을 측정하고
TIME_BASED 로 해두면 시간을 기준으로 비율을 측정해둔다. 위에서 10초를 기준으로 설명했는데 그 기준이 되는 값이라 보면 된다.
그리고 CircuitBreakerFactory 를 주입받아서 위와 같이 호출하는 코드를 작성해두었다.
그런데 다음과 같은 오류가 발생했다.
java.lang.NullPointerException: Cannot invoke "org.springframework.web.context.request.ServletRequestAttributes.getRequest()" because the return value of "org.springframework.web.context.request.RequestContextHolder.getRequestAttributes()" is null
원인을 살펴보니
프로젝트에서 사용되었던 위 코드가 문제였다는 것을 알 수 있었다.
이것은 FeignClient 를 호출하기 전에 현재 쓰레드의 Cookie 값을 받아와서 전달하는 역할을 하는 코드이다.
그런데 코드를 보면 RequestContextHolder 를 호출하는 것을 볼 수 있는데, 여기서 RequestContextHolder 는 내부적으로 ThreadLocal 기술을 사용한다.
그리고 CircuitBreaker 는 호출 시 별도의 쓰레드 풀에서 쓰레드를 가져다가 사용하게 된다는 사실을 디버깅을 통해 알 수 있었다.
디버깅한 결과를 보도록 하자.
CircuitBreaker를 호출하기 전에 현재 쓰레드의 이름을 로그로 출력한 후에
CircuitBreaker를 호출하고나서 실행되는 도중에도 로그를 찍도록 해두었다.
그리고 결과를 보면 서로 두 쓰레드의 이름이 다른 것을 확인할 수 있다.
당연히 두 개의 쓰레드가 다르기 때문에 ThreadLocal 기술을 사용하는 RequestContextHolder 는 null 값을 반환했던 것이다.
2. 문제 해결
이 문제를 해결하기 위해 Resilience4J 관련 설정들과 Context 를 전파시키는 방법 등에 대해서 구글링을 해보았지만 그렇다할 해결 방안은 찾지 못했다. 정확히 말하면 Resilience4J 관련 설정을 건드려서 최대한 유연하게 문제를 해결하고 싶었지만 이것을 적용하기란 결코 쉽지 않았다.
그래서 그냥 내 방식대로 문제를 풀어나가기로 했다.
자바에서 제공하는 InheritableThreadLocal 을 사용하면 위 문제를 해결할 수 있다.
이렇게 별도의 CookieHolder 클래스를 만들고 내부에서 InheritableThreadLocal 변수를 선언해둔다.
setCokkie() 메서드를 호출하면 현재 쓰레드의 쿠키 값을 받아와서 저장해두는 것을 볼 수 있다.
그리고 기존 쿠키 값을 전달해주는 클래스의 코드를 수정해준다. InheritableThreadLocal 변수의 값을 가져오라고 해두었기 때문에 부모 쓰레드의 값도 참조할 수 있게 된다.
그리고 실제 FeignClient 를 호출하기 전에 setCookie() 메서드를 호출해서 쿠키 값을 저장해두게 하면 해결 된다.
3. 코드 리팩토링
근데 위 코드들은 조금의 문제가 있다.
매번 FeignClient 를 호출할 때마다 setCookie() 메서드를 호출해주어야 하고 CircuitBreak 를 호출하는 코드도 다소 복잡해보인다. 이 코드를 좀 더 효율적으로 수정해보자.
setCookie() 메서드를 호출하는 AOP 를 만든다. @CookieSet 어노테이션을 기반으로 동작하도록 해준다.
새로운 CircuitClient 클래스를 만들고 기존 FeignClient 인터페이스를 상속받아서 위와 같이 감싸준다.
(여기서 MemberServiceClient 가 FeignClient 이다.)
이제 매번 호출할 때마다 setCookie() 를 호출할 필요가 없어졌다.
그리고 위 코드를 더 간략하게 리팩토링하면
이렇게 어노테이션으로 처리해줄 수가 있다.
이제 기존 FeignClient 를 사용하던 클래스의 타입을 CircuitClient 를 사용하도록 변경해주면 된다.
4. 결과
처음 호출했을 때 2.3초가 걸리며 오류가 발생한다.
그리고 반복된 실패 요청으로 CircuitBreaker 가 열리면 빠른 시간 안에 응답하는 것을 확인할 수 있다.
참고 자료
https://mangkyu.tistory.com/333
https://bottom-to-top.tistory.com/57
https://tweety1121.tistory.com/entry/Spring-circuitbreaker-actuator-health-check-%EC%84%A4%EC%A0%95
https://mangkyu.tistory.com/289
[Spring] OpenFeign에 Resilence4J 서킷 브레이커 적용하는 방법과 예시 및 주의사항
이번에는 Java 진영의 서킷브레이커 라이브러리인 Resilence4J를 OpenFeign에 적용하는 방법에 대해 알아보도록 하겠습니다. 아래의 내용은 공식 문서와 직접 구현 및 테스트한 부분을 바탕으로 작성
mangkyu.tistory.com
Spring circuitbreaker actuator health check 설정
Gradle implementation "org.springframework.cloud:spring-cloud-starter-circuitbreaker-reactor-resilience4j" implementation 'org.springframework.boot:spring-boot-starter-actuator' CircuitBreaker Configuration 빈 등록 @Bean public Customizer defaultCustomi
tweety1121.tistory.com
Resilience4j CircuitBreaker 사용하기
들어가며 Resilience4j는 넷플릭스의 히스트릭스에 영감을 받아 개발된 경량화 Fault Tolerance 라이브러리이다. 그 중 Circuit Breaker(이하 서킷브레이커)를 분석하고 적용하였다. 서킷브레이커의 상태 서
bottom-to-top.tistory.com
[Java] 스레드 로컬(ThreadLocal)과 상속 가능한 스레드 로컬( InheritableThreadLocal)에 대하여
1. 스레드 로컬(ThreadLocal)과 상속 가능한 스레드 로컬( InheritableThreadLocal)에 대하여 [ 스레드 로컬(ThreadLocal)이란? ] 자바는 오랜 기간 동안 동시성 처리를 위해 스레드를 사용해왔다. 대표적으로 스
mangkyu.tistory.com
'기타 > 문제 해결' 카테고리의 다른 글
Spring Security 세션 정책 적용되지 않는 문제 해결 (0) | 2024.12.12 |
---|---|
Spring @WebMvcTest 사용 시 Security 설정 제거하기 (0) | 2024.12.10 |
Redis 를 통한 세션 동기화 (0) | 2024.06.28 |
fegin client GET 메서드 요청 시 오류 해결 (0) | 2024.06.17 |
Feign Client 를 통해 요청 시 쿠키 값 유지하기 (0) | 2024.06.13 |