Resilience4j CircuitBreaker, 직접 구현하면서 이해해보자! - 3편: 슬라이딩 윈도우

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

🔷 들어가며

이전 글인 2편: 상태 머신, 상태 전이에서는 CircuitBreaker가 어떤 기준으로 상태를 전이하는지, 그리고 그 기준이 되는 통계를 어떻게 활용하는지를 살펴봤습니다. 하지만 상태 전이는 결국 통계가 어떻게 계산되느냐에 달려 있습니다. 실패율, 최소 호출 수 같은 값들이 어떤 방식으로 유지되고 업데이트되는지를 이해해야 CircuitBreaker의 동작을 온전히 파악할 수 있습니다. 

 

이번 글에서는 Resilience4j가 사용하는 슬라이딩 윈도우 기반 통계 구조를 코드 중심으로 살펴보겠습니다. Count 기반과 Time 기반 두 방식이 각각 어떤 특징을 가지는지, 내부 버킷 구조가 어떻게 동작하는지를 단계적으로 정리해 보겠습니다.

 

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

 

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의 각 상태(State)는 고유한 CircuitBreakerMetrics 인스턴스를 가지고 있습니다. 이 객체는 요청 결과를 슬라이딩 윈도우에 기록하고, 실패율이나 최소 호출 수 같은 지표를 계산한 뒤 상태 전이에 필요한 신호를 만들어내는 역할을 합니다.

 

CircuitBreakerMetrics 내부에는 두 가지 Metrics 구현 중 하나가 주입됩니다.

  • 요청 개수 기반으로 동작하는 CountBasedMetrics
  • 시간 기반으로 윈도우를 유지하는 TimeBasedMetrics

요청이 발생하면 Metrics.record 메서드를 통해 해당 시점의 버킷에 값이 기록되고, 동시에 TotalBucket이 갱신됩니다. 이후 전체 통계를 담은 Snapshot이 생성되며, CircuitBreakerMetrics는 이 Snapshot을 읽어 실패율이 임계값을 초과했는지, 최소 호출 수를 만족하는지를 판단합니다. 이렇게 계산된 결과는 상태 머신으로 전달되어 다음 상태 전이를 결정하는 기준이 됩니다.

 

통계 수집은 Metrics 인터페이스로 추상화되어 있습니다.

package io.github.alstn113.resilience4j.metrics

import java.util.concurrent.TimeUnit


interface Metrics {

    fun record(duration: Long, durationUnit: TimeUnit, outcome: Outcome): Snapshot
    fun getSnapshot(): Snapshot

    enum class Outcome {
        SUCCESS, ERROR
    }
}
package io.github.alstn113.resilience4j.metrics

import io.github.alstn113.resilience4j.metrics.impl.TotalBucket
import java.time.Duration

class Snapshot(
    private val totalBucket: TotalBucket,
) {

    val totalDuration: Duration = Duration.ofMillis(totalBucket.totalDurationMillis)
    val totalNumberOfCalls: Int = totalBucket.numberOfCalls
    val numberOfSuccessfulCalls: Int = totalBucket.numberOfCalls - totalBucket.numberOfFailedCalls
    val numberOfFailedCalls: Int = totalBucket.numberOfFailedCalls
    val failureRate: Float = if (totalBucket.numberOfCalls == 0) {
        0.0f
    } else {
        totalBucket.numberOfFailedCalls * 100.0f / totalBucket.numberOfCalls
    }
}

record 메서드를 통해서 요청 결과를 받아 통계를 갱신하고 즉시 스냅샷을 반환합니다. Snapshot은 실패율, 총 호출 수, 실패 수 등을 담고 있습니다. 내부적으로는 TotalBucket을 기반으로 전체 집계를 유지합니다. 즉, 모든 구현은 버킷 구조를 가지고 record 메서드만 다르게 구현한다고 볼 수 있습니다.

 

🔷 CircuitBreakerMetrics

CircuitBreakerMetrics는 CircuitBreaker가 현재 상태를 판단하기 위해 필요한 모든 통계값을 관리합니다. 

package io.github.alstn113.resilience4j.metrics

