7장. 나쁜 상황에 대비한 스프링 클라우드와 Resilience4j를 사용한 회복성 패턴
- 다루는 핵심내용
- 회로 차단기, 폴백, 벌크헤드 구현
- 클라이언트 자원을 절약하기 위한 회로 차단기 패턴 사용
- 원격 서비스 호출에 실패할 때는 Resilience4j 사용
- 원격 자원 호출을 분리하기 위한 Resilience4j 벌크헤드 패턴 구현
- Resilience4j 회로 차단기와 벌크헤드 구현 튜닝
- Resilience4j 동시성 맞춤화 전략
- 서비스가 망가지면 쉽게 감지할 수 있고, 애플리케이션은 이를 우회할 수 있다.
하지만 서비스가 느려진다면, 성능 저하를 감지하고 우회하는 일은 다음 이유로 매우 어렵다.
- 서비스 성능 저하는 간헐적으로 시작되어 확산될 수 있다.
- 서비스 저하도 작은 곳에서 갑자기 발생할 수 있다.
- 순식간에 애플리케이션 컨테이너의 스레드 풀이 완전히 소진되고 붕괴되기 전까지, 실패의 첫 징후는 소규모 사용자가 문제에 대해 불평하는 정도로 나타날 수 있다.
- 원격 서비스 호출은 대개 동기식이며, 장기간 수행되는 호출을 중단하지 않는다.
- 일반적으로 애플리케이션 개발자는 작업을 수행하려고 서비스를 호출하고 결과를 기다린다.
- 호출자에게는 서비스 호출이 행(hanging)되는 것을 방지하는 타임아웃 개념이 없다.
- 대개 원격 자원의 부분적인 저하가 아닌, 완전한 실패를 처리하도록 애플리케이션을 설계한다.
- 서비스가 완전히 실패하지 않는 한, 애플리케이션은 계속해서 불량한 서비스를 호출하고 빠르게 실패하지 못하는 경우가 많다.
- 이때 호출하는 애플리케이션이나 서비스는 정상적으로 성능이 저하될 수도 있지만, 자원 고갈로 고장 날 가능성이 더 높다.
- 자원 고갈(resource exhaustion)이란, 스레드 풀이나 데이터베이스 커넥션 같은 제한된 자원이 초과 사용되어 호출 클라이언트가 자원이 다시 가용해질 때까지 대기해야 하는 상황이다.
- 성능이 나쁜 원격 서비스가 야기하는 문제를 간과할 수 없는 것은, 이를 탐지하기 어려울 뿐만 아니라 전체 애플리케이션 생태계에 파급되는 연쇄효과를 유발할 수 있기 때문이다.
- 회복성 패턴은 마이크로서비스 아키텍처에서 가장 중요한 요소 중 하나이다.
7.1 클라이언트 측 회복성이란?
- 클라이언트 측 회복성 소프트웨어 패턴들은 에러나 성능 저하고 원격 자원이 실패할 때,
원격 자원의 클라이언트가 고장 나지 않게 보호하는 데 중점을 둔다.
- 클라이언트가 빨리 실패하고, 데이터베이스 커넥션과 스레드 풀 같은 소중한 자원을 소비하는 것을 밪이할 수 있다.
- 제대로 성능이 낮은 원격 서비스 문제가 소비자에게 ‘상향(upstream)’으로 확산되는 것을 막는다.

