[E-commerce App] CircuitBreaker와 Resilience4J 사용
마이크로 서비스 간의 통신 시 발생할 수 있는 대표적인 오류와 그것을 해결할 수 있는 방법에 대해 알아보자.
Microservice 통신 시 연쇄 오류
아래 사진을 보면, user-service
를 호출했을 때, 500 Error가 발생했는데,
trace
를 보게 되면 그 원인은 order-service
에 있다는 것을 알 수 있다.
user-service
에서 order-service
를 호출하고, order-service
에서 catalog-service
를 호출한다고 가정하자.
만약 order-service
나 catalog-service
에서 오류가 발생했을 때,
해당하는 마이크로 서비스로 더이상 요청을 전달하지 않아야 한다.
따라서 Feign Client 측에서는 에러가 발생했을 때, 에러를 대신할 수 있는 default 값이라던가 정상적인 데이터처럼 보여질 수 있는 다른 데이터 값을 응답할 준비가 되어있어야 한다.
즉, 이는 user-service
의 문제가 아니므로 user-service
에서는 500 Error가 아닌 200 OK를 반환해야 한다는 것이다.
CuircuitBreaker란, 문제가 생긴 서비스를 더이상 사용하지 않도록 막아주고,
문제가 생긴 서비스가 정상적으로 복구 되어 사용할 수 있는 상태가 되었을 때 다시 사용할 수 있도록 정상적인 흐름으로 바꿔주는 장치이다.
CuircuitBreaker
- 장애가 발생하는 서비스에 반복적인 호출이 되지 못하게 차단
- 특정 서비스가 정상적으로 동작하지 않을 경우다른 기능으로 대체 수행 -> 장애 회피
정상적으로 다른 마이크로 서비스를 사용할 수 있을 때, Circuit Breaker는 closed 되어있고,
어떤 마이크로 서비스에 문제가 발생했을 때, Circuit Breaker가 open 상태가 되고,
그러면 클라이언트의 요청을 더이상 마이크로 서비스에게 전달하지 않고, Circuit Breaker에서 자체적으로 기본 값이라던가 우회할 수 있는 값을 가지고 리턴 시켜준다.
지금까지 만들었던 마이크로 서비스에 이러한 Circuit Breaker를 추가함으로써,
하나의 마이크로 서비스에서 발생한 문제가 연쇄적으로 다른 마이크로 서비스에 전달되는 것을 막을 수 있다.
-> 문제가 발생한 마이크로 서비스를 제외한 정상적인 마이크로 서비스는, 정상적으로 응답할 수 있다.
Resilience4j
2019년도 이전까지는 CircuitBreaker를 사용하기 위해서 Spring Cloud Netflix Hystrix 라이브러리를 사용할 수 있었지만,
Spring boot 2.4 버전에서는 Hystrix 라이브러리가 더이상 제공되지 않기 때문에 대체 할 수 있는 라이브러리로 바꿔야 한다.
그것이 바로 Resilience4j이다.
문제 확인
문제를 확인해보기 위해 아래 순서대로 서버를 기동하자
- eureka 서버
- rabbitmq 서버
- configuration 서버 (
config-service
) - gateway 서버 (
gateway-service
) user-service
포스트맨에서 회원가입 - 로그인 순서대로 진행하자.
로그인 후 발금받은 토큰과 userId
를 이용해 사용자의 상세정보를 요청하면, 다음과 같은 에러가 발생한다.
이는 사용자 단건 조회 시, 사용자의 주문 목록도 가져오는데,
현재 order-service
가 기동중이지 않아 발생한 오류이다.
이제 CuircuitBreaker를 구현해보자.
user-service
pom.xml
<!-- Resilience4j -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
UserServiceImpl.java
@Service
@Slf4j
public class UserServiceImpl implements UserService {
...
@Override
public UserDto getUserByUserId(String userId) {
UserEntity userEntity = userRepository.findByUserId(userId);
if (userEntity == null) {
throw new UsernameNotFoundException("User not found");
}
UserDto userDto = new ModelMapper().map(userEntity, UserDto.class);
// 주문 생성
/* ErrorDecoder */
// List<ResponseOrder> orderList = orderServiceClient.getOrders(userId);
/* 🌟 CircuitBreaker */
CircuitBreaker circuitbreaker = circuitBreakerFactory.create("circuitbreaker");
List<ResponseOrder> orderList = circuitbreaker.run(() -> orderServiceClient.getOrders(userId),
throwable -> new ArrayList<>());
userDto.setOrders(orderList);
return userDto;
}
}
테스트
이전과 마찬가지로 order-service
를 기동하지 않은 채로 “사용자 단건 조회”를 요청해보자.
이번에는 500 에러가 발생하지 않고 사용자의 기본 정보는 가져오되, 주문 내역만 가지고 오지 못하는 것을 확인할 수 있다.
참고로 인텔리제이의 콘솔에서는 아래와 같이 에러 로그가 찍힌다.
만약, order-service
를 기동한 뒤에 다시 테스트해보면, 그때는 사용자의 주문 내역까지도 정상적으로 가져올 것이다.
이번에는 일반적인 방법으로 얻어오는 것 말고 circuitbreaker를 위한 설정파일을 추가해보자.
Resilience4jConfig.java
Circuit Breaker를 커스터마이징 하기 위해서 해당 클래스를 생성한다.
@Configuration
public class Resilience4jConfig {
@Bean
public Customizer<Resilience4JCircuitBreakerFactory> globalCustomConfiguration() {
CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
.failureRateThreshold(4)
.waitDurationInOpenState(Duration.ofMillis(1000))
.slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
.slidingWindowSize(2)
.build();
TimeLimiterConfig timeLimiterConfig = TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofSeconds(4))
.build();
return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
.timeLimiterConfig(timeLimiterConfig)
.circuitBreakerConfig(circuitBreakerConfig)
.build()
);
}
}
💛 개인 공부 기록용 블로그입니다. 👻