Resilience4j CircuitBreaker, 직접 구현하면서 이해해보자! - 2편: 상태 머신, 상태 전이

2025. 11. 21. 10:40·서버
※ 시리즈 글
Resilience4j CircuitBreaker, 직접 구현하면서 이해해보자! - 1편: 이해와 설계 
Resilience4j CircuitBreaker, 직접 구현하면서 이해해보자! - 2편: 상태 머신, 상태 전이 (현재 글)
Resilience4j CircuitBreaker, 직접 구현하면서 이해해보자! - 3편: 슬라이딩 윈도우
Resilience4j CircuitBreaker, 직접 구현하면서 이해해보자! - 4편: 설정, 사용, Fallback

🔷 들어가며

저번 글인 1편: 이해와 설계에서는 Resilience4j CircuitBreaker의 기본적인 이해와 구조에 대해서 살펴보았습니다. CircuitBreaker는 3가지 핵심적인 상태인 CLOSED, OPEN, HALF_OPEN을 가지고 있다고 했었는데요. 이번 글에서는 이것들이 어떻게 구현되어 있고, 어떻게 상태 전이를 하는지 알아보겠습니다. 또한, CircuitBreaker를 설정하고 생성하는 방법도 알아보겠습니다.

 

구현된 코드는 Spring Boot 3.5.7, Kotlin으로 작성되어 있습니다. 이 글에서는 core 패키지를 중심으로 설명하겠습니다.

 

spring-lab/resilience4j-circuitbreaker at main · alstn113/spring-lab

Spring에 대한 다양한 실험. Contribute to alstn113/spring-lab development by creating an account on GitHub.

github.com

 

🔷 CircuitBreaker 인터페이스와 확장함수

제일 먼저 CircuitBreaker의 행동 규약을 인터페이스를 통해서 정의해 보겠습니다.

package io.github.alstn113.resilience4j.core

import java.util.concurrent.TimeUnit


interface CircuitBreaker {

    fun onSuccess(duration: Long, durationUnit: TimeUnit)
    fun onError(duration: Long, durationUnit: TimeUnit, throwable: Throwable)

    fun tryAcquirePermission(): Boolean
    fun acquirePermission()
    fun getState(): State
    fun getName(): String

    enum class State {
        CLOSED, OPEN, HALF_OPEN
    }

    enum class StateTransition(
        private val fromState: State,
        private val toState: State,
    ) {
        CLOSED_TO_OPEN(State.CLOSED, State.OPEN),
        OPEN_TO_HALF_OPEN(State.OPEN, State.HALF_OPEN),
        HALF_OPEN_TO_CLOSED(State.HALF_OPEN, State.CLOSED),
        HALF_OPEN_TO_OPEN(State.HALF_OPEN, State.OPEN)
        ;

        companion object {
            private val STATE_TRANSITION_MAP: Map<Pair<State, State>, StateTransition> =
                entries.associateBy { it.fromState to it.toState }

            fun transitionBetween(name: String, fromState: State, toState: State): StateTransition {
                val transition = STATE_TRANSITION_MAP[fromState to toState]
                require(transition != null) { "유효하지 않은 상태 전이입니다: $name from=$fromState to=$toState" }
                return transition
            }
        }
    }
}

여러 가지 메서드들과 Enum들이 존재하는데요. Enum들을 먼저 설명하겠습니다.

🔶 State Enum

세 가지 대표적인 상태를 enum으로 정의한 것입니다.

  • CLOSED: 정상적인 상태로, 모든 호출이 허용됩니다. 
  • OPEN 상태는 장애가 발생한 상태로, 모든 호출이 차단됩니다. 
  • HALF_OPEN 상태에서는 일부 호출만 허용하고, 그 결과를 바탕으로 상태를 결정합니다.

🔶 StateTransition Enum

상태 간 이동을 명시적으로 기록한 enum입니다. 허용 가능한 조합만을 정의해서 상태 전이의 유효성 검증을 책임지기 위해서 만들어졌습니다. transitionBetween이 유효성 검증을 책임지고 있습니다.

  • CLOSED → OPEN
  • OPEN → HALF_OPEN
  • HALF_OPEN → CLOSED
  • HALF_OPEN → OPEN

🔶 CircuitBreaker 인터페이스 메서드

CircuitBreaker 인터페이스의 메서드들을 설명해 보겠습니다.

 

이 메서드들은 메서드 이름과 같이 상태와 이름을 반환합니다.

  • getState: CircuitBreaker의 상태를 반환합니다.
  • getName: CircuitBreaker의 이름을 반환합니다.

tryAcquirePermission과 acquirePermission은 유사한 메서드로 호출이 허용되는지 판단하는 메서드입니다.

  • tryAcquirePermission: 현재 상태가 호출을 허용하는지 boolean으로만 응답합니다. HALF_OPEN 상태에서는 남은 허용 횟수를 감소시키고, OPEN 상태에서는 false를 반환하는 등의 역할을 합니다.
  • acquirePermission: 호출을 실제로 확정합니다. 허용되지 않는 상태일 경우 CallNotPermittedException을 던지기 때문에, 보호하려는 코드가 실행되기 직전에 호출합니다. 간단하게 설명해서, tryAcquirePermission은 가능하냐고 묻는 거고, acquirePermission은 가능하지 않으면 예외를 던지는 것입니다.