7.1.1 클라이언트 측 로드 밸런싱
- 스프링 클라우드 로드 밸런서(Spring Cloud Load Balancer) 라이브러리
7.1.2 회로 차단기
- 회로 차단기 패턴(circuit breaker pattern)은 전기 회로의 차단기를 모델링했다.
- 과전류가 흐르는지 탐지
- 문제를 탐지하면 나머지 전기 시스템의 연결을 끊고, 하부 구성 요소가 타 버리지 않도록 보호한다.
- 소프트웨어 회로 차단기는 원격 서비스가 호출될 때 호출을 모니터링한다.
- 호출이 오래 걸리면, 차단기가 개입해서 호출을 종료한다.
- 회로 차단기 패턴은 원격 자원에 대한 모든 호출을 모니터링하고,
호출이 충분히 실패하면 회로 차단기 구현체가 열리면서(pop) 빠르게 실패하고 고장난 원격 자원에 대한 추가 호출을 방지한다.
7.1.3 폴백 처리
- 폴백 패턴(fallback pattern)을 사용하면 원격 서비스 호출이 실패할 때 예외(exception)를 생성하지 않고,
서비스 소비자가 대체 코드 경로를 실행하여 다른 수단을 통해 작업을 수행할 수 있다.
- 보통 다른 데이터 소스에서 데이터를 찾거나, 향후 처리를 위해 사용자 요청 큐(queue)에 입력하는 작업이 포함된다.
- 사용자 호출에 문제가 있다고 예외를 표시하지는 않지만, 나중에 요청을 시도해야 한다고 알려 줄 수 있다.
- 예시
- 사용자 행동 양식을 모니터링하고 구매 희망 항목을 추천하는 기능을 제공하는 전자 상거래 사이트
- 일반적으로 마이크로서비스를 호출하여 사용자 행동을 분석하고, 특정 사용자에게 맞춤화된 추천 목록을 반환한다.
- 하지만 추천목록 제공 서비스가 실패하면, 폴백은 모든 사용자의 구매 정보를 기반으로 더욱 일반화된 기호 목록을 검색할 수 있다.
- 그리고 이 데이터는 완전히 다른 서비스와 데이터 소스에서 추출될 수 있다.
7.1.4 벌크 헤드
- 벌크헤드(bulkhead) 패턴은 선박을 건조하는 개념에서 유래되었다.
- 배는 격벽이라는 완전히 격리된 수밀 구획으로 나뉘는데, 선체에 구멍이 뚫려도 침수 구역을 구멍이 난 격벽으로만 제한하므로, 배 전체에 물이 차서 침몰되는 것을 방지한다.
- 벌크헤드 패턴을 사용할 때, 원격 자원에 대한 호출을 ‘자원별 스레드 풀’로 분리하면
느린 원격 자원 호출 하나로 발생한 문제가, 전체 애플리케이션을 다운시킬 위험을 줄일 수 있다.
- 스레드 풀은 서비스의 벌크헤드(격벽) 역할을 한다.
- 각 원격 자원을 분리하여, 스레드 풀에 각각 할당한다.
- 한 서비스가 느리게 응답한다면, 해당 서비스의 호출 그룹에 대한 스레드 풀만 포화되어 요청 처리를 중단하게 될 수 있다.
- 스레드 풀 별로 서비스를 할당하면, 다른 서비스는 포화되지 않기 때문에 이러한 병목 현상을 우회하는 데 유용하다.
7.2 클라이언트 회복성이 중요한 이유
-
장애 예시
- 자기 데이터베이스에 대한 쓰기와 서비스에서 읽기가 동일한 트랜잭션 안에서 수행되는 코드를 작성
- 서비스가 느리게 실행되기 시작면, 서비스 요청에 대한 스레드 풀이 쌓이기 시작할 뿐 아니라, 서비스 컨테이너의 커넥션 풀에 있는 데이터베이스 커넥션 풀 수도 고갈된다.
- 이것은 서비스에 대한 호출이 완료되지 않아 커넥션이 사용 중이기 때문에 발생하는 현상이다.
- 회로 차단기 패턴이 부산 자원이 호출되는 곳(데이터 베이스 호출 또는 서비스 호출하는 곳 모두)에 구현되었다면 이 모든 시나리오를 피할 수 있었다.
-
회로 차단기를 구현했다면, 해당 서비스가 제대로 수행되지 못하기 시작했을 때
해당 호출에 대한 회로 차단기가 작동해서 스레드를 소모하지 않고 빠르게 실패했을 것이다.
- 여러 엔드포인트가 있다면, 서비스에 대한 특정 호출과 연관된 엔드포인트만 영향을 받을 것이다.
- 나머지 기능은 온전히 유지되어 클라이언트 요청을 수행할 수 있다.
-
회로 차단기는 애플리케이션과 원격 서비스 사이에서 중개자 역할을 한다는 것을 기억하라.
-
예시