import io.github.alstn113.resilience4j.core.CircuitBreakerConfig
import io.github.alstn113.resilience4j.metrics.impl.CountBasedMetrics
import io.github.alstn113.resilience4j.metrics.impl.TimeBasedMetrics
import java.time.Clock
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.LongAdder

class CircuitBreakerMetrics(
    private val slidingWindowType: CircuitBreakerConfig.SlidingWindowType,
    private val slidingWindowSize: Int,
    private val circuitBreakerConfig: CircuitBreakerConfig,
    private val clock: Clock,
) {

    private val metrics: Metrics
    private val failureRateThreshold: Float
    private val numberOfNotPermittedCalls: LongAdder
    private var minimumNumberOfCalls: Int

    init {
        when (slidingWindowType) {
            CircuitBreakerConfig.SlidingWindowType.COUNT_BASED -> {
                this.metrics = CountBasedMetrics(slidingWindowSize)
                this.minimumNumberOfCalls = minOf(circuitBreakerConfig.minimumNumberOfCalls, slidingWindowSize)
            }

            CircuitBreakerConfig.SlidingWindowType.TIME_BASED -> {
                this.metrics = TimeBasedMetrics(slidingWindowSize, clock)
                this.minimumNumberOfCalls = circuitBreakerConfig.minimumNumberOfCalls
            }
        }
        this.failureRateThreshold = circuitBreakerConfig.failureRateThreshold
        this.numberOfNotPermittedCalls = LongAdder()
    }

    fun onCallNotPermitted() {
        numberOfNotPermittedCalls.increment()
    }

    fun onSuccess(duration: Long, durationUnit: TimeUnit): Result {
        val snapshot = metrics.record(duration, durationUnit, Metrics.Outcome.SUCCESS)
        return checkIfThresholdsExceeded(snapshot)
    }

    fun onError(duration: Long, durationUnit: TimeUnit): Result {
        val snapshot = metrics.record(duration, durationUnit, Metrics.Outcome.ERROR)
        return checkIfThresholdsExceeded(snapshot)
    }

    fun getFailureRate(): Float {
        val snapshot = metrics.getSnapshot()
        return snapshot.failureRate
    }

    fun getNumberOfNotPermittedCalls(): Long {
        return numberOfNotPermittedCalls.sum()
    }

    fun getNumberOfFailedCalls(): Int {
        val snapshot = metrics.getSnapshot()
        return snapshot.numberOfFailedCalls
    }

    fun getNumberOfSuccessfulCalls(): Int {
        val snapshot = metrics.getSnapshot()
        return snapshot.numberOfSuccessfulCalls
    }

    fun getTotalNumberOfCalls(): Int {
        val snapshot = metrics.getSnapshot()
        return snapshot.totalNumberOfCalls
    }

    private fun checkIfThresholdsExceeded(snapshot: Snapshot): Result {
        return when {
            snapshot.totalNumberOfCalls < minimumNumberOfCalls -> Result.BELOW_MINIMUM_CALLS_THRESHOLD
            snapshot.failureRate >= failureRateThreshold -> Result.FAILURE_RATE_ABOVE_THRESHOLD
            else -> Result.BELOW_THRESHOLD
        }
    }

    companion object {
        fun forClosed(
            circuitBreakerConfig: CircuitBreakerConfig,
            clock: Clock,
        ): CircuitBreakerMetrics {
            return CircuitBreakerMetrics(
                slidingWindowType = circuitBreakerConfig.slidingWindowType,
                slidingWindowSize = circuitBreakerConfig.slidingWindowSize,
                circuitBreakerConfig = circuitBreakerConfig,
                clock = clock,
            )
        }

        fun forHalfOpen(
            permittedNumberOfCallsInHalfOpenState: Int,
            circuitBreakerConfig: CircuitBreakerConfig,
            clock: Clock,
        ): CircuitBreakerMetrics {
            return CircuitBreakerMetrics(
                slidingWindowType = CircuitBreakerConfig.SlidingWindowType.COUNT_BASED,
                slidingWindowSize = permittedNumberOfCallsInHalfOpenState,
                circuitBreakerConfig = circuitBreakerConfig,
                clock = clock,
            )
        }
    }

    enum class Result {
        BELOW_THRESHOLD, // 실패율이 임계값 이하임. (정상 작동)
        FAILURE_RATE_ABOVE_THRESHOLD, // 실패율이 임계값을 넘음.
        BELOW_MINIMUM_CALLS_THRESHOLD, // 데이터가 충분히 쌓이지 않음.
        ;
    }
}