onSuccess와 onError 메서드는 실행 결과를 기록하는 메서드입니다. 

  • onSuccess: 호출이 정상 처리되었을 때 성공 카운트를 기록합니다. 상태가 CLOSED라면 실패율 계산을 통해 OPEN 전환 여부를 검사하고, HALF_OPEN 상태라면 회복 판단 근거로 사용됩니다.
  • onError: 호출 실패를 기록합니다. 슬라이딩 윈도우 내부에서 실패율이 임계치를 초과하면 상태 전이가 발생할 수 있습니다.
Resilience4j CircuitBreaker에서는 느린 요청도 CircuitBreaker를 OPEN 하기 위한 고려 대상입니다.
onSuccess와 onError 메서드에서 (duration: Long, durationUnit: TimeUnit) 대신 Duration을 사용하면 되지 않을까 궁금할 수 있습니다. 이것은 불필요한 단위 변환을 막고, Duration 객체를 생성하지 않아 GC 부담을 줄이기 위한 것으로 판단됩니다.

🔶 CircuitBreaker 확장함수

CircuitBreaker를 실행하기 위한 확장함수입니다. 

fun <T> CircuitBreaker.execute(block: () -> T): T {
    this.acquirePermission()

    val start = System.nanoTime()

    try {
        val result = block()

        val duration = System.nanoTime() - start
        this.onSuccess(duration, TimeUnit.NANOSECONDS)
        return result
    } catch (e: Exception) {
        val duration = System.nanoTime() - start
        this.onError(duration, TimeUnit.NANOSECONDS, e)
        throw e
    }
}

CircuitBreaker의 확장함수인 execute는 다음과 같이 동작합니다.

  • acquirePermission 메서드를 실행합니다.
    • 현재 상태에 따라 호출 허용 여부를 결정합니다.
    • HALF_OPEN이라면 호출 가능 수를 차감하고, OPEN이면 CallNotPermittedException 예외가 발생합니다.
  • 실제 비즈니스 로직 block 메서드를 실행합니다.
  • 성공 시 onSuccess 메서드 호출합니다.
  • 실패 시 onError 메서드 호출 후 예외를 재전파합니다.
Spring 환경에서는 개발자가 직접 execute를 작성하지 않아도 되며, AOP가 CircuitBreaker의 핵심 흐름 전체를 자동으로 주입하여 처리합니다.

 

🔷 CircuitBreakerConfig와 CircuitBreakerRegistry

CircuitBreakerConfig는 CircuitBreaker가 어떻게 동작해야 하는지 정의하는 설정 객체입니다. 

  • 실패율 임계치(failureRateThreshold)
  • HALF_OPEN 시 몇 번의 호출을 시험할지(permittedNumberOfCallsInHalfOpenState)
  • OPEN 상태 유지 시간(waitDurationInOpenState)
  • 슬라이딩 윈도우 방식(slidingWindowType)
  • 슬라이딩 윈도우 크기(slidingWindowSize)
  • 실패율을 판단하기 위한 최소 호출 횟수(minimumNumberOfCalls)
package io.github.alstn113.resilience4j.core

import java.time.Duration

