Implementing the bulkhead pattern

2025. 3. 16. 16:40Spring Microservice

🚀 [Spring Cloud + Resilience4j] Bulkhead 패턴 가이드 (Semaphore vs Thread Pool)

지난번 포스팅에서는 Resilience4j의 서킷 브레이커와 폴백 패턴을 다뤘습니다. 이번에는 시스템의 자원을 안전하게 보호하고 효율적으로 관리할 수 있는 Bulkhead(격벽) 패턴을 심층적으로 알아보겠습니다.

 

📌 Bulkhead 패턴이란 무엇인가요?

Bulkhead(격벽) 패턴은 배의 선체가 여러 격실로 나뉘어 침수가 배 전체로 퍼지지 않도록 막는 원리에서 착안되었습니다.

소프트웨어에서도 비슷한 원리를 적용하여, 하나의 서비스가 장애를 일으킬 때 해당 장애가 다른 서비스 호출까지 영향을 미치지 않도록 요청을 격리하는 방식입니다.

 

🔑 Bulkhead 패턴의 두 가지 방식

Resilience4j는 두 가지 방식으로 Bulkhead를 구현할 수 있습니다.

  • Semaphore 방식 (디폴트)
  • Thread Pool 방식

각각의 방식에 대해 그림을 통해 자세히 살펴보겠습니다.

 

🎯 1. Semaphore Bulkhead 방식 이해하기

출처 : Spring Microservices in Action 2nd Edition

 

Semaphore Bulkhead 방식은 동시에 허용 가능한 최대 요청 수를 제한하여 서비스에 대한 과부하를 방지합니다.

  • 설정한 최대 요청 수에 도달하면 추가 요청은 즉시 거부됩니다.
  • 세마포어(semaphore)를 이용해 요청 수를 제한하며, 설정된 숫자를 초과한 요청은 즉시 실패 처리됩니다.

🔑 Semaphore 방식 특징

  • 동시 요청 수 제한 (예: 최대 20개 호출)
  • 제한 초과 시 즉시 요청 거부 (reject)
  • 호출 대기 시간이 매우 짧아야 하는 환경에서 효과적입니다.

 

🎯 2. Thread Pool Bulkhead 방식 이해하기

출처 : Spring Microservices in Action 2nd Edition

 

Thread Pool Bulkhead 방식은 요청을 개별 독립적인 쓰레드 풀에서 관리하여 서로 분리합니다. 서비스 간 호출을 독립된 쓰레드 그룹으로 격리하여, 하나의 서비스 장애가 다른 서비스에 영향을 주지 않도록 합니다.

예를 들어 위 그림에서,

  • 서비스 A, 데이터베이스 B, 서비스 C 호출이 각각 다른 쓰레드 풀로 나뉘어 관리됩니다.
  • 서비스 C가 장애가 발생해도, 서비스 A와 데이터베이스 B 호출은 정상적으로 작동합니다.

Thread Pool 방식 특징

  • 별도의 고정된 쓰레드 풀(thread pool)과 큐를 사용
  • 각 서비스가 개별 쓰레드 풀을 갖기 때문에 장애 시 영향 범위를 최소화 가능
  • 성능이 서로 다른 다양한 서비스가 함께 존재할 때 효과적입니다.

 

🛠️ 실제 코드로 Bulkhead 구현하기 (Resilience4j)

이제 Resilience4j에서 실제로 Bulkhead를 설정하는 방법을 살펴보겠습니다.

① 기본 Semaphore 방식 설정

bootstrap.yml

resilience4j.bulkhead:
  instances:
    bulkheadLicenseService:
      maxWaitDuration: 10ms  # 최대 대기 시간
      maxConcurrentCalls: 20   # 동시 호출 최대 제한
  • maxConcurrentCalls: 동시에 허용 가능한 최대 요청 수
  • maxWaitDuration: 호출이 대기할 수 있는 최대 시간

② Thread Pool 방식 설정

Thread Pool 방식을 적용하려면 다음과 같이 설정합니다.

