CircuitBreaker는 왜 필요한가?
도메인 별로 서비스(서버)가 나뉘어지는 MSA 아키텍처에서는 특정 유저의 주문 리스트를 조회하는 경우에 다음과 같은 순서로 이루어 집니다.
1. user-service 호출
user-service / controller
@GetMapping("/users/{userId}")
public ResponseEntity<ResponseUser> getUser(@PathVariable String userId){
UserDto userDto = userService.getUserByUserId(userId);
return ResponseEntity.status(HttpStatus.OK)
.body(new ModelMapper().map(userDto, ResponseUser.class));
}
2. order-service 호출
user-service / service
@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);
// TODO order service 구현
List<ResponseOrder> orders = orderServiceClient.getOrders(userId);
userDto.setOrders(orders);
return userDto;
}
@FeignClient(name = "order-service")
public interface OrderServiceClient {
@GetMapping("/order-service/{userId}/orders")
List<ResponseOrder> getOrders(@PathVariable String userId);
}
3. order-service 반환
4. user-service 반환
이때 만약 order service가 다운되었거나 기동이 안되어 있으면 user-service까지 에러가 전파되어 500 서버 에러가 반환될 수 있습니다.
java.net.UnknownHostException: order-service
at java.base/sun.nio.ch.NioSocketImpl.connect(NioSocketImpl.java:567) ~[na:na]
at java.base/java.net.Socket.connect(Socket.java:633) ~[na:na]
at java.base/sun.net.NetworkClient.doConnect(NetworkClient.java:178) ~[na:na]
이런 경우 문제가 있는 서비스에 호출을 하지 않고 기본값이나 우회할 수 있게 예방해야 합니다.
Feign Client의 ErrorDocder는..?
public class FeignErrorDecoder implements ErrorDecoder {
@Override
public Exception decode(String methodKey, Response response) {
switch (response.status()){
case 400:
break;
case 404:
if(methodKey.contains("getOrders")){
return new ResponseStatusException(HttpStatusCode.valueOf(response.status()),
"User's order is empty");
}
break;
default:
return new Exception(response.reason());
}
return null;
}
}
Feign Client의 ErrorDecoder를 통해 500에러를 404에러로 바꿨습니다. 호출하는 서비스가 기동중이지만 에러가 발생했을 때는 ErrorDecoder가 동작하지만 호출 서비스 자체가 다운되면 에러가 전파됩니다.
CircuitBreaker
circuitBreaker를 도입하면 평소에는 정상적으로 동작하고
호출하는 서비스가 불안정하거나, 다운되면 설정한 기본값을 제공해 에러 전파를 막을 수 있습니다.
CircuitBreaker 설정 & 과정
2018년 까진 hetrix를 활용했지만 최근에는 resilience4J가 그 자리를 대체합니다.
의존성 추가
implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-circuitbreaker-resilience4j'
설정 추가
@Configuration
public class Resilience4JConfig {
@Bean
public Customizer<Resilience4JCircuitBreakerFactory> globalCustomConfiguration(){
CircuitBreakerConfig circuitBreakConfig = CircuitBreakerConfig.custom()
.failureRateThreshold(4) // 실패율, 100번에 4번 실패하면 circuitbreaker open
.waitDurationInOpenState(Duration.ofMillis(1000)) // open 상태 유지하는 시간, 이후는 half-oepn 상태로 진입
.slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED) // close 될때 통화 결과 기록(카운트 기반, 시간 기반)
.slidingWindowSize(2).build();
TimeLimiterConfig timeLimiterConfig = TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofSeconds(4)) // 호출 대기하는 time limit, limit 초과시 circuitbreaker open
.build();
return factory -> factory.configureDefault(id ->
new Resilience4JConfigBuilder(id)
.timeLimiterConfig(timeLimiterConfig)
.circuitBreakerConfig(circuitBreakConfig)
.build());
}
}
1. 외부 서비스 호출할 때 문제가 없다면 CircuitBreaker는 close 진입
2. timelimit을 초과하거나, 실패율을 초과하면 open 상태에 진입
3. open 유지 시간이 지난 뒤에 half-open 상태에 진입해 다시 외부 서비스를 호출 결과를 통해 open, close에 진입
CircuitBreaker 구현
package com.example.user.service;
import com.example.user.client.OrderServiceClient;
import com.example.user.dto.UserDto;
import com.example.user.error.FeignErrorDecoder;
import com.example.user.jpa.UserEntity;
import com.example.user.jpa.UserRepository;
import com.example.user.vo.ResponseOrder;
import lombok.RequiredArgsConstructor;
import org.modelmapper.ModelMapper;
import org.modelmapper.convention.MatchingStrategies;
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService{
....
// circuitBreakerFactory 주입
private final CircuitBreakerFactory circuitBreakerFactory;
@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);
// Fegin Client 자체 이용
// List<ResponseOrder> orders = orderServiceClient.getOrders(userId);
CircuitBreaker circuitBreaker = circuitBreakerFactory.create("circuitbreaker");
// CircuitBreak 감싼 Feign Client 이용
List<ResponseOrder> orders = circuitBreaker.run(() -> orderServiceClient.getOrders(userId),
throwable -> new ArrayList<>());
userDto.setOrders(orders);
return userDto;
}
}
Order Serivce가 down되어 있어도 fallback method를 통해 빈 List를 반환해 User Service 까지 에러가 전파되지 않습니다.