1. EmailService가 느려짐 (응답 5초)2. OrderService가 EmailService 호출 시 대기 ┌─────────────────────────────────────────┐ │ OrderService 스레드 풀 (20개) │ │ │ │ [대기] [대기] [대기] [대기] [대기] │ │ [대기] [대기] [대기] [대기] [대기] │ │ [대기] [대기] [대기] [대기] [대기] │ │ [대기] [대기] [대기] [대기] [대기] │ │ │ │ → 모든 스레드가 EmailService 응답 대기 │ └─────────────────────────────────────────┘3. 새 주문 요청 처리 불가 → OrderService도 다운4. OrderService에 의존하는 다른 서비스도 영향→ 하나의 느린 서비스가 전체 시스템을 마비시킴
1.3 Resilience 패턴의 목표
목표
설명
장애 격리
한 서비스 장애가 다른 서비스로 전파되지 않음
빠른 실패
느린 응답보다 빠른 에러가 나음
우아한 저하
일부 기능이 안 되더라도 핵심 기능은 동작
자동 복구
장애 서비스가 복구되면 자동으로 정상화
2. Circuit Breaker 패턴
2.1 전기 차단기에서 이름을 따옴
실제 전기 차단기:과전류 발생 → 차단기 내림 → 화재 방지소프트웨어 Circuit Breaker:장애 감지 → 호출 차단 → 시스템 보호
2.2 3가지 상태
실패율 < 임계값 ┌───────────────────┐ │ │ ▼ │ ┌─────────┐ │ │ CLOSED │──────────────┘ │ (정상) │ └────┬────┘ │ 실패율 >= 임계값 ▼ ┌─────────┐ │ OPEN │ ← 모든 요청 즉시 실패 │ (차단) │ └────┬────┘ │ 대기 시간 경과 ▼ ┌─────────┐ │HALF-OPEN│ ← 일부 요청만 허용 │ (테스트) │ └────┬────┘ │ ┌───────┴───────┐ │ │ 성공률 높음 실패 계속 │ │ ▼ ▼ CLOSED OPEN
2.3 프로젝트 설정
# application.ymlresilience4j: circuitbreaker: instances: orderService: sliding-window-size: 10 # 최근 10개 요청 기준 failure-rate-threshold: 50 # 50% 이상 실패 시 OPEN wait-duration-in-open-state: 10s # 10초 후 HALF-OPEN permitted-number-of-calls-in-half-open-state: 3 # 테스트 요청 3개 slow-call-duration-threshold: 2s # 2초 이상이면 느린 호출 slow-call-rate-threshold: 50 # 느린 호출 50% 이상이면 OPEN ignore-exceptions: - com.example.marketplace.common.BusinessException # 비즈니스 예외는 무시
시나리오: DB 연결 장애 발생Time 0s: 요청 1 - 성공Time 1s: 요청 2 - 성공Time 2s: 요청 3 - 실패 (DB timeout)Time 3s: 요청 4 - 실패Time 4s: 요청 5 - 실패Time 5s: 요청 6 - 실패Time 6s: 요청 7 - 실패 → 실패율 71% (5/7) > 50% → OPEN 상태로 전환Time 7s: 요청 8 - 즉시 실패 (DB 호출 안 함)Time 8s: 요청 9 - 즉시 실패 ...Time 16s: HALF-OPEN으로 전환 요청 10 - 성공 요청 11 - 성공 요청 12 - 성공 → 3개 모두 성공 → CLOSED로 복구
3. Rate Limiter (처리율 제한)
3.1 왜 필요한가?
문제 상황:┌─────────────────────────────────────────┐│ 악의적 사용자 or 버그 있는 클라이언트 ││ ││ 초당 10,000건 요청 발생 ││ │ ││ ▼ ││ ┌─────────────────┐ ││ │ 서버 과부하 │ → 정상 사용자도 피해 ││ │ 응답 지연 │ ││ │ 메모리 부족 │ ││ └─────────────────┘ │└─────────────────────────────────────────┘
3.2 Resilience4j RateLimiter 옵션
Resilience4j는 토큰 버킷 알고리즘 기반의 RateLimiter를 제공합니다.
핵심 설정 옵션
옵션
설명
기본값
limitForPeriod
한 주기에 허용되는 요청 수
50
limitRefreshPeriod
권한(토큰)이 리프레시되는 주기
500ns
timeoutDuration
권한 획득 대기 시간 (0이면 즉시 거부)
5s
설정 상세 설명
resilience4j: ratelimiter: instances: orderCreation: limit-for-period: 10 # 주기당 10개 요청 허용 limit-refresh-period: 1s # 1초마다 토큰 리필 timeout-duration: 0s # 대기 없이 즉시 거부
# application.ymlresilience4j: ratelimiter: instances: default: limit-for-period: 100 # 1초에 100개 허용 limit-refresh-period: 1s # 1초마다 리셋 timeout-duration: 0s # 대기 없이 즉시 거부 orderCreation: limit-for-period: 10 # 주문 생성은 초당 10개만 limit-refresh-period: 1s timeout-duration: 0s
3.4 Spring Filter에서 Resilience4j 활용
// RateLimitingFilter.kt@Componentclass RateLimitingFilter( private val rateLimiterRegistry: RateLimiterRegistry) : OncePerRequestFilter() { override fun doFilterInternal( request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain ) { // 요청 경로에 따라 다른 Rate Limiter 적용 val rateLimiterName = determineRateLimiter(request) val rateLimiter = rateLimiterRegistry.rateLimiter(rateLimiterName) if (rateLimiter.acquirePermission()) { filterChain.doFilter(request, response) // 허용 } else { handleRateLimitExceeded(response) // 429 응답 } } private fun determineRateLimiter(request: HttpServletRequest): String { return when { // 주문 생성 API는 더 엄격하게 request.requestURI.startsWith("/api/v1/orders") && request.method == "POST" -> "orderCreation" else -> "default" } }}
3.5 응답 예시
HTTP/1.1 429 Too Many RequestsContent-Type: application/json{ "success": false, "code": "RATE_LIMITED", "message": "요청이 너무 많습니다. 잠시 후 다시 시도해주세요."}
3.6 Rate Limiting 알고리즘 상세
1) 고정 윈도우 (Fixed Window)
시간을 고정된 구간으로 나눠서 카운팅limit: 초당 10개00:00:00 ~ 00:00:01 (윈도우 1)├── 요청 1~10: ✅ 허용└── 요청 11: ❌ 거부00:00:01 ~ 00:00:02 (윈도우 2)├── 카운터 리셋└── 요청 1~10: ✅ 허용문제: 경계 시점 버스트────────────────────────────────────00:00:00.9에 10개 요청 ✅00:00:01.1에 10개 요청 ✅→ 0.2초 동안 20개 요청 통과 (의도한 2배!)
장점: 구현 간단, 메모리 효율적
단점: 윈도우 경계에서 버스트 허용
2) 슬라이딩 윈도우 (Sliding Window)
현재 시점 기준으로 최근 N초를 계산limit: 1초당 10개현재 시각: 00:00:01.5슬라이딩 윈도우: 00:00:00.5 ~ 00:00:01.5┌─────────────────────────────────────────────────┐│ 시간축 ││ 0.0 0.5 1.0 1.5 2.0 ││ │ │ │ │ │ ││ ├─────┴─────┤ │ ││ │ 이전 윈도우 │ │ ││ ├─────┴─────┤ ││ │ 슬라이딩 윈도우│ ← 현재 기준 │└─────────────────────────────────────────────────┘계산 방식:- 이전 윈도우 요청 수: 8개- 현재 윈도우 요청 수: 4개 (1.0~1.5에 발생)- 이전 윈도우 가중치: 50% (0.5초/1초)- 예상 요청 수: 8 * 0.5 + 4 = 8개- 10개 미만이므로 → ✅ 허용
장점: 경계 버스트 문제 해결
단점: 계산 복잡, 약간의 메모리 추가
3) 토큰 버킷 (Token Bucket) - Resilience4j 사용
버킷에 일정 속도로 토큰이 채워지고, 요청 시 토큰 소비설정: limit-for-period: 10, limit-refresh-period: 1s┌─────────────────────────────────────────────────┐│ 토큰 버킷 ││ ┌─────────────────────────────────┐ ││ │ [●][●][●][●][●][●][●][●][●][●] │ ← 10개 ││ └─────────────────────────────────┘ ││ ↑ ││ 1초마다 10개 리필 ││ (비어있는 만큼만) │└─────────────────────────────────────────────────┘시나리오:T=0.0s: 버킷 [●●●●●●●●●●] (10개)T=0.1s: 요청 5개 → [●●●●●] (5개 남음)T=0.2s: 요청 3개 → [●●] (2개 남음)T=0.3s: 요청 5개 → 2개만 처리, 3개 거부 or 대기T=1.0s: 리필 → [●●●●●●●●●●] (10개)버스트 허용:────────────────────────────────────한동안 요청이 없으면 토큰이 쌓여있음→ 순간적으로 많은 요청 처리 가능 (버스트)→ 평균적으로는 limit 유지
장점: 버스트 허용, 부드러운 제한
단점: 순간 트래픽 급증 가능
4) Leaky 버킷 (Leaky Bucket)
버킷에 요청이 쌓이고, 일정 속도로 "흘러나감"처리 속도: 초당 10개 (100ms마다 1개)┌─────────────────────────────────────────────────┐│ 요청 도착 ││ ↓ ↓ ↓ ↓ ↓ ││ ┌─────────────────────────────────┐ ││ │ [ ][ ][ ][●][●][●][●][●] │ ← 큐 ││ └─────────────────────────────────┘ ││ ↓ ││ 일정 속도로 ││ 처리 (흘러나감) ││ ↓ ││ [처리됨] │└─────────────────────────────────────────────────┘특징:- 아무리 요청이 몰려도 처리 속도는 일정- 버킷(큐)이 가득 차면 새 요청 거부- 트래픽을 "균일하게" 만듦
장점: 균일한 처리 속도, 백엔드 보호
단점: 버스트 불허, 지연 발생
알고리즘 비교 요약
알고리즘
버스트
정확도
구현 복잡도
사용처
고정 윈도우
경계에서 2배
낮음
매우 간단
간단한 API 제한
슬라이딩 윈도우
없음
높음
중간
정교한 제한 필요 시
토큰 버킷
허용
중간
중간
Resilience4j, 대부분의 경우
Leaky 버킷
없음
높음
중간
균일한 처리 필요 시
Resilience4j가 토큰 버킷을 사용하는 이유
1. 버스트 허용 - 실제 트래픽은 불균일함 - 순간적인 요청 증가를 자연스럽게 처리2. 구현 효율성 - AtomicInteger로 토큰 카운트만 관리 - 요청 히스토리 저장 불필요3. 설정 직관성 - "1초에 10개" = limit-for-period: 10, limit-refresh-period: 1s - 이해하기 쉬움
4. Bulkhead 패턴 (격벽)
4.1 배의 격벽에서 유래
배의 구조:┌─────┬─────┬─────┬─────┐│ │ │ │ ││ 격실1│ 격실2│ 격실3│ 격실4││ │ │ │ │└─────┴─────┴─────┴─────┘ │ └── 한 격실에 물이 차도 다른 격실은 안전소프트웨어 Bulkhead:┌─────────────────────────────────────────┐│ 스레드 풀 분리 ││ ││ ┌─────────┐ ┌─────────┐ ┌─────────┐ ││ │주문 처리 │ │상품 조회 │ │결제 처리 │ ││ │ 20 스레드│ │ 30 스레드│ │ 10 스레드│ ││ └─────────┘ └─────────┘ └─────────┘ ││ │ ││ └── 주문 처리가 느려져도 ││ 상품 조회는 영향 없음 │└─────────────────────────────────────────┘
4.2 프로젝트 설정
# application.ymlresilience4j: bulkhead: instances: orderService: max-concurrent-calls: 20 # 동시 처리 최대 20개 max-wait-duration: 0s # 대기 없이 즉시 거부
설정: max-concurrent-calls = 20현재 상태:┌─────────────────────────────────────────┐│ OrderService Bulkhead ││ ││ 처리 중: [1] [2] [3] ... [18] [19] [20] ││ ││ 슬롯: 20/20 사용 중 │└─────────────────────────────────────────┘새 요청 21번 도착:→ max-wait-duration: 0s 이므로 즉시 거부→ BulkheadFullException 발생→ Fallback 호출 또는 503 Service Unavailable
5. Retry 패턴
5.1 일시적 장애 대응
네트워크 일시 단절:요청 1: ❌ 실패 (네트워크 순간 끊김)요청 2: ✅ 성공 (0.5초 후 복구됨)→ 재시도하면 성공할 수 있는 상황
5.2 프로젝트 설정
# application.ymlresilience4j: retry: instances: orderService: max-attempts: 3 # 최대 3번 시도 wait-duration: 500ms # 재시도 간격 500ms retry-exceptions: - java.io.IOException # 네트워크 에러만 재시도 - java.util.concurrent.TimeoutException
5.3 코드 적용
// OrderService.kt@Retry(name = "orderService")@Bulkhead(name = "orderService")@CircuitBreaker(name = "orderService", fallbackMethod = "createOrderFallback")fun createOrder(buyerId: Long, req: CreateOrderRequest): OrderResponse { // IOException 발생 시 자동으로 재시도}
5.4 주의: 멱등성
문제 상황:┌─────────────────────────────────────────┐│ 1차 시도: 주문 생성 요청 ││ DB에 저장 완료 ││ 응답 반환 중 네트워크 끊김 ││ ││ 2차 시도: 같은 요청 재시도 ││ DB에 또 저장 → 주문 중복!! │└─────────────────────────────────────────┘해결: 멱등키(Idempotency Key) 사용POST /api/v1/ordersIdempotency-Key: abc-123-def→ 같은 키로 요청 시 이전 결과 반환 (새로 생성 안 함)
5.5 Retry vs Circuit Breaker
상황
Retry
Circuit Breaker
일시적 장애
재시도로 성공 가능
-
지속적 장애
계속 실패, 리소스 낭비
빠르게 실패, 보호
조합
Retry 먼저 → 실패 누적 → Circuit Breaker 발동
6. 패턴 조합
6.1 적용 순서
// OrderService.kt@Retry(name = "orderService") // 3. 재시도@Bulkhead(name = "orderService") // 2. 동시성 제한@CircuitBreaker(name = "orderService", fallbackMethod = "...") // 1. 차단기fun createOrder(buyerId: Long, req: CreateOrderRequest): OrderResponse { // 실행}실행 순서 (바깥쪽부터):[CircuitBreaker] → [Bulkhead] → [Retry] → [실제 로직]요청 처리 흐름:1. CircuitBreaker: OPEN이면 즉시 fallback2. Bulkhead: 슬롯 없으면 거부3. Retry: 실패 시 재시도4. 실제 로직 실행