밤빵's 개발일지
[TIL]20241208 Circuit Breaker 도입 고민 본문
외부 API를 사용하는 프로젝트를 개발하면서 안정성에 대한 고민을 하게됐다. PUBG 데이터를 제공받는 샤드 기반 클래스들에서 예외 처리를 진행했지만, 예상하지 못한 외부 API의 장애나 네트워크 문제로 인해 시스템 전체가 영향을 받을 가능성을 발견했고, 그 문제를 해결하기 위해 서킷 브레이커패턴을 도입할지 고민하게 되었다.
1. 서킷 브레이커란?
서킷 브레이커는 외부 API 호출 시 발생할 수 있는 장애로부터 시스템을 보호하는 디자인 패턴이다. 정상 상태에서는 API 요청을 그대로 처리하지만, 일정 비율 이상의 요청 실패나 타임아웃이 발생하면 회로를 열어 더 이상 요청을 보내지 않는다. 일정 시간이 지나고 나면 테스트를 위해 회로를 반쯤 열어 요청을 일부 허용하고, 성공 여부에 따라 회로를 닫는 과정을 반복한다. 목적은 장애 전파를 방지하고 시스템의 안정성을 높이는 데 있다. 특히, 외부 API의 응답이 느리거나 다운되었을 때 호출을 차단해 불필요한 리소스 낭비를 줄일 수 있다.
2. 서킷 브레이커를 알게 된 계기
내 프로젝트에서는 외부 PUBG API를 호출해 데이터를 받아오는 작업이 많다. 특정 샤드에 대한 매치 정보, 시즌 정보 등을 처리한다. 하지만 외부 API는 내가 제어할 수 없는 영역이고, 종종 다음과 같은 문제가 발생할 수 있다.
- API 응답이 느리거나, 타임아웃이 발생
- 외부 서버가 다운되었을 때, 계속해서 요청을 보내면서 내 서버에도 부하가 증가
- 사용자가 즉각적인 응답을 기대하는 서비스에서 API 장애가 전파되어 전체 시스템이 느려질 수 있다.
왜 서킷 브레이커가 필요할까?
- 외부 API 장애의 전파 방지
샤드 클래스에서 외부 API 호출이 실패하면 예외가 발생하고, 이는 다른 서비스나 사용자 응답 시간에 영향을 준다.
특정 시점에서 호출이 몰릴 경우, 실패 요청이 지속적으로 누적될 가능성이 있다.
- 자원 낭비 방지
실패할 것이 확실한 요청을 계속 보내는 것은 CPU, 메모리와 같은 자원을 낭비한다.
서킷 브레이커를 통해 불필요한 호출을 막아 리소스를 효율적으로 관리할 수 있다.
- 시스템 복구 지원
장애가 발생한 API에 대해 적절한 시간 동안 호출을 중지하면, 해당 서비스가 복구될 시간을 벌 수 있다.
3. 적용 대상
현재 프로젝트에서 샤드 기반 외부 API를 사용하는 코드 중 MatchesService 클래스는 서킷 브레이커를 적용해보기에 적합한 후보라고 생각했다. 해당 클래스의 getMatches 메서드는 PUBG API에서 매치 데이터를 가져오는 역할을 한다.
public Mono<MatchesDto> getMatches(String platform, String matchId) {
return webClient.get()
.uri("{platform}/matches/{matchId}", platform, matchId)
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/vnd.api+json")
.header("Accept", "application/vnd.api+json")
.retrieve()
.bodyToMono(MatchesDto.class)
.doOnNext(this::saveOrUpdateData)
.doOnError(e -> e.printStackTrace());
}
→ 현재 이 메서드는 예외가 발생할 경우 로그를 출력하는 수준에 머물러 있다. 만약 API가 불안정한 상태라면 해당 메서드가 계속 호출되면서 시스템이 불안정해질 가능성이 있다.
@Service
public class MatchesService {
private final WebClient webClient;
public MatchesService(WebClient.Builder webClientBuilder) {
this.webClient = webClientBuilder.baseUrl("https://api.pubg.com/shards/").build();
}
public Mono getMatches(String platform, String matchId) {
return webClient.get()
.uri("{platform}/matches/{matchId}", platform, matchId)
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/vnd.api+json")
.retrieve()
.bodyToMono(MatchesDto.class);
}
}
→ WebClient를 사용해 외부 API를 호출하고 있다. 문제는 API 서버가 비정상적일 경우 계속해서 호출이 쌓이며 시스템에 영향을 준다는 점이었다. 이를 해결하기 위해 서킷 브레이커를 적용하고자 했다.
4. 적용에 대한 고민
서킷 브레이커를 어떻게 적용할까?
Spring Cloud Resilience4j를 활용하면 쉽게 서킷 브레이커를 적용할 수 있다고 한다. 하지만 Resilience4j 설정 방법과 기존 코드와의 통합이 아직 익숙하지 않다.
실패 기준은 어떻게 설정해야 할까?
서킷 브레이커는 실패율과 호출 횟수 같은 기준을 설정해야 한다. 예를 들어, 50% 이상의 요청이 실패했을 때 회로를 열 것인지, 혹은 5초 동안 3번 연속 실패하면 열 것인지 등 고민이 많다.
Fallback 로직은 어떻게 작성할까?
API 호출이 차단되었을 때 대체 데이터를 제공하거나, 적절한 오류 메시지를 반환해야 한다. 하지만 어떤 데이터를 대체로 제공할지 결정하기 어렵다.
서킷 브레이커가 항상 필요한가?
프로젝트의 규모와 중요도에 따라 서킷 브레이커를 적용하지 않아도 되는 경우가 있을 수 있다. 단순한 시스템이라면 오히려 복잡성을 증가시킬 가능성도 있다.
5. 서킷 브레이커 도입 시 기대 효과
- 외부 API 장애가 발생해도 빠르게 사용자에게 실패를 알리고, 시스템 전체의 장애로 번지는 것을 막을 수 있다.
- 장애 상황에서 적절한 대체 데이터를 반환하거나, 서비스가 복구될 때까지 대기할 수 있다.
6. 다음 단계 계획
- Resilience4j 서킷 브레이커를 적용해보기 위해 간단한 테스트 환경을 만들어본다.
- MatchesService 클래스에 서킷 브레이커를 적용하고 실패 기준과 Fallback 로직을 설정해본다.
- 서킷 브레이커 적용 전후의 성능과 안정성을 비교해본다.
- 필요하다면 다른 서비스 클래스에도 점진적으로 적용한다.
7. 서킷 브레이커 적용 과정
7-1 라이브러리 선택
Spring Boot에서는 Resilience4j를 주로 사용한다는 것을 알게 되었다. Spring Cloud Circuit Breaker도 사용 가능했지만, Resilience4j가 더 가볍고 설정이 유연하다고 판단했다.
7-2 Resilience4j 의존성 추가
Gradle에 아래와 같이 의존성을 추가한다.
dependencies {
implementation 'io.github.resilience4j:resilience4j-spring-boot2'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
}
7-3 CircuitBreaker 설정
Resilience4j에서는 @CircuitBreaker 어노테이션을 활용해 서킷 브레이커를 간단히 적용할 수 있다.
@Service
public class MatchesService {
private final WebClient webClient;
public MatchesService(WebClient.Builder webClientBuilder) {
this.webClient = webClientBuilder.baseUrl("https://api.pubg.com/shards/").build();
}
@CircuitBreaker(name = "matchesCircuitBreaker", fallbackMethod = "fallbackGetMatches")
public Mono getMatches(String platform, String matchId) {
return webClient.get()
.uri("{platform}/matches/{matchId}", platform, matchId)
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/vnd.api+json")
.retrieve()
.bodyToMono(MatchesDto.class);
}
public Mono fallbackGetMatches(String platform, String matchId, Throwable t) {
// Fallback 처리: 기본값 반환 또는 사용자 정의 에러 처리
return Mono.error(new CustomException(ErrorMsg.EXTERNAL_API_ERROR));
}
}
→ @CircuitBreaker 어노테이션은 서킷 브레이커를 적용하고, fallbackMethod는 API 호출 실패 시 실행할 메서드를 지정한다.
8. 적용 후 예상 결과
API 장애 상황에서 안정성 증가
API 호출 실패 시 빠르게 차단되어 시스템 리소스 낭비가 줄어든다. 장애가 전파되지 않아 다른 서비스에 영향을 주지 않는다.
사용자 경험 향상
Fallback 메서드에서 기본값을 반환하거나 사용자 친화적인 에러 메시지를 전달함으로써, 사용자에게 즉각적인 피드백을 제공할 수 있다.
API 호출 성공률과 실패율 분석 가능
Resilience4j에서 제공하는 모니터링 도구를 활용해 서킷 브레이커의 상태, 호출 성공/실패 비율 등을 시각적으로 확인할 수 있다.
서킷 브레이커는 외부 API와의 연동에서 안정성 보장 장치로 보인다. 장애 상황에서의 서비스 설계와 단순히 API 호출을 차단하는 것을 넘어 대응 방법을 고민하게 해주는 중요한 패턴이지만 새로운 기술을 도입하는 것은 항상 어려운 일이다. 예시를 적용하는 과정에서 많은것을 배울수 있었고, 도입 여부와 상관없이 이번 고민은 시스템 설계의 중요성을 배우는 계기가 되었다.
'개발Article' 카테고리의 다른 글
[TIL]20241210 알림 기능 추가에 대한 고민 (1) | 2024.12.10 |
---|---|
[TIL]20241209 JWT 기반 로그아웃 처리와 블랙리스트 방식 (2) | 2024.12.09 |
[TIL]20241207 Enum은 왜 쓰는걸까? (2) | 2024.12.07 |
[TIL]20241206 equals()와 hashCode()를 재정의 해야하는 이유? (2) | 2024.12.06 |
[TIL]20241205 Event Publisher (1) | 2024.12.05 |