class CircuitBreakerConfig private constructor(
    val failureRateThreshold: Float,
    val permittedNumberOfCallsInHalfOpenState: Int,
    val waitDurationInOpenState: Duration,
    val slidingWindowType: SlidingWindowType,
    val slidingWindowSize: Int,
    val minimumNumberOfCalls: Int,
) {

    class Builder {
        private var failureRateThreshold = DEFAULT_FAILURE_RATE_THRESHOLD
        private var permittedNumberOfCallsInHalfOpenState = DEFAULT_PERMITTED_NUMBER_OF_CALLS_IN_HALF_OPEN_STATE
        private var waitDurationInOpenState = Duration.ofMillis(DEFAULT_WAIT_DURATION_IN_OPEN_STATE_MS)
        private var slidingWindowType = DEFAULT_SLIDING_WINDOW_TYPE
        private var slidingWindowSize = DEFAULT_SLIDING_WINDOW_SIZE
        private var minimumNumberOfCalls = DEFAULT_MINIMUM_NUMBER_OF_CALLS

        fun failureRateThreshold(threshold: Float) = apply {
            require(threshold in 0.0..100.0) {
                "failureRateThreshold는 0.0에서 100.0 사이여야 합니다."
            }
            this.failureRateThreshold = threshold
        }

        fun permittedNumberOfCallsInHalfOpenState(number: Int) = apply {
            require(number > 0) {
                "permittedNumberOfCallsInHalfOpenState는 0보다 커야 합니다."
            }
            this.permittedNumberOfCallsInHalfOpenState = number
        }

        fun waitDurationInOpenState(duration: Duration) = apply {
            require(!duration.isNegative && !duration.isZero) {
                "waitDurationInOpenState는 0보다 커야 합니다."
            }
            this.waitDurationInOpenState = duration
        }

        fun slidingWindow(
            type: SlidingWindowType,
            size: Int,
            minimumNumberOfCalls: Int,
        ) = apply {
            require(size > 0) {
                "slidingWindowSize는 0보다 커야 합니다."
            }
            require(minimumNumberOfCalls > 0) {
                "minimumNumberOfCalls는 0보다 커야 합니다."
            }
            if (type == SlidingWindowType.COUNT_BASED) {
                this.minimumNumberOfCalls = minOf(size, minimumNumberOfCalls)
            } else {
                this.minimumNumberOfCalls = minimumNumberOfCalls
            }
            this.slidingWindowType = type
            this.slidingWindowSize = size
        }

        fun build(): CircuitBreakerConfig {
            return CircuitBreakerConfig(
                failureRateThreshold = failureRateThreshold,
                permittedNumberOfCallsInHalfOpenState = permittedNumberOfCallsInHalfOpenState,
                waitDurationInOpenState = waitDurationInOpenState,
                slidingWindowType = slidingWindowType,
                slidingWindowSize = slidingWindowSize,
                minimumNumberOfCalls = minimumNumberOfCalls,
            )
        }
    }

    enum class SlidingWindowType {
        COUNT_BASED,
        TIME_BASED
    }

    companion object {
        fun custom() = Builder()

        private const val DEFAULT_FAILURE_RATE_THRESHOLD = 50F
        private const val DEFAULT_PERMITTED_NUMBER_OF_CALLS_IN_HALF_OPEN_STATE = 10
        private const val DEFAULT_WAIT_DURATION_IN_OPEN_STATE_MS = 60_000L
        private val DEFAULT_SLIDING_WINDOW_TYPE = SlidingWindowType.COUNT_BASED
        private const val DEFAULT_SLIDING_WINDOW_SIZE = 100
        private const val DEFAULT_MINIMUM_NUMBER_OF_CALLS = 100
    }
}

CircuitBreakerConfig는 Builder를 통해서 쉽게 설정 객체를 만들 수 있습니다.

val config = CircuitBreakerConfig.Builder()
            .failureRateThreshold(50.0f)
            .waitDurationInOpenState(Duration.ofMillis(1000))
            .permittedNumberOfCallsInHalfOpenState(2)
            .slidingWindow(
                type = CircuitBreakerConfig.SlidingWindowType.COUNT_BASED,
                size = 3,
                minimumNumberOfCalls = 3
            )
            .build()

 

CircuitBreaker는 애플리케이션에서 하나만 사용되지 않고 다양하게 존재할 수 있습니다. CircuitBreakerRegistry는 CircuitBreaker 인스턴스를 생성하고 관리하는 저장소 역할을 합니다.

package io.github.alstn113.resilience4j.core

import java.time.Clock
import java.util.concurrent.ConcurrentHashMap

class CircuitBreakerRegistry(
    private val defaultConfig: CircuitBreakerConfig,
    private val clock: Clock,
) {

    private val entryMap = ConcurrentHashMap<String, CircuitBreaker>()

    constructor(
        configMap: Map<String, CircuitBreakerConfig>,
        defaultConfig: CircuitBreakerConfig = CircuitBreakerConfig.custom().build(),
        clock: Clock,
    ) : this(defaultConfig = defaultConfig, clock = clock) {
        configMap.forEach { (name, config) ->
            entryMap[name] = CircuitBreakerStateMachine(
                name = name,
                config = config,
                clock = clock,
            )
        }
    }

    fun circuitBreaker(
        name: String,
        config: CircuitBreakerConfig = defaultConfig,
    ): CircuitBreaker {
        return entryMap.computeIfAbsent(name) {
            CircuitBreakerStateMachine(
                name = name,
                config = config,
                clock = clock,
            )
        }
    }
}

CircuitBreakerRegistry에서는 기본 설정을 바탕으로 새로운 CircuitBreaker를 만들 수도 있고, 새로운 설정을 적용하여 생성할 수도 있습니다. 또한, ConcurrentHashMap을 사용해서 여러 스레드에서 동시에 접근해도 안전하게 작동하도록 했습니다. circuitBreaker 메서드는 이름을 기준으로 등록된 CircuitBreaker를 찾거나, 없다면 생성해 저장한 뒤 반환하는 역할을 합니다. 즉, 중복 생성을 막고 일관된 인스턴스를 공유하도록 보장하는 구조입니다.

 

🔷 CircuitBreakerStateMachine

🔶 전체 구조

CircuitBreakerStateMachine은 CircuitBreaker의 실제 동작을 구현하는 핵심 클래스입니다. CircuitBreaker는 호출 결과와 에러 비율에 따라 CLOSED -> OPEN -> HALF_OPEN 상태로 이동하는데, 각 상태마다 권한 부여 방식, 전환 조건, 에러 처리 방식이 다릅니다. 그래서 이 구현은 상태 패턴을 사용해 각 상태별로 클래스를 분리하고, 전이 로직은 StateMachine이 관리합니다.

 