- 라이선싱 서비스는 조직 서비스를 직접 호출하지 않는다.
그 대신 호출되면 라이선싱 서비스는 회로 차단기에 서비스에 대한 실제 호출을 위임한다.
- 회로 차단기는 해당 호출을 스레드(대개 스레드로 풀에서 관리되는)로 래핑(wrapping)한다.
- 호출을 래핑하면 클라이언트는 더 이상 호출이 완료되길 직접 기다리지 않아도 된다.
회로 차단기가 스레드를 모니터링하고 너무 오래 실행된다면, 호출을 종료할 수 있다.
- 세 가지 시나리오
- 첫 번째, 정상 시나리오(the happy path)
- 회로 차단기는 타이머를 설정하고, 타이머가 만료되기 전에 원격 호출이 완료되면, 라이선싱 서비스는 정상적으로 모든 작업을 계속 수행할 수 있다.
- 두 번째, 서비스 성능 저하 시나리오
- 라이선싱 서비스는 회로 차단기를 통해 조직 서비스를 호출한다.
- 하지만 조직 서비스가 느리게 실행되어 회로 차단기가 관리하는 스레드 타이머가 만료되기 전에 호출이 완료되지 않으면,
회로 차단기는 원격 서비스에 대한 연결을 종료하고, 라이선싱 서비스는 호출 오류를 반환한다.
- 라이선싱 서비스는 (조직 서비스 호출이 완료되길 기다리기 위해서) 자원(자체 스레드 및 커넥션 풀)을 점유하지 않는다.
- 또한 조직 서비스에 대한 호출 시간이 만료되면, 회로 차단기는 발생한 실패 횟수를 추적하기 시작하는데,
특정 시간 동안 서비스에서 오류가 필요 이상으로 발생하면 회로 차단기는 회로를 ‘차단(trip)’하고 조직 서비스에 대한 모든 호출은 조직 서비스 호출 없이 실패한다.
- 세 번째, 빠르게 실패 or 대체 코드(폴백)
- 라이선싱 서비스는 회로 차단기의 타임아웃을 기다릴 필요 없이, 문제가 있다는 것을 즉시 알 수 있다.
- 완전히 실패하거나, 대체 코드(폴백)을 사용하여 조치하는 것에서 선택할 수 있다.
- 회로 차단기가 차단되면 라이선싱 서비스가 조직 서비스를 호출하지 않았기 때문에, 조직 서비스는 회복할 수 있는 기회가 생긴다.
- 이것으로 조직 서비스는 약간의 여유가 생기므로, 서비스 저하가 발생할 때 연쇄 장애를 방지하는 데 도움이 된다.
-
회로 차단기는 때때로 저하된 서비스에 호출을 허용하는데,
이 호출이 연속적으로 필요한 만큼 성공하면 회로 차단기를 스스로 재설정한다.
-
원격 호출에 대해 회로 차단기 패턴이 제공하는 주요 이점
7.3 Resilience4j 구현
- Resilience4j는 넷플릭스-히스트릭스에서 영감을 받은 내결함성 라이브러리다.
- 네트워크 문제나 여러 서비스의 고장으로 발생하는 결함 내성을 높이기 위해 다음 패턴을 제공한다.
- Resilience4j를 사용하면, 메서드에 여러 애너테이션을 정의하여 동일한 메서드 호출에 여러 패턴을 적용할 수 있다.
- Resilience4j의 재시도 순서에서 주목할 사항은 다음과 같다.
- 스프링 부트와 Resilience4j 라이브러리를 사용하면, 여러 마이크로서비스 아키텍처에서 항상 사용되는 검증된 도구를 제공할 수 있다.
7.4 스프링 클라우드와 Resilience4j를 사용하는 라이선싱 서비스 설정
7.5 회로 차단기 구현
- Resilience4j의 회로 차단기에는 세 개의 일반 상태를 가진 유한 상태 기계가 구현되어 있다.
- Resilience4j에서는 추가 상태를 정의할 수 있다.
다음 상태를 벗어나는 유일한 방법은 회로 차단기를 재설정하거나 상태 전환을 트리거하는 것이다.
- Resilience4j는 모든 원격 자원 호출 사이에 위치하여 클라이언트를 보호하고,
원격 자원이 데이터베이스나 REST 기반 서비스를 호출하는지는 중요하지 않다.
7.5.1 회로 차단기 추가
@CircuitBreaker(name = "licenseService")
public List<License> getLicensesByOrganization(String organizationId) throws TimeoutException {
return licenseRepository.findByOrganizationId(organizationId);
}
7.5.2 회로 차단기 사용자 정의(설정)
resilience4j.circuitbreaker:
instances:
licenseService: # 라이선싱 서비스의 인스턴스 구성(회로 차단기 애너테이션에 전달되는 이름과 동일)
registerHealthIndicator: true # 상태 정보 엔드포인트에 대한 구성 정보 노출 여부 설정
ringBufferSizeInClosedState: 5 # 링 버퍼의 닫힌 상태 크기를 설정한다.
ringBufferSizeInHalfOpenState: 3 # 링 버퍼의 반열린 상태 크기를 설정한다.
waitDurationInOpenState: 10s # 열린 상태의 대기 시간을 설정한다
failureRateThreshold: 50 # 실패율 임계치를 백분율(%)로 설정한다.
recordExceptions: # 실패로 기록될 예외를 설정한다.
- org.springframework.web.client.HttpServerErrorException
- java.io.IOException
- java.util.concurrent.TimeoutException
- org.springframework.web.client.ResourceAccessException
organizationService: # 조직 서비스의 인스턴스 구성(회로 차단기 애너테이션에 전달되는 이름과 동일)
registerHealthIndicator: true
ringBufferSizeInClosedState: 6
ringBufferSizeInHalfOpenState: 4
waitDurationInOpenState: 20s
failureRateThreshold: 60