"이미지 업로드 기능이 느려져서 스레드가 다 묶였습니다. 그 때문에 텍스트만 조회하는 가벼운 게시판 기능까지 응답 불가(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) 방식을 사용하여 아예 격리 수용소에 가둬두는 것이 안전합니다.
'프로그래밍 > 디자인패턴' 카테고리의 다른 글
| 비동기 요청-응답 패턴(Asynchronous Request-Reply) 완벽 정리 (0) | 2025.12.07 |
|---|---|
| 사이드카 패턴(Sidecar Pattern) 완벽 정리 (0) | 2025.12.07 |
| 서비스 디스커버리 패턴(Service Discovery) 완벽 정리 (0) | 2025.12.07 |
| 재시도 패턴(Retry Pattern) 완벽 정리 (0) | 2025.12.07 |
| 헬스 체크 패턴(Health Check Pattern) 완벽 정리 (0) | 2025.12.07 |