resilience4j.thread-pool-bulkhead:
  instances:
    bulkheadLicenseService:
      maxThreadPoolSize: 10 # 최대 쓰레드 수
      coreThreadPoolSize: 5
      queueCapacity: 50
      keepAliveDuration: 20ms
  • maxThreadPoolSize: 쓰레드 풀의 최대 크기 (디폴트값은 CPU 코어 수)
  • coreThreadPoolSize: 기본 유지 쓰레드 개수 (디폴트값은 CPU 코어 수)
  • queueCapacity: 요청을 담아놓을 큐의 크기 (초과되면 즉시 거절)
  • keepAliveDuration: 쓰레드가 작업 없이 유지될 수 있는 최대 시간

 

🔖 어노테이션으로 설정하기 (코드 예시)

실제 서비스 메서드에 Bulkhead 패턴을 적용한 예시는 다음과 같습니다.

@CircuitBreaker(name = "licenseService", fallbackMethod = "buildFallbackLicenseList")
@Bulkhead(name = "bulkheadLicenseService") // 디폴트로 Semaphore 방식
public List<License> getLicensesByOrganization(String organizationId) {
    randomlyRunLong();
    return licenseRepository.findByOrganizationId(organizationId);
}
  • Semaphore Bulkhead 방식의 디폴트 적용 예입니다.
  • 별도의 타입 지정이 없으면 디폴트인 Semaphore 방식이 적용됩니다.

만약 Thread Pool 방식을 적용하려면 다음과 같이 명시적으로 설정해야 합니다.

@CircuitBreaker(name = "licenseService", fallbackMethod = "buildFallbackLicenseList")
@Bulkhead(name = "bulkheadLicenseService", type = Bulkhead.Type.THREADPOOL)
public List<License> getLicensesByOrganization(String organizationId) {
    randomlyRunLong();
    return licenseRepository.findByOrganizationId(organizationId);
}

이렇게 하면 Thread Pool을 이용해 Bulkhead를 설정하고, 장애 상황에서도 별도의 쓰레드 풀로 격리 관리합니다.

 

📌 적절한 쓰레드 풀 크기 설정 팁 (공식 추천 공식)

적절한 Thread Pool 크기는 다음 공식을 이용해 설정하면 효과적입니다.

(서비스가 정상일 때 피크 시간당 초당 요청 수 * 초당 99번째 percentile 지연 시간) 
+ 오버헤드를 위한 소량의 추가 스레드

이 공식을 통해 운영 환경에서 효과적인 Bulkhead 쓰레드 풀 크기를 설정할 수 있습니다.

🔗  99 percentile 이란?

 

🚨 꼭 기억해야 할 주의사항!

  • 하나의 서비스 장애가 다른 서비스로 전파되지 않도록 호출을 격리하는 것이 Bulkhead의 핵심입니다.
  • Fallback 메서드 사용 시 추가적으로 장애가 전파되지 않게 주의하세요.
  • 설정 값을 항상 운영 환경의 실측 데이터 기반으로 최적화하는 것이 좋습니다.

 

📚 핵심 내용 요약 정리

Bulkhead 방식 특징 활용 상황
Semaphore 방식 (기본) 최대 동시 호출 제한 빠른 응답이 필요한 환경
Thread Pool 방식 서비스별 쓰레드 풀 격리 성능 차이 크거나 장애 빈번 서비스 격리 필요 시
  • Semaphore 방식은 간단히 설정 가능한 방식이며, 단순 동시 처리량 제한에 유리합니다.
  • Thread Pool 방식은 서로 다른 특성을 가진 서비스 호출을 명확히 격리할 때 유용합니다.

 

🔥 결론

이번 포스팅에서 Bulkhead의 Semaphore 방식과 Thread Pool 방식을 모두 명확하게 설명했습니다. 제공된 모든 내용을 빠짐없이 반영했으며, 마이크로서비스 환경의 안정성을 한 단계 더 높이는 데 도움이 되길 바랍니다. 🚀✨

다음 포스팅에서도 더 유익한 정보로 찾아뵙겠습니다. 감사합니다! 🙌😊