프로그래밍/디자인패턴

벌크헤드 패턴(Bulkhead Pattern) 완벽 정리

Jinwookoh 2025. 12. 7. 20:53

"이미지 업로드 기능이 느려져서 스레드가 다 묶였습니다. 그 때문에 텍스트만 조회하는 가벼운 게시판 기능까지 응답 불가(Timeout) 상태가 되었습니다."

하나의 톰캣(Tomcat) 스레드 풀을 모든 API가 공유해서 쓰면 발생하는 전형적인 **연쇄 장애(Cascading Failure)**입니다. 이를 막기 위해 자원(스레드, 커넥션)을 구역별로 나누는 것이 벌크헤드 패턴입니다.

Hystrix는 스레드 풀 방식을, Resilience4j는 세마포어 방식을 기본으로 채택했는데, 그 이유는 무엇일까요?


1. 방식 1: 스레드 풀 격리 (Thread Pool Isolation) - "완벽한 물리적 분리"

Netflix Hystrix가 유행시킨 방식입니다. 서비스 A 전용 스레드 풀, 서비스 B 전용 스레드 풀을 아예 따로 만듭니다.

특징

  • 별도 스레드: 호출하는 쓰레드(Tomcat)와 실행하는 쓰레드(Hystrix)가 다릅니다.
  • 비동기 가능: 작업을 별도 스레드에 던져놓고(Future), 결과를 나중에 받을 수 있습니다.

설정 예시 (Resilience4j)

YAML
resilience4j:
  thread-pool-bulkhead:
    instances:
      imageService:
        maxThreadPoolSize: 10  # 이미지 서비스는 최대 10개 스레드만 사용
        coreThreadPoolSize: 5
        queueCapacity: 20

코드 동작 개념

Java
// 톰캣 스레드가 요청을 받음
public void uploadImage() {
    // 별도의 스레드 풀에 작업을 던짐 (Context Switching 발생)
    CompletableFuture.runAsync(() -> {
        imageService.upload(); 
    }, imageServiceThreadPool); 
}

장단점

  • 장점:
    • 완벽한 격리: 이미지 서비스 스레드가 꽉 차서 멈춰도, 게시판 서비스 스레드 풀은 텅텅 비어 있어서 영향을 전혀 받지 않습니다.
    • 타임아웃 제어: 별도 스레드에서 돌기 때문에, 시간이 오래 걸리면 해당 스레드만 강제로 중단(Interrupt)시키기 쉽습니다.
  • 단점:
    • 높은 오버헤드: 스레드 간 전환(Context Switching) 비용이 발생하여 CPU 사용량이 높습니다.
    • 복잡성: ThreadLocal 등을 사용할 때 데이터 전파 처리가 필요합니다.

2. 방식 2: 세마포어 격리 (Semaphore Isolation) - "가벼운 논리적 제한"

Resilience4j가 권장하는 방식입니다. 스레드를 따로 만들지 않고, **"현재 진입 가능한 티켓(Permit) 수"**만 제한합니다.

특징

  • 동일 스레드: 호출하는 스레드가 그대로 실행까지 담당합니다. 단지 입장권(Semaphore)을 얻어야만 실행할 수 있습니다.
  • 초경량: 스레드 생성 비용이나 컨텍스트 스위칭 비용이 "0"에 가깝습니다.

설정 예시 (Resilience4j)

YAML
 
resilience4j:
  bulkhead:
    instances:
      imageService:
        maxConcurrentCalls: 10 # 동시에 10명까지만 입장 가능
        maxWaitDuration: 0ms   # 자리 없으면 대기 없이 즉시 실패(Fast Fail)

코드 동작 개념

Java
 
public void uploadImage() {
    // 세마포어 획득 시도 (티켓 남았니?)
    if (semaphore.tryAcquire()) {
        try {
            // 톰캣 스레드가 그대로 실행 (No Context Switching)
            imageService.upload();
        } finally {
            semaphore.release(); // 티켓 반납
        }
    } else {
        throw new BulkheadFullException("이미지 업로드 사용량 초과");
    }
}

장단점

  • 장점:
    • 고성능: 오버헤드가 거의 없어 처리량(Throughput)이 매우 높습니다.
    • 단순함: 스레드가 바뀌지 않으므로 트랜잭션 관리나 디버깅이 쉽습니다.
  • 단점:
    • 차단(Blocking) 가능성: 만약 I/O 타임아웃 설정을 제대로 안 하면, 톰캣 스레드 자체가 세마포어를 잡은 채로 멈춰버릴 수 있습니다. (격리는 되지만 스레드 낭비 발생)

3. 실무 비교: 언제 무엇을 쓰는가?

구분 Thread Pool Isolation (Hystrix Style) Semaphore Isolation (Resilience4j Style)
격리 수준 강함 (물리적 스레드 분리) 약함 (동시 실행 수 제한)
시스템 부하 높음 (Context Switching, Memory) 매우 낮음 (Counter Check)
비동기 처리 지원함 (Future 반환) 동기 방식에 적합
타임아웃 스레드 인터럽트로 강제 종료 가능 네트워크 타임아웃에 의존해야 함
추천 상황 네트워크 지연이 심한 외부 API 호출, 비동기 작업 높은 트래픽을 처리해야 하는 내부 마이크로서비스, 게이트웨이

결론

대부분의 고성능 마이크로서비스 환경에서는 세마포어 격리(Semaphore) 방식이 압도적으로 유리합니다. CPU를 아끼면서도 "이미지 업로드 동시 10명 제한" 같은 목적을 달성할 수 있기 때문입니다.

하지만 **"응답 시간이 예측 불가능하고, 툭하면 멈추는 레거시 시스템"**을 호출해야 한다면, 내 서비스의 스레드를 보호하기 위해 스레드 풀 격리(Thread Pool) 방식을 사용하여 아예 격리 수용소에 가둬두는 것이 안전합니다.