2 분 소요

마이크로 서비스 간의 통신 시 발생할 수 있는 대표적인 오류와 그것을 해결할 수 있는 방법에 대해 알아보자.

Microservice 통신 시 연쇄 오류

아래 사진을 보면, user-service를 호출했을 때, 500 Error가 발생했는데,
trace를 보게 되면 그 원인은 order-service에 있다는 것을 알 수 있다.
스크린샷 2022-10-12 오전 2 09 28

user-service에서 order-service를 호출하고, order-service에서 catalog-service를 호출한다고 가정하자.
만약 order-servicecatalog-service에서 오류가 발생했을 때,
해당하는 마이크로 서비스로 더이상 요청을 전달하지 않아야 한다.

따라서 Feign Client 측에서는 에러가 발생했을 때, 에러를 대신할 수 있는 default 값이라던가 정상적인 데이터처럼 보여질 수 있는 다른 데이터 값을 응답할 준비가 되어있어야 한다.
즉, 이는 user-service의 문제가 아니므로 user-service에서는 500 Error가 아닌 200 OK를 반환해야 한다는 것이다.
스크린샷 2022-10-12 오전 2 10 42

CuircuitBreaker란, 문제가 생긴 서비스를 더이상 사용하지 않도록 막아주고,
문제가 생긴 서비스가 정상적으로 복구 되어 사용할 수 있는 상태가 되었을 때 다시 사용할 수 있도록 정상적인 흐름으로 바꿔주는 장치이다.

CuircuitBreaker

  • 장애가 발생하는 서비스에 반복적인 호출이 되지 못하게 차단
  • 특정 서비스가 정상적으로 동작하지 않을 경우다른 기능으로 대체 수행 -> 장애 회피

스크린샷 2022-10-12 오전 2 19 48 스크린샷 2022-10-12 오전 2 22 07
정상적으로 다른 마이크로 서비스를 사용할 수 있을 때, 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를 이용해 사용자의 상세정보를 요청하면, 다음과 같은 에러가 발생한다.
스크린샷 2022-10-13 오전 9 48 56
이는 사용자 단건 조회 시, 사용자의 주문 목록도 가져오는데,
현재 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를 기동하지 않은 채로 “사용자 단건 조회”를 요청해보자.
스크린샷 2022-10-13 오전 10 06 57
이번에는 500 에러가 발생하지 않고 사용자의 기본 정보는 가져오되, 주문 내역만 가지고 오지 못하는 것을 확인할 수 있다.

참고로 인텔리제이의 콘솔에서는 아래와 같이 에러 로그가 찍힌다.
스크린샷 2022-10-13 오전 10 09 16

만약, 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()
        );
    }
}

스크린샷 2022-10-13 오전 10 42 15

스크린샷 2022-10-13 오전 10 42 40



💛 개인 공부 기록용 블로그입니다. 👻

맨 위로 이동하기