백엔드 시스템을 개발하다 보면 필연적으로 외부 서비스와 연동하게 됩니다. 그런데 만약 우리가 의존하는 외부 서비스가 갑자기 느려지거나 장애가 발생한다면 어떻게 될까요? 동기 방식으로 호출하는 경우, 외부 서비스의 문제는 고스란히 우리 서비스의 성능 저하와 장애로 이어질 수 있습니다.
이 글에서는 동기 방식 외부 서비스 호출 시 발생할 수 있는 문제점들을 살펴보고, 이를 해결하기 위한 핵심 전략인 타임아웃 설정, 벌크헤드 패턴, 그리고 서킷 브레이커 패턴에 대해 깊이 있게 알아보겠습니다.
동기 방식으로 외부 API를 호출하는 상황을 가정해 봅시다. 정상적인 상황에서는 문제없지만, 만약 외부 서비스의 응답이 지연되기 시작하면 어떤 일이 벌어질까요?
가장 기본적인 해결책: 타임아웃 설정!
이러한 문제를 막기 위한 가장 기본적인 방어선은 바로 타임아웃(Timeout) 설정입니다. 외부 서비스 호출 시 무한정 기다리지 않고, 일정 시간 내에 응답이 없으면 연결을 중단하거나 읽기를 포기하도록 설정하는 것입니다. 주요 타임아웃 종류는 다음과 같습니다.
타임아웃 설정, 어떻게 하는 것이 좋을까? 🤔
무작정 짧게 설정하면 정상적인 상황에서도 타임아웃이 발생할 수 있고, 너무 길게 설정하면 장애 상황을 늦게 인지하게 됩니다. 적절한 타임아웃 값을 설정하기 위한 기준은 다음과 같습니다.
기준: "한 번의 패킷 유실 정도는 재전송을 통해 복구될 수 있는 수준의 타임아웃"
중요! 사용하는 HTTP 클라이언트 라이브러리가 커넥션 타임아웃과 리드 타임아웃을 개별적으로 설정할 수 있도록 지원하는지 확인해야 합니다. 하나의
Timeout
값으로 통합 관리하는 경우도 있습니다. 가장 중요한 것은 "그냥 남들이 하니까"가 아니라, 타임아웃의 의미를 정확히 이해하고 서비스 특성에 맞는 기준을 세워 설정하는 것입니다.
타임아웃 설정만으로는 부족한 경우가 있습니다. 특히 여러 외부 서비스를 동시에 호출하거나, 하나의 자원(예: HTTP 커넥션 풀, 스레드 풀)을 여러 기능이 공유할 때 문제가 발생할 수 있습니다.
🤔 이런 상황, 어떡하죠?
이러한 장애 전파(Cascading Failure)를 막기 위한 강력한 패턴이 바로 벌크헤드 패턴(Bulkhead Pattern)입니다.
◼️ 벌크헤드 패턴이란?
선박의 격벽(Bulkhead)에서 유래한 용어입니다. 선박은 내부에 여러 격벽을 설치하여 특정 구획에 물이 차더라도 다른 구획으로 침수가 확산되는 것을 막아 배 전체의 침몰을 방지합니다.
이와 마찬가지로, 벌크헤드 패턴은 애플리케이션의 리소스를 기능별 또는 외부 서비스별로 격리하여, 일부 컴포넌트나 외부 서비스에 문제가 발생하더라도 그 영향이 시스템 전체로 퍼지지 않도록 막아주는 아키텍처 패턴입니다.
◼️ 적용 예시: 외부 서비스별 HTTP 커넥션 풀 분리
위 예시 상황에서, 서비스 A, B, C를 호출할 때 각각 별도의 HTTP 커넥션 풀을 사용하도록 구성하는 것이 벌크헤드 패턴의 적용입니다.
이렇게 하면, 서비스 A에 장애가 발생하여 커넥션 풀 A의 자원이 모두 소진되더라도, 서비스 B와 C를 호출하는 데 사용되는 커넥션 풀 B와 C는 영향을 받지 않습니다. 따라서 서비스 A의 장애가 서비스 B, C로 전파되는 것을 효과적으로 차단할 수 있습니다.
◼️ 다양한 리소스에 적용 가능
벌크헤드 패턴은 HTTP 커넥션 풀뿐만 아니라 다음과 같은 다양한 리소스에 적용될 수 있습니다.
핵심 아이디어는 "리소스 격리를 통한 장애 격리"입니다.
◼️ Apache POI 엑셀 생성 기능 장애 격리 사례
최근 대용량 데이터를 Apache POI를 사용하여 엑셀 파일로 생성하여 사용자에게 제공하는 기능을 개발했습니다. Apache POI는 큰 데이터를 다룰 때 OutOfMemoryError(OOM) 발생 가능성이 있다는 점을 인지했고, 만약 엑셀 생성 중 OOM이 발생하면 전체 서버가 다운될 수 있는 심각한 문제였습니다.
이상적인 해결책은 해당 기능을 별도의 서비스로 분리하여 물리적으로 장애를 격리하는 것이지만, 일정과 운영 비용을 고려했을 때 이는 오버 엔지니어링이라고 판단했습니다. 대신, 기존 서비스 내에 기능을 구현하되 벌크헤드 패턴을 적용하여 동시에 엑셀 생성을 요청할 수 있는 최대 스레드 수를 제어하기로 결정했습니다. Resilience4j 라이브러리의 벌크헤드 기능을 사용하여, 엑셀 생성 작업 전용으로 최대 동시 호출 수를 제한하는 세마포어 기반 벌크헤드를 설정했습니다. 이를 통해 특정 사용자의 대용량 엑셀 요청이 다른 핵심 기능에 영향을 미치지 않도록 방지할 수 있었습니다.
(참고: Apache POI 사용 시 메모리 문제를 완화하기 위해 스트리밍 방식의
SXSSFWorkbook
구현체를 사용하는 것이 좋습니다.)
◼️ Resilience4j를 활용한 벌크헤드 구현 (간단 예시)
Java 생태계에서는 Resilience4j와 같은 라이브러리를 사용하면 어노테이션이나 설정을 통해 손쉽게 벌크헤드 패턴을 적용할 수 있습니다.
의존성 추가:
implementation("io.github.resilience4j:resilience4j-spring-boot2:2.2.0") // 버전은 최신으로
설정 (application.yml
또는 application.properties
):
resilience4j:
bulkhead:
instances:
myExternalService: # 벌크헤드 이름
maxConcurrentCalls: 5 # 최대 동시 호출 수
maxWaitDuration: 0ms # 추가 요청 대기 시간 (0ms는 즉시 실패)
애플리케이션 코드 적용:
import io.github.resilience4j.bulkhead.annotation.Bulkhead;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
public class ExternalServiceClient {
private final RestTemplate restTemplate = new RestTemplate();
@Bulkhead(name = "myExternalService", type = Bulkhead.Type.SEMAPHORE) // 또는 THREADPOOL
public String callExternalService() {
// Thread.sleep(10000); // 외부 서비스 지연 시뮬레이션
return restTemplate.getForObject("http://external-service/api", String.class);
}
}
maxConcurrentCalls
를 초과하는 동시 요청이 발생하면 BulkheadFullException
이 발생하여 더 이상 해당 리소스를 점유하지 못하도록 막습니다.
타임아웃과 벌크헤드 패턴을 적용했음에도 불구하고, 외부 서비스 장애가 지속적으로 발생한다면 어떻게 될까요?
이러한 문제를 해결하기 위한 패턴이 바로 서킷 브레이커(Circuit Breaker)입니다. 전기 회로의 차단기(Circuit Breaker)에서 아이디어를 얻은 패턴으로, 오류가 일정 횟수 이상 지속되면 일정 시간 동안 해당 외부 서비스로의 요청을 자동으로 차단합니다.
◼️ 서킷 브레이커의 3가지 상태
◼️ 서킷 브레이커의 장점
Resilience4j, Hystrix(Netflix, 유지보수 모드) 등의 라이브러리를 사용하면 서킷 브레이커 패턴도 손쉽게 구현할 수 있습니다.
동기 방식으로 외부 서비스와 연동하는 것은 편리하지만, 외부 서비스의 장애는 언제든 발생할 수 있으며 이는 우리 시스템에 치명적인 영향을 미칠 수 있습니다.
- 타임아웃 설정은 외부 서비스 지연으로부터 우리 시스템을 보호하는 가장 기본적인 방어선입니다.
- 벌크헤드 패턴은 특정 기능이나 외부 서비스의 장애가 시스템 전체로 전파되는 것을 막는 효과적인 격리 전략입니다.
- 서킷 브레이커 패턴은 반복되는 외부 서비스 장애로부터 우리 시스템을 보호하고, 빠른 실패와 자동 복구 메커니즘을 제공합니다.
이러한 장애 대응 패턴들을 서비스의 특성과 요구사항에 맞게 적절히 조합하여 적용함으로써, 예측 불가능한 외부 환경 변화에도 흔들리지 않는 견고하고 안정적인 백엔드 시스템을 구축할 수 있을 것입니다. 단순히 코드를 작성하는 것을 넘어, 장애 상황까지 고려한 방어적인 프로그래밍이야말로 진정한 프로의 자세가 아닐까요?