먼저, CircuitBreakerStateMachine의 전체 코드를 확인하고 부분 부분을 나눠서 알아보겠습니다.

package io.github.alstn113.resilience4j.core

import io.github.alstn113.resilience4j.core.exception.CallNotPermittedException
import io.github.alstn113.resilience4j.metrics.CircuitBreakerMetrics
import io.github.alstn113.resilience4j.metrics.CircuitBreakerMetrics.Result.BELOW_THRESHOLD
import io.github.alstn113.resilience4j.metrics.CircuitBreakerMetrics.Result.FAILURE_RATE_ABOVE_THRESHOLD
import org.slf4j.LoggerFactory
import java.time.Clock
import java.time.Instant
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference

class CircuitBreakerStateMachine(
    private val name: String,
    private val config: CircuitBreakerConfig,
    private val clock: Clock,
) : CircuitBreaker {

    private val log = LoggerFactory.getLogger(javaClass)
    private val stateRef: AtomicReference<CircuitBreakerState> = AtomicReference(ClosedState())

    override fun tryAcquirePermission(): Boolean {
        return stateRef.get().tryAcquirePermission()
    }

    override fun acquirePermission() {
        try {
            stateRef.get().acquirePermission()
        } catch (e: Exception) {
            throw e
        }
    }

    override fun onSuccess(duration: Long, durationUnit: TimeUnit) {
        stateRef.get().onSuccess(duration, durationUnit)
    }

    override fun onError(duration: Long, durationUnit: TimeUnit, throwable: Throwable) {
        stateRef.get().onError(duration, durationUnit)
    }

    override fun getState(): CircuitBreaker.State {
        return stateRef.get().getState()
    }

    override fun getName(): String {
        return name
    }

    private fun stateTransition(
        newState: CircuitBreaker.State,
        newStateGenerator: (curState: CircuitBreakerState) -> CircuitBreakerState,
    ) {
        val prevState = stateRef.getAndUpdate { curState ->
            CircuitBreaker.StateTransition.Companion.transitionBetween(
                name = name,
                fromState = curState.getState(),
                toState = newState
            )

            newStateGenerator(curState)
        }

        log.info("CircuitBreaker '$name' 상태 전이: ${prevState.getState()} -> $newState")
    }

    private interface CircuitBreakerState {
        fun tryAcquirePermission(): Boolean
        fun acquirePermission()
        fun onSuccess(duration: Long, durationUnit: TimeUnit)
        fun onError(duration: Long, durationUnit: TimeUnit)
        fun getState(): CircuitBreaker.State
        fun getMetrics(): CircuitBreakerMetrics
    }

    private inner class ClosedState : CircuitBreakerState {

        private val circuitBreakerMetrics = CircuitBreakerMetrics.forClosed(config, clock)
        private val isClosed = AtomicBoolean(true)

        override fun tryAcquirePermission(): Boolean {
            return isClosed.get()
        }

        override fun acquirePermission() {
            // no-op
        }

        override fun onSuccess(duration: Long, durationUnit: TimeUnit) {
            checkIfThresholdExceeded(circuitBreakerMetrics.onSuccess(duration, durationUnit))
        }

        override fun onError(duration: Long, durationUnit: TimeUnit) {
            checkIfThresholdExceeded(circuitBreakerMetrics.onError(duration, durationUnit))
        }

        override fun getState(): CircuitBreaker.State {
            return CircuitBreaker.State.CLOSED
        }

        override fun getMetrics(): CircuitBreakerMetrics {
            return circuitBreakerMetrics
        }

        private fun checkIfThresholdExceeded(result: CircuitBreakerMetrics.Result) {
            if (result == FAILURE_RATE_ABOVE_THRESHOLD && isClosed.compareAndSet(true, false)) {
                stateTransition(
                    newState = CircuitBreaker.State.OPEN,
                    newStateGenerator = { curState -> OpenState(curState.getMetrics()) }
                )
            }
        }
    }

    private inner class OpenState(
        private val circuitBreakerMetrics: CircuitBreakerMetrics,
    ) : CircuitBreakerState {

        private val isOpen = AtomicBoolean(true)
        private val retryAfterWaitDuration: Instant = clock.instant().plus(config.waitDurationInOpenState)

        /**
         * Open 상태에서는 설정된 대기 시간 이후에 대해서 Half-Open 상태로 전환을 시도합니다.
         */
        override fun tryAcquirePermission(): Boolean {
            if (clock.instant().isAfter(retryAfterWaitDuration)) {
                toHalfOpenState()
                val callPermitted = stateRef.get().tryAcquirePermission()
                if (!callPermitted) {
                    circuitBreakerMetrics.onCallNotPermitted()
                }
                return callPermitted
            }
            circuitBreakerMetrics.onCallNotPermitted()
            return false
        }

        override fun acquirePermission() {
            if (!tryAcquirePermission()) {
                throw CallNotPermittedException.from(circuitBreaker = this@CircuitBreakerStateMachine)
            }
        }

        override fun onSuccess(duration: Long, durationUnit: TimeUnit) {
            circuitBreakerMetrics.onSuccess(duration, durationUnit)
        }

        override fun onError(duration: Long, durationUnit: TimeUnit) {
            circuitBreakerMetrics.onError(duration, durationUnit)
        }

        override fun getState(): CircuitBreaker.State {
            return CircuitBreaker.State.OPEN
        }

        override fun getMetrics(): CircuitBreakerMetrics {
            return circuitBreakerMetrics
        }

        @Synchronized
        private fun toHalfOpenState() {
            if (isOpen.compareAndSet(true, false)) {
                stateTransition(
                    newState = CircuitBreaker.State.HALF_OPEN,
                    newStateGenerator = { HalfOpenState() }
                )
            }
        }
    }

    private inner class HalfOpenState : CircuitBreakerState {

        private val permittedNumberOfCallsInHalfState = config.permittedNumberOfCallsInHalfOpenState
        private val permittedNumberOfCalls = AtomicInteger(permittedNumberOfCallsInHalfState)
        private val circuitBreakerMetrics = CircuitBreakerMetrics.forHalfOpen(
            permittedNumberOfCallsInHalfOpenState = permittedNumberOfCallsInHalfState,
            circuitBreakerConfig = config,
            clock = clock,
        )
        private val isHalfOpen = AtomicBoolean(true)

        /**
         * Half-Open 상태에서는 제한된 수의 호출만 허용합니다.
         */
        override fun tryAcquirePermission(): Boolean {
            val previousValue = permittedNumberOfCalls.getAndUpdate { current ->
                if (current > 0) current - 1 else 0
            }

            if (previousValue > 0) {
                return true
            }

            circuitBreakerMetrics.onCallNotPermitted()
            return false
        }

        override fun acquirePermission() {
            if (!tryAcquirePermission()) {
                throw CallNotPermittedException.from(circuitBreaker = this@CircuitBreakerStateMachine)
            }
        }

        override fun onSuccess(duration: Long, durationUnit: TimeUnit) {
            checkIfThresholdExceeded(circuitBreakerMetrics.onSuccess(duration, durationUnit))
        }

        override fun onError(duration: Long, durationUnit: TimeUnit) {
            checkIfThresholdExceeded(circuitBreakerMetrics.onError(duration, durationUnit))
        }

        override fun getState(): CircuitBreaker.State {
            return CircuitBreaker.State.HALF_OPEN
        }

        override fun getMetrics(): CircuitBreakerMetrics {
            return circuitBreakerMetrics
        }

        private fun checkIfThresholdExceeded(result: CircuitBreakerMetrics.Result) {
            if (result == FAILURE_RATE_ABOVE_THRESHOLD && isHalfOpen.compareAndSet(true, false)) {
                stateTransition(
                    newState = CircuitBreaker.State.OPEN,
                    newStateGenerator = { curState -> OpenState(curState.getMetrics()) }
                )
            } else if (result == BELOW_THRESHOLD && isHalfOpen.compareAndSet(true, false)) {
                stateTransition(
                    newState = CircuitBreaker.State.CLOSED,
                    newStateGenerator = { ClosedState() }
                )
            }
        }
    }
}

CircuitBreakerStateMachine의 내부 구현을 숨겨보면 다음과 같은 구조를 확인할 수 있습니다.

package io.github.alstn113.resilience4j.core

import io.github.alstn113.resilience4j.core.exception.CallNotPermittedException
import io.github.alstn113.resilience4j.metrics.CircuitBreakerMetrics
import io.github.alstn113.resilience4j.metrics.CircuitBreakerMetrics.Result.BELOW_THRESHOLD
import io.github.alstn113.resilience4j.metrics.CircuitBreakerMetrics.Result.FAILURE_RATE_ABOVE_THRESHOLD
import org.slf4j.LoggerFactory
import java.time.Clock
import java.time.Instant
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference

class CircuitBreakerStateMachine(
    private val name: String,
    private val config: CircuitBreakerConfig,
    private val clock: Clock,
) : CircuitBreaker {

    private val log = LoggerFactory.getLogger(javaClass)
    private val stateRef: AtomicReference<CircuitBreakerState> = AtomicReference(ClosedState())

    override fun tryAcquirePermission(): Boolean { ... }
    override fun acquirePermission() { ... }
    override fun onSuccess(duration: Long, durationUnit: TimeUnit) { ... }
    override fun onError(duration: Long, durationUnit: TimeUnit, throwable: Throwable) { ... }
    override fun getState(): CircuitBreaker.State { ... }
    override fun getName(): String { ... }
    
    private fun stateTransition( ... ) { ... }

    private interface CircuitBreakerState {
        fun tryAcquirePermission(): Boolean
        fun acquirePermission()
        fun onSuccess(duration: Long, durationUnit: TimeUnit)
        fun onError(duration: Long, durationUnit: TimeUnit)
        fun getState(): CircuitBreaker.State
        fun getMetrics(): CircuitBreakerMetrics
    }

    private inner class ClosedState : CircuitBreakerState { ... }
    private inner class OpenState( ... ) : CircuitBreakerState { ... }
    private inner class HalfOpenState : CircuitBreakerState { ... }
}

이 구조를 살펴보면 흥미로운 부분들이 몇 가지 보입니다. 먼저 CircuitBreakerStateMachine은 상태별 로직을 분리하기 위해 전형적인 상태 패턴의 형태를 따르지만, 상태 전환 자체는 각 상태 객체가 아니라 StateMachine이 전담한다는 점이 특징입니다.

CircuitBreaker는 멀티스레드 환경에서 여러 스레드가 동시에 상태를 바꾸려 할 수 있기 때문에, 상태 객체가 직접 전환을 수행하는 방식은 안전하지 않습니다. 이를 해결하기 위해 현재 상태는 AtomicReference로 보관하고, 전이는 stateTransition 메서드에서 원자적으로 처리합니다. 또한 StateTransition enum을 사용해 허용된 상태 조합만 전환이 이루어지도록 강제합니다.

즉, 전체 구조는 상태 패턴을 기반으로 하지만, 실제 전이는 중앙에서 통제하고 상태 객체는 판단 역할에만 집중하도록 확장된 형태라고 볼 수 있습니다.

또한 CircuitBreakerState 인터페이스와 각 State 구현체를 CircuitBreakerStateMachine 내부 클래스로 선언함으로써, 외부에서 상태 객체를 임의로 생성하거나 수정할 수 없도록 캡슐화를 강화했습니다. 이러한 구조 덕분에 상태 관리가 StateMachine 내부로 명확하게 고립되고, 전체 동작이 더 안전하고 일관되게 유지됩니다.

 

CircuitBreakerStateMachine의 각 메서드는 현재 상태 객체에 요청을 위임하는 구조이므로, 개별 메서드의 동작 설명은 별도로 다루지 않겠습니다. 상태별 로직은 각 State 구현체에 분리되어 있어, 전체 흐름만 이해하면 동작 방식은 자연스럽게 따라옵니다.

🔶 ClosedState - 정상 상태

Closed 상태는 서비스가 정상적으로 동작하는 기본 상태입니다. 모든 호출을 그대로 허용하고, 호출 성공과 실패에 대한 통계를 계속 누적합니다. CircuitBreakerMetrics 클래스가 나오는데 이 부분은 다음 글에서 다루겠습니다. CircuitBreaker 통계 정보라고 이해하시면 되겠습니다.

private inner class ClosedState : CircuitBreakerState {

    private val circuitBreakerMetrics = CircuitBreakerMetrics.forClosed(config, clock)
    private val isClosed = AtomicBoolean(true)

    override fun tryAcquirePermission(): Boolean {
        return isClosed.get()
    }

    override fun acquirePermission() {
        // no-op
    }

    override fun onSuccess(duration: Long, durationUnit: TimeUnit) {
        checkIfThresholdExceeded(circuitBreakerMetrics.onSuccess(duration, durationUnit))
    }

    override fun onError(duration: Long, durationUnit: TimeUnit) {
        checkIfThresholdExceeded(circuitBreakerMetrics.onError(duration, durationUnit))
    }

    override fun getState(): CircuitBreaker.State {
        return CircuitBreaker.State.CLOSED
    }

    override fun getMetrics(): CircuitBreakerMetrics {
        return circuitBreakerMetrics
    }

    private fun checkIfThresholdExceeded(result: CircuitBreakerMetrics.Result) {
        if (result == FAILURE_RATE_ABOVE_THRESHOLD && isClosed.compareAndSet(true, false)) {
            stateTransition(
                newState = CircuitBreaker.State.OPEN,
                newStateGenerator = { curState -> OpenState(curState.getMetrics()) }
            )
        }
    }
}
  • 호출 허용 여부
    • tryAcquirePermission()은 항상 true를 반환합니다.
    • 내부적으로 isClosed 플래그로 중복 전환을 막습니다.
  • 메트릭 처리
    • onSuccess, onError를 통해 슬라이딩 윈도우 기반 실패율을 갱신합니다.
    • 실패율이 임계값을 넘으면 checkIfThresholdExceeded에서 Open으로 전환합니다.
  • 전환 조건
    • 실패율이 임계값 초과 → Open 상태로 전환

ClosedState에서는 오직 “정상 상태인지”만 판단하며, 회로 차단 여부는 메트릭 변화로 결정됩니다.

🔶 OpenState - 호출 차단 상태

Open 상태는 장애가 감지되어 더 이상 외부 호출을 허용하지 않는 상태입니다. 설정된 대기 시간(waitDurationInOpenState)이 지나기 전까지는 모든 호출을 차단합니다.

private inner class OpenState(
    private val circuitBreakerMetrics: CircuitBreakerMetrics,
) : CircuitBreakerState {

    private val isOpen = AtomicBoolean(true)
    private val retryAfterWaitDuration: Instant = clock.instant().plus(config.waitDurationInOpenState)

    /**
     * Open 상태에서는 설정된 대기 시간 이후에 대해서 Half-Open 상태로 전환을 시도합니다.
     */
    override fun tryAcquirePermission(): Boolean {
        if (clock.instant().isAfter(retryAfterWaitDuration)) {
            toHalfOpenState()
            val callPermitted = stateRef.get().tryAcquirePermission()
            if (!callPermitted) {
                circuitBreakerMetrics.onCallNotPermitted()
            }
            return callPermitted
        }
        circuitBreakerMetrics.onCallNotPermitted()
        return false
    }

    override fun acquirePermission() {
        if (!tryAcquirePermission()) {
            throw CallNotPermittedException.from(circuitBreaker = this@CircuitBreakerStateMachine)
        }
    }

    override fun onSuccess(duration: Long, durationUnit: TimeUnit) {
        circuitBreakerMetrics.onSuccess(duration, durationUnit)
    }

    override fun onError(duration: Long, durationUnit: TimeUnit) {
        circuitBreakerMetrics.onError(duration, durationUnit)
    }

    override fun getState(): CircuitBreaker.State {
        return CircuitBreaker.State.OPEN
    }

    override fun getMetrics(): CircuitBreakerMetrics {
        return circuitBreakerMetrics
    }

    @Synchronized
    private fun toHalfOpenState() {
        if (isOpen.compareAndSet(true, false)) {
            stateTransition(
                newState = CircuitBreaker.State.HALF_OPEN,
                newStateGenerator = { HalfOpenState() }
            )
        }
    }
}
  • 호출 허용 여부
    • 대기 시간이 지나지 않았다면 모든 호출을 차단하고 onCallNotPermitted를 기록합니다.
    • 대기 시간 이후에는 Half-Open 상태로 전환을 시도한 뒤 그 상태에 위임합니다.
  • 메트릭 처리
    • Open 상태에서도 메트릭은 기록되지만, 상태 전환에는 관여하지 않습니다.
  • 전환 조건
    • 대기 시간이 지나면 → Half-Open으로 전환 시도

OpenState는 “대기 후 재시도 준비” 역할만 수행하며, 호출 허용 여부는 Half-Open에 위임하도록 설계되었습니다.

 

Open 상태에서만 @Synchronized가 필요한 이유는, 대기 시간이 지나 Half-Open으로 전환되는 시점에 여러 스레드가 동시에 접근할 수 있기 때문입니다. Closed나 Half-Open에서는 각 요청의 성공 또는 실패 결과가 즉시 반영되며, AtomicBoolean을 이용한 CAS만으로도 상태 전환을 정확히 한 번만 일어나도록 충분히 제어할 수 있습니다.

 

하지만 Open에서는 상황이 다릅니다. 여러 스레드가 동시에 “대기 시간이 지났으니 이제 Half-Open으로 바꿀 수 있다”라고 판단할 수 있고, 이 전환 과정을 오직 하나의 스레드만 처리해야 합니다. 이러한 경쟁 상황을 안전하게 막기 위해 Open→Half-Open 전환 구간에 synchronized를 사용해 전환 작업 전체를 원자적으로 보장합니다.

Resilience4j CircuitBreaker에서는 attempts라는 값을 사용합니다. 이것은 Open으로 갈 때마다 증가되며, Open에서 Half-Open으로 가기 위한 대기 시간을 지수적으로 늘리기 위해서 사용됩니다.
Resilience4j CircuitBreaker는 기본적으로 요청이 왔을 때 상태를 전이합니다. 즉, Open 상태에서 대기 시간이 지났다고 해서 바로 Half-Open으로 전이하지 않습니다. 하지만 그것을 가능하게 하는 설정도 있습니다. (automaticTransition=true)

🔶 HalfOpenState - 재시험 상태

Half-Open 상태는 Open에서 일정 시간 대기 후, 시스템이 회복되었는지 소규모로 재시험하는 단계입니다.

private inner class HalfOpenState : CircuitBreakerState {

    private val permittedNumberOfCallsInHalfState = config.permittedNumberOfCallsInHalfOpenState
    private val permittedNumberOfCalls = AtomicInteger(permittedNumberOfCallsInHalfState)
    private val circuitBreakerMetrics = CircuitBreakerMetrics.forHalfOpen(
        permittedNumberOfCallsInHalfOpenState = permittedNumberOfCallsInHalfState,
        circuitBreakerConfig = config,
        clock = clock,
    )
    private val isHalfOpen = AtomicBoolean(true)

    /**
     * Half-Open 상태에서는 제한된 수의 호출만 허용합니다.
     */
    override fun tryAcquirePermission(): Boolean {
        val previousValue = permittedNumberOfCalls.getAndUpdate { current ->
            if (current > 0) current - 1 else 0
        }

        if (previousValue > 0) {
            return true
        }

        circuitBreakerMetrics.onCallNotPermitted()
        return false
    }

    override fun acquirePermission() {
        if (!tryAcquirePermission()) {
            throw CallNotPermittedException.from(circuitBreaker = this@CircuitBreakerStateMachine)
        }
    }

    override fun onSuccess(duration: Long, durationUnit: TimeUnit) {
        checkIfThresholdExceeded(circuitBreakerMetrics.onSuccess(duration, durationUnit))
    }

    override fun onError(duration: Long, durationUnit: TimeUnit) {
        checkIfThresholdExceeded(circuitBreakerMetrics.onError(duration, durationUnit))
    }

    override fun getState(): CircuitBreaker.State {
        return CircuitBreaker.State.HALF_OPEN
    }

    override fun getMetrics(): CircuitBreakerMetrics {
        return circuitBreakerMetrics
    }

    private fun checkIfThresholdExceeded(result: CircuitBreakerMetrics.Result) {
        if (result == FAILURE_RATE_ABOVE_THRESHOLD && isHalfOpen.compareAndSet(true, false)) {
            stateTransition(
                newState = CircuitBreaker.State.OPEN,
                newStateGenerator = { curState -> OpenState(curState.getMetrics()) }
            )
        } else if (result == BELOW_THRESHOLD && isHalfOpen.compareAndSet(true, false)) {
            stateTransition(
                newState = CircuitBreaker.State.CLOSED,
                newStateGenerator = { ClosedState() }
            )
        }
    }
}
  • 호출 허용 여부
    • 정해진 횟수(permittedNumberOfCallsInHalfOpenState)만 호출을 허용합니다.
    • AtomicInteger로 관리하며, 모두 소진되면 추가 호출은 차단합니다.
  • 메트릭 처리
    • 제한된 호출들의 성공·실패 비율로 회복 여부를 판단합니다.
  • 전환 조건
    • 실패율이 임계값보다 높으면 → Open 상태로 회귀
    • 실패율이 정상 범위라면 → Closed 상태로 복귀

Half-OpenState는 회복 판별을 위한 “시험 상태”이며, 소수의 호출만으로 정상 여부를 판단하는 구조입니다.

 

🔷 마치며

이번 글에서는 CircuitBreaker의 핵심인 상태 머신 구조와 상태 전이 흐름을 중심으로 살펴보았습니다. 다음 글인 3편: 슬라이딩 윈도우에서는 상태 전이를 판단하는 기준이 되는 요청 통계가 어떻게 집계되는지, 그리고 이를 위해 사용되는 슬라이딩 윈도우 구조를 자세히 설명할 예정입니다. 특히 Resilience4j에서 제공하는 두 가지 방식인 Count-based와 Time-based 윈도우가 어떻게 동작하는지 각각의 구현 관점에서 비교해 보겠습니다. 잘못된 부분이 있다면 댓글 부탁드립니다. 읽어주셔서 감사합니다.

 

🔷 참고

  • Resilience4j 깃허브
  • Resilience4j 공식문서

'서버' 카테고리의 다른 글

Resilience4j CircuitBreaker, 직접 구현하면서 이해해보자! - 4편: 설정, 사용, Fallback  (1) 2025.11.21
Resilience4j CircuitBreaker, 직접 구현하면서 이해해보자! - 3편: 슬라이딩 윈도우  (1) 2025.11.21
Resilience4j CircuitBreaker, 직접 구현하면서 이해해보자! - 1편: 이해와 설계  (0) 2025.11.21
Redis 분산락, 직접 구현하면서 이해해보자! - 2편: Pub/Sub 방식  (0) 2025.10.31
Redis 분산락, 직접 구현하면서 이해해보자! - 1편: Spin Lock 방식  (0) 2025.10.31
'서버' 카테고리의 다른 글
  • Resilience4j CircuitBreaker, 직접 구현하면서 이해해보자! - 4편: 설정, 사용, Fallback
  • Resilience4j CircuitBreaker, 직접 구현하면서 이해해보자! - 3편: 슬라이딩 윈도우
  • Resilience4j CircuitBreaker, 직접 구현하면서 이해해보자! - 1편: 이해와 설계
  • Redis 분산락, 직접 구현하면서 이해해보자! - 2편: Pub/Sub 방식
alstn113
alstn113
웹 프론트엔드, 서버 개발에 관한 이야기를 다룹니다 :D
  • alstn113
    alstn113's devlog
    alstn113
  • 전체
    오늘
    어제
    • 분류 전체보기 (62)
      • 서버 (31)
      • 웹 프론트엔드 (5)
      • 협업 (2)
      • 우아한테크코스 6기 백엔드 (12)
      • 책, 영상, 블로그 정리 (10)
      • 회고 (1)
  • 블로그 메뉴

    • 홈
  • 링크

    • Github
  • 공지사항

  • 인기 글

  • 태그

    회고
    굿폰
    플러피
    우아한테크코스
    글쓰기
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
alstn113
Resilience4j CircuitBreaker, 직접 구현하면서 이해해보자! - 2편: 상태 머신, 상태 전이
상단으로

티스토리툴바