개요
MSA(Microservices Architecture) 환경에서는 여러 서비스 간의 통신이 빈번히 이루어집니다. 하지만 특정 서비스에서 장애가 발생하거나 응답 시간이 지연되면 호출하는 서비스에도 영향을 미쳐 전체 시스템의 장애로 이어질 수 있습니다. 이 글에서는 Resilience4J의 Circuit Breaker 패턴을 적용하여 User 서버와 Ticket 서버 간 통신의 안정성을 높이는 방법을 소개합니다.
User 서버에 Resilience4J 의존성을 추가합니다.
implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'
application.yml
파일에 Circuit Breaker와 Time Limiter 설정을 추가합니다.
따로 설정하지 않은 이유는 추후 설명합니다.
spring:
cloud:
openfeign:
circuitbreaker:
enabled: true
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
resilience4j:
circuitbreaker:
configs:
default:
failure-rate-threshold: 50
slow-call-rate-threshold: 80
slow-call-duration-threshold: 5s
permitted-number-of-calls-in-half-open-state: 3
max-wait-duration-in-half-open-state: 0
sliding-window-type: COUNT_BASED
sliding-window-size: 10
minimum-number-of-calls: 10
wait-duration-in-open-state: 10s
timelimiter:
configs:
default:
timeoutDuration: 7s
cancelRunningFuture: true
Ticket 서버와 통신하는 FeignClient에 Circuit Breaker와 Fallback을 추가합니다.
@FeignClient(name = "ticketing-service", fallbackFactory = TicketingServiceClientFallbackFactory.class)
public interface TicketingServiceClient {
@GetMapping("/api/v1/ticketing/order/my")
List<TicketInfoDto> getMyTickets(@RequestParam Long userId);
}
Ticket 서버 장애 발생 시 기본 응답을 반환하는 Fallback 클래스를 작성합니다.
@Component
public class TicketingServiceClientFallbackFactory implements FallbackFactory<TicketingServiceClient> {
@Override
public TicketingServiceClient create(Throwable cause) {
return new TicketingServiceClient() {
@Override
public List<TicketInfoDto> getMyTickets(Long userId) {
return Collections.emptyList();
}
};
}
}
서킷이 OPEN 상태로 바뀌면 더 이상 요청이 전달되지 않는다. 대신 요청을 차단하고 바로 CallNotPermittedException 예외를 발생시킨다. 그러므로 각각의 예외 처리 방법에 맞게 CallNotPermittedException 예외를 처리해주어야 한다.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(CallNotPermittedException.class)
public ResponseEntity<String> handleCircuitBreakerException(CallNotPermittedException ex) {
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body("티켓 조회 서비스가 현재 사용 불가 상태입니다. 나중에 다시 시도해주세요.");
}
}
cb_default
) 반환.package com.example.ficketuser.global.config.fegin;
import feign.Target;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.CircuitBreakerNameResolver;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
@Slf4j
@Component
public class CustomCircuitBreakerNameResolver implements CircuitBreakerNameResolver {
@Override
public String resolveCircuitBreakerName(String feignClientName, Target<?> target, Method method) {
String url = target.url();
try {
String host = new URL(url).getHost();
String methodName = method.getName();
return String.format("cb_%s_%s", host, methodName);
} catch (MalformedURLException e) {
log.error("MalformedURLException 발생: {}", url, e);
return "cb_default";
}
}
}
정상 요청
서버 장애
public List<TicketInfoDto> getMyTickets(Long userId) {
throw new RuntimeException("실패 테스트");
}
지연 요청
public List<TicketInfoDto> getMyTickets(Long userId) {
try {
// 10초 대기 (시간 지연 시뮬레이션)
Thread.sleep(10000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Thread sleep interrupted", e);
}
// 마이 티켓 조회 로직
}
Resilience4J를 활용하여 User 서버와 Ticket 서버 간의 통신에 Circuit Breaker를 적용함으로써:
Reference