반응형

개념

Circuit Breaker란?

  • 하나의 서비스가 실패할 때, 그 서비스로의 트래픽을 차단하여 실패가 확산되는 것을 방지하는 패턴을 말한다.
  • 서비스간의 의존성 문제를 해결하고, 전체 시스템의 회복력을 높이기 위해 사용된다.
  • 대표적인 라이브러리로는 resilience4j가 있다. (아래 예제도 resilience4j를 활용하여 진행한다.)

Circuit Breaker의 상태

  • CLOSED: 서비스 호출이 정상적으로 이루어지는 상태. 모든 요청이 통과된다.
  • OPEN: 서비스 호출이 실패하여 트래픽을 차단한 상태. 모든 요청이 즉시 실패로 반환된다.
  • HALF-OPEN: 일정 시간이 지난 후, 서비스가 다시 정상적으로 동작하는지 확인하기 위해 일부 요청을 허용하는 상태. 해당 상태에서 요청이 성공하면 CLOSED, 실패하면 OPEN 상태로 전환된다.

예제

build.gradle.kts

dependencyManagement {
    imports {
        mavenBom("org.springframework.cloud:spring-cloud-dependencies:2023.0.3")
    }
}

dependencies {
    // for resilience4j
    implementation("org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j")

    // 아래 의존성 추가하지 않으면 @CircuitBreaker 어노테이션이 동작하지 않음
    implementation("org.springframework.boot:spring-boot-starter-aop")
}

application.yml

resilience4j:
  circuitbreaker:
    configs:
      default:
        sliding-window-type: count_based                  # 값을 집계하는 방식
        sliding-window-size: 10                           # 윈도우 사이즈
        failure-rate-threshold: 50                        # 실폐율 임계값. 실패율 >= 50%이면 OPEN상태로 전환
        minimum-number-of-calls: 5                        # 집계를 위한 최소 호출 수
        permitted-number-of-calls-in-half-open-state: 2   # HALF_OPEN 상태일 때 허용되는 호출 수
        wait-duration-in-open-state: 30s                  # OPEN -> HALF_OPEN 상태로 전환되는 시간

    instances:
      message-service: # message-service라는 이름의 인스턴스 등록
        base-config: default

MessageService

@Service
class MessageService {
    @CircuitBreaker(name = "message-service", fallbackMethod = "fallbackDemo")
    fun getMessage(name: String): String {
        if (name.isEmpty()) throw IllegalStateException("not supported")
        return "success"
    }

    private fun fallbackDemo(name: String, e: Exception): String {
        LoggerFactory.getLogger(this::class.java).warn("fallback. name: {}", name, e)
        return "fallback"
    }
}

CircuitBreakerTest

@SpringBootTest
class CircuitBreakerTest {
    @Autowired
    private lateinit var messageService: MessageService

    @Autowired
    private lateinit var circuitBreakerRegistry: CircuitBreakerRegistry

    private lateinit var circuitBreaker: CircuitBreaker

    @BeforeEach
    fun setup() {
        circuitBreaker = circuitBreakerRegistry.circuitBreaker("message-service")
    }

    @Test
    fun `CLOSED 상태에서 5번 연속 실패하면 OPEN 상태로 전환`() {
        circuitBreaker.transitionToClosedState()

        (1..4).forEach {
            assertEquals("fallback", messageService.getMessage(""), "count: $it")
            assertEquals(State.CLOSED, circuitBreaker.state, "count: $it")
        }

        assertEquals("fallback", messageService.getMessage(""), "count: 5")
        assertEquals(State.OPEN, circuitBreaker.state, "count: 5")
    }

    @Test
    fun `CLOSED 상태에서 5번 성공 & 5번 실패하면 OPEN 상태로 전환`() {
        circuitBreaker.transitionToClosedState()

        (1..5).forEach {
            assertEquals("success", messageService.getMessage("john"), "count: $it")
            assertEquals(State.CLOSED, circuitBreaker.state, "count: $it")
        }

        (6..9).forEach {
            assertEquals("fallback", messageService.getMessage(""), "count: $it")
            assertEquals(State.CLOSED, circuitBreaker.state, "count: $it")
        }

        assertEquals("fallback", messageService.getMessage(""), "count: 10")
        assertEquals(State.OPEN, circuitBreaker.state, "count: 10")
    }

    @Test
    fun `CLOSED 상태에서 50번 성공해도 CLOSED 상태 유지`() {
        circuitBreaker.transitionToClosedState()

        (1..50).forEach {
            assertEquals("success", messageService.getMessage("john"), "count: $it")
            assertEquals(State.CLOSED, circuitBreaker.state, "count: $it")
        }
    }

    @Test
    fun `HALF-OPEN 상태에서 2번 연속 실패하면 OPEN 상태로 전환`() {
        circuitBreaker.transitionToOpenState()
        circuitBreaker.transitionToHalfOpenState()

        assertEquals("fallback", messageService.getMessage(""))
        assertEquals(State.HALF_OPEN, circuitBreaker.state)

        assertEquals("fallback", messageService.getMessage(""))
        assertEquals(State.OPEN, circuitBreaker.state)
    }

    @Test
    fun `HALF-OPEN 상태에서 1번 실패 & 1번 성공하면 OPEN 상태로 전환`() {
        circuitBreaker.transitionToOpenState()
        circuitBreaker.transitionToHalfOpenState()

        assertEquals("fallback", messageService.getMessage(""))
        assertEquals(State.HALF_OPEN, circuitBreaker.state)

        assertEquals("success", messageService.getMessage("john"))
        assertEquals(State.OPEN, circuitBreaker.state)
    }

    @Test
    fun `HALF-OPEN 상태에서 2번 성공하면 CLOSED 상태로 전환`() {
        circuitBreaker.transitionToOpenState()
        circuitBreaker.transitionToHalfOpenState()

        assertEquals("success", messageService.getMessage("john"))
        assertEquals(State.HALF_OPEN, circuitBreaker.state)

        assertEquals("success", messageService.getMessage("john"))
        assertEquals(State.CLOSED, circuitBreaker.state)
    }

    @Test
    fun `OPEN 상태에서 어떤걸 요청해도 fallback 응답`() {
        circuitBreaker.transitionToOpenState()

        assertEquals("fallback", messageService.getMessage(""))
        assertEquals(State.OPEN, circuitBreaker.state)

        assertEquals("fallback", messageService.getMessage("john"))
        assertEquals(State.OPEN, circuitBreaker.state)
    }
}

참고

반응형

'Development > Spring' 카테고리의 다른 글

[Spring] Partitioning  (2) 2024.09.13
[Spring] Sharding  (0) 2024.07.12
[Spring] multi-module 프로젝트 구성하기(kotlin, gradle)  (0) 2024.07.07
[Spring] Exposed  (0) 2024.04.10
[Spring] HTTP Interface  (0) 2024.03.19

+ Recent posts