🔶  슬라이딩 윈도우 타입 초기화

CircuitBreaker는 두 가지 방식 중 하나로 통계를 저장합니다.

  • COUNT_BASED: 최근 N개의 호출만 가지고 통계를 계산합니다.
  • TIME_BASED: 최근 T초 동안의 호출만 고려합니다.

생성자에서는 이 타입을 확인해 적절한 Metrics 구현체(CountBasedMetrics, TimeBasedMetrics)를 선택해 초기화합니다. 또한 최소 호출 수(minimumNumberOfCalls)도 이때 함께 설정합니다.

🔶  호출 성공/실패 기록과 임계값 검사

onSuccess, onError 메서드가 실제 호출 결과를 기록합니다. Metrics 구현체는 내부적으로 실패 횟수, 총 호출 수, 실패율을 계속 계산하고 있습니다. 업데이트가 끝나면 Snapshot을 받아 현재 임계 조건을 넘었는지 확인합니다.

 

checkIfThresholdsExceeded 메서드는 아래 3가지를 순서대로 검사합니다.

  1. 총 호출 수가 최소 기준보다 작은가?: 샘플 데이터가 충분하지 않기 때문에 상태 전환을 하지 않습니다.
  2. 실패율이 임계치를 넘는가?: OPEN 상태로 전환해야 하는 조건입니다.
  3. 그 외: 정상 범위로 판단합니다.

이렇게 결과는 Result enum 값으로 반환됩니다.

🔶  Half-Open / Closed 전용 팩토리 생성

상태별로 통계 수집 기준이 다르기 때문에 팩토리 메서드를 제공합니다.

  • forClosed: 설정에 맞는 슬라이딩 윈도우 기반 초기화합니다.
  • forHalfOpen: Half-Open은 반드시 “허용할 호출 수”만큼 카운트 기반으로 동작해야 하므로 Count-based로 고정합니다.

🔶  Not Permitted Calls 기록

서킷이 OPEN 상태일 때 들어오는 요청은 실제 비즈니스 로직을 실행하지 못하고 바로 거절됩니다. 이런 거절된 호출은 정상적인 실패나 성공 통계에 포함되면 안 되기 때문에 별도의 카운터로 관리합니다. 이를 위해 LongAdder를 사용합니다.

LongAdder는 내부적으로 여러 셀(Cell) 을 두고 업데이트를 분산시키는 방식으로 동작합니다. AtomicLong처럼 단일 변수에 대해 CAS 연산이 집중되지 않기 때문에, 동시성이 높은 환경에서 충돌 가능성이 훨씬 낮습니다.

값을 조회할 때는 각 셀의 값을 합산하는 sum() 메서드를 사용하여 전체 카운트를 얻습니다. 이 방식은 “거절된 호출”처럼 읽기보다 쓰기 비율이 압도적으로 높은 통계값을 기록할 때 특히 효율적입니다.

 

🔷 Bucket 구조 살펴보기

CountBucket, TimeBucket, TotalBucket 모두 추상 클래스인 BaseBucket를 상속받습니다.

  • 하나의 Bucket은 일정 구간(1개 요청 또는 1초)의 통계만 저장합니다.
  • TotalBucket은 전체 윈도우 구간의 합을 유지합니다.
  • 오래된 Bucket이 사라지면 TotalBucket에서 removeBucket을 통해 그만큼 값을 빼줍니다.

이 구조가 O(1) 비용으로 집계 유지가 가능한 핵심 이유입니다.

package io.github.alstn113.resilience4j.metrics.impl

import io.github.alstn113.resilience4j.metrics.Metrics
import java.util.concurrent.TimeUnit

abstract class BaseBucket {

    var totalDurationMillis: Long = 0
    var numberOfCalls: Int = 0
    var numberOfFailedCalls: Int = 0

    fun record(duration: Long, durationUnit: TimeUnit, outcome: Metrics.Outcome) {
        this.numberOfCalls++
        this.totalDurationMillis += durationUnit.toMillis(duration)

        when (outcome) {
            Metrics.Outcome.SUCCESS -> {
                // no-op
            }

            Metrics.Outcome.ERROR -> {
                this.numberOfFailedCalls++
            }
        }
    }

    fun reset() {
        this.totalDurationMillis = 0
        this.numberOfCalls = 0
        this.numberOfFailedCalls = 0
    }
}
package io.github.alstn113.resilience4j.metrics.impl

class CountBucket : BaseBucket()
package io.github.alstn113.resilience4j.metrics.impl

class TimeBucket(
    private var epochSecond: Long,
) : BaseBucket() {

    fun reset(newEpochSecond: Long) {
        super.reset()
        epochSecond = newEpochSecond
    }

    fun getEpochSecond(): Long = epochSecond
}
package io.github.alstn113.resilience4j.metrics.impl

class TotalBucket : BaseBucket() {

    fun removeBucket(bucket: BaseBucket) {
        this.totalDurationMillis -= bucket.totalDurationMillis
        this.numberOfCalls -= bucket.numberOfCalls
        this.numberOfFailedCalls -= bucket.numberOfFailedCalls
    }
}

🔷 CountBasedMetrics

CountBasedMetrics는 최근 N개의 호출을 기준으로 실패율을 계산하는 슬라이딩 윈도우 구현체입니다. 설정된 slidingWindowSize만큼의 호출 결과를 유지하면서, 오래된 기록을 제거하고 새로운 기록을 채워 넣는 방식으로 동작합니다. 이를 위해 순환 배열(Ring Buffer) 형태를 사용합니다.

package io.github.alstn113.resilience4j.metrics.impl

import io.github.alstn113.resilience4j.metrics.Metrics
import io.github.alstn113.resilience4j.metrics.Snapshot
import java.util.concurrent.TimeUnit

class CountBasedMetrics(
    private val windowSize: Int,
) : Metrics {

    private val buckets: Array<CountBucket> = Array(windowSize) { CountBucket() }
    private val total: TotalBucket = TotalBucket()
    private var headIndex: Int = 0

    @Synchronized
    override fun record(
        duration: Long,
        durationUnit: TimeUnit,
        outcome: Metrics.Outcome,
    ): Snapshot {
        total.record(duration, durationUnit, outcome)
        val bucket = advanceWindow()
        bucket.record(duration, durationUnit, outcome)

        return Snapshot(total)
    }

    @Synchronized
    override fun getSnapshot(): Snapshot = Snapshot(total)

    private fun advanceWindow(): CountBucket {
        moveHeadIndex()
        val bucket = buckets[headIndex]
        total.removeBucket(bucket)
        bucket.reset()
        return bucket
    }

    private fun moveHeadIndex() {
        this.headIndex = (headIndex + 1) % windowSize
    }
}

실제 비즈니스 로직이 끝나면 CircuitBreaker는 onSuccess 또는 onError를 호출하고, 내부적으로는 metrics.record가 실행됩니다. CountBasedMetrics의 record 메서드는 다음 순서로 작동합니다.

  1. TotalBucket에 요청 결과를 즉시 반영합니다.
    전체 집계(total) 값이 항상 최신 상태가 되도록 먼저 더합니다.
  2. headIndex를 한 칸 이동합니다.
    모듈러 연산을 사용해 배열을 순환시키며, 최근 요청이 들어갈 버킷 위치를 결정합니다.
  3. 해당 위치 버킷을 TotalBucket에서 제거한 뒤 초기화합니다.
    슬라이딩 윈도우에서 가장 오래된 요청 데이터를 total에서 빼고, 해당 버킷은 reset()으로 비워 재사용할 준비를 합니다.
  4. 비워진 버킷에 이번 요청 결과를 기록합니다.
    Count 기반이므로 “요청 1개 = 버킷 1개” 방식으로 데이터를 채웁니다.
  5. 현재 TotalBucket을 Snapshot으로 만들어 반환합니다.
    Snapshot 생성은 O(1)이며, CircuitBreaker 상태 머신은 이를 근거로 상태 전이를 판단합니다.

이 구조 덕분에 버킷을 매번 새로 생성하지 않고 재사용하므로 GC 오버헤드를 최소화할 수 있습니다. 메모리 사용량은 버킷 개수(N)만큼으로 고정되며, Snapshot 조회도 이미 집계가 완료된 TotalBucket만 읽으면 되기 때문에 O(1) 성능을 보장합니다.

 

record와 getSnapshot에 @Synchronized가 적용된 이유는 TotalBucket과 headIndex가 가변 상태이기 때문입니다. 

 

🔷 TimeBasedMetrics

TimeBasedMetrics는 최근 N초 동안의 호출 결과를 기준으로 실패율을 계산하는 슬라이딩 윈도우 구현체입니다. CountBasedMetrics가 요청 N개를 기준으로 한다면, TimeBased는 시간 N초를 기준으로 윈도우를 슬라이딩합니다. 내부적으로는 1초 단위 버킷(TimeBucket)을 순환 배열(Ring Buffer) 형태로 유지합니다. 각 Bucket은 특정 epochSecond(초 단위 시간)에 고정되며, 오래된 버킷은 TotalBucket에서 제거하고 새로운 시점에 맞춰 재사용합니다.

package io.github.alstn113.resilience4j.metrics.impl

import io.github.alstn113.resilience4j.metrics.Metrics
import io.github.alstn113.resilience4j.metrics.Snapshot
import java.time.Clock
import java.util.concurrent.TimeUnit
import kotlin.math.min

class TimeBasedMetrics(
    private val windowSizeInSeconds: Int,
    private val clock: Clock,
) : Metrics {

    private val buckets: Array<TimeBucket>
    private val total: TotalBucket = TotalBucket()
    private var headIndex: Int = 0

    init {
        var now = clock.instant().epochSecond
        this.buckets = Array(windowSizeInSeconds) { TimeBucket(now++) }
    }

    @Synchronized
    override fun record(duration: Long, durationUnit: TimeUnit, outcome: Metrics.Outcome): Snapshot {
        total.record(duration, durationUnit, outcome)
        val bucket = advanceWindowToCurrentEpoch()
        bucket.record(duration, durationUnit, outcome)
        return Snapshot(total)
    }

    @Synchronized
    override fun getSnapshot(): Snapshot {
        advanceWindowToCurrentEpoch()
        return Snapshot(total)
    }

    private fun advanceWindowToCurrentEpoch(): TimeBucket {
        var currentBucket = buckets[headIndex]
        val now = clock.instant().epochSecond
        val delta = now - currentBucket.getEpochSecond()

        if (delta == 0L) return currentBucket

        var bucketsToMove = min(delta, windowSizeInSeconds.toLong())
        while (bucketsToMove > 0) {
            bucketsToMove--
            moveHeadIndex()
            currentBucket = buckets[headIndex]
            total.removeBucket(currentBucket)
            currentBucket.reset(now - bucketsToMove)
        }
        return currentBucket
    }

    private fun moveHeadIndex() {
        headIndex = (headIndex + 1) % windowSizeInSeconds
    }
}

TimeBasedMetrics의 핵심은 현재 시간(now)에 맞추어 headIndex를 이동시키고 오래된 버킷을 정리하는 과정입니다.

  1. TotalBucket에 요청 결과를 먼저 반영합니다.
    전체 집계(total)를 최신 상태로 유지하기 위해 즉시 더합니다.
  2. 현재 시각(now)에 맞는 버킷으로 슬라이딩합니다.
    advanceWindowToCurrentEpoch()가 다음을 수행합니다.
    • 현재 head가 가리키는 버킷 시간이 now와 얼마나 차이(delta) 나는지 계산
    • delta초만큼 headIndex를 앞으로 이동시키며 버킷 갱신
    • 오래된 버킷은 total에서 제거 후 reset
  3. 현재 시각 버킷에 요청 결과를 기록합니다.
  4. TotalBucket 기반의 Snapshot 반환
    집계는 이미 total에 반영되어 있으므로 Snapshot 생성은 O(1)입니다.

 

🔷 CountBasedMetrics과 TimeBasedMetrics 비교

CountBasedMetrics는 최근 N개의 호출 결과를 기준으로 실패율을 계산합니다. 구현이 단순하고 직관적이며, 고정된 크기의 버킷만 유지하면 되기 때문에 메모리 사용량이 일정하게 유지되는 장점이 있습니다. 트래픽이 안정적인 환경에서는 충분히 정확한 통계를 제공합니다. 하지만 요청 간격이 길거나 불규칙할 경우 오래된 데이터가 과도하게 반영될 수 있으며, 최근 N개의 호출만 기준으로 하기 때문에 실제 시간과는 무관합니다. 따라서 순간적인 트래픽 급증이나 폭주 상황을 포착하는 데 한계가 있습니다.

 

반면, TimeBasedMetrics는 최근 N초 동안의 호출을 기준으로 집계합니다. 트래픽이 많거나 적어도 일정 시간 단위로 실패율을 평가할 수 있어 보다 안정적인 통계를 제공합니다. 또한 최근 몇 초 동안의 실패율과 같이 운영 정책을 직관적으로 적용할 수 있으며, 순간적인 실패율 증가를 감지하는 데 유리합니다. 다만 구현이 조금 더 복잡하고, 일부 시간 버킷이 비어 있을 수 있으며, 버킷 관리와 시간 계산으로 인한 약간의 연산 오버헤드가 발생할 수 있습니다.

 

결론적으로, Count 기반은 구현 단순성과 메모리 효율성이 강점이고, Time 기반은 시간 단위 안정성과 운영 정책 반영력이 강점이라고 볼 수 있습니다.

 

🔷 마치며

 

이번 글에서는 CircuitBreaker가 상태 전이의 기준으로 활용하는 통계가 어떤 방식으로 집계되는지 살펴보았습니다. Count 기반과 Time 기반 슬라이딩 윈도우가 어떻게 버킷을 유지하고 TotalBucket을 업데이트하는지 이해하면, Resilience4j의 동작 원리가 훨씬 명확하게 보일 것입니다. 

 

다음 글인 4편: 설정, 사용, Fallback에서는 CircuitBreaker를 Spring에서 어떻게 사용하는지 알아보겠습니다.

  • YML 설정 방식
  • Spring에서의 어노테이션 기반 적용
  • Fallback 메서드의 동작 원리

읽어주셔서 감사합니다. 잘못된 부분이나 궁금한 점은 언제든 댓글로 알려주세요!

 

🔷 참고

  • Resilience4j 깃허브
  • Resilience4j 공식문서

 

 

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

선착순 쿠폰 발급 시스템 개선과 정합성 문제 해결 (Redis·Kafka)  (1) 2025.12.20
Resilience4j CircuitBreaker, 직접 구현하면서 이해해보자! - 4편: 설정, 사용, Fallback  (1) 2025.11.21
Resilience4j CircuitBreaker, 직접 구현하면서 이해해보자! - 2편: 상태 머신, 상태 전이  (0) 2025.11.21
Resilience4j CircuitBreaker, 직접 구현하면서 이해해보자! - 1편: 이해와 설계  (0) 2025.11.21
Redis 분산락, 직접 구현하면서 이해해보자! - 2편: Pub/Sub 방식  (0) 2025.10.31
'서버' 카테고리의 다른 글
  • 선착순 쿠폰 발급 시스템 개선과 정합성 문제 해결 (Redis·Kafka)
  • Resilience4j CircuitBreaker, 직접 구현하면서 이해해보자! - 4편: 설정, 사용, Fallback
  • Resilience4j CircuitBreaker, 직접 구현하면서 이해해보자! - 2편: 상태 머신, 상태 전이
  • Resilience4j CircuitBreaker, 직접 구현하면서 이해해보자! - 1편: 이해와 설계
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, 직접 구현하면서 이해해보자! - 3편: 슬라이딩 윈도우
상단으로

티스토리툴바