마이크로서비스 서비스 간 API 통신 시 발생할 수 있는 문제 상황에 대한 해결책으로 FeignClient, CircuitBreaker, Zipkin 을 사용한다.
해당 프로젝트에서는 사용자의 일기장 리스트(EDIARY-DIARY) 조회 요청 시
해당 사용자 정보(AUTH-SERVICE)와 해당 사용자의 포인트 정보(EDIARY-POINT) 를 FeignClient 통신해서 클라이언트로 응답 한다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
//...(중략)
}
@RequiredArgsConstructor
@Configuration
public class FeignConfig {
@Bean
public RequestInterceptor feignInterceptor() {
return new AuthRequestInterceptor();
}
static class AuthRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
template.header(HttpHeaders.AUTHORIZATION, TokenUtil.getToken());
}
}
}
@FeignClient(name = "auth-service")
public interface AuthServiceClient {
@GetMapping(value = "/member/{id}")
ApiResult<MemberResponse> getMember(@PathVariable Long id);
}
FeignClient 와 연동해서 다른 마이크로서비스에 오류가 발생했을 때 오류 전파를 막기 위해 리트라이, 페일오버 기능 지원
@Configuration
public class Resilience4JConfig {
@Bean
public Customizer<Resilience4JCircuitBreakerFactory> globalCustomConfiguration(ContextAwareScheduledThreadPoolExecutor executor) {
CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
.failureRateThreshold(40) //실패 임계치 % 설정
.waitDurationInOpenState(Duration.ofSeconds(4)) //써킷 open 지속 시간
.slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED) //카운트 or 시간 기반으로 설정할 수 있음
.slidingWindowSize(10) // 카운트일 경우 사이즈 만큼의 마지막 요청 횟수로 실패 임계치 계산, 시간일 경우 사이즈 만큼의 seconds 로 call 집계해서 실패 임계치 계산
.build();
TimeLimiterConfig timeLimiterConfig = TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofSeconds(4)) //supplier 서비스(auth-service)의 타임아웃 오류 발생 기준 seconds
.build();
return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
.timeLimiterConfig(timeLimiterConfig)
.circuitBreakerConfig(circuitBreakerConfig)
.build()
);
}
}
@GetMapping("/{memberId}/month-list")
public ResponseEntity<ApiResult<DiaryListResponse>> findDiariesByMonth(@PathVariable Long memberId, @RequestParam String diaryYearMonth,
HttpServletRequest request) {
log.debug("start /diary/{memberId}/month-list");
List<DiaryResponse> diaryResponseList = service.findDiariesByMonth(memberId, diaryYearMonth).stream()
.map(mapper::toResponse)
.toList();
request.getHeader(HttpHeaders.AUTHORIZATION));
log.info("Authorization Token : {}", TokenUtil.getToken());
CircuitBreaker circuitbreaker = circuitBreakerFactory.create("circuitbreaker");
MemberResponse memberResponse = circuitbreaker.run(
() -> {
log.info("Authorization Async Token : {}", TokenUtil.getToken());
return authServiceClient.getMember(memberId).getResponse();
},
throwable -> {
throwable.printStackTrace();
return new MemberResponse();
}
);
PointResponse pointResponse = circuitbreaker.run(
() -> pointServiceClient.getPointByMemberId(memberId).getResponse(),
throwable -> {
throwable.printStackTrace();
return new PointResponse();
}
);
memberResponse.setPointResponse(pointResponse);
Payload payload = TokenUtil.getPayload();
log.debug(payload.toString());
log.debug("end /diary/{memberId}/month-list");
return ResponseEntity.ok(ApiResult.OK(
new DiaryListResponse(diaryResponseList, memberResponse)
));
}
1) SecurityContextHolder 전략 MODE_INHERITABLETHREADLOCAL 로 수정
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);2) ContextAwareScheduledThreadPoolExecutor 의 contextPropagators 설정
public class SecurityContextPropagator implements ContextPropagator<SecurityContext> {
@Override
public Supplier<Optional<SecurityContext>> retrieve() {
return () -> Optional.ofNullable(SecurityContextHolder.getContext());
}
@Override
public Consumer<Optional<SecurityContext>> copy() {
return context -> SecurityContextHolder.setContext(context.orElse(null));
}
@Override
public Consumer<Optional<SecurityContext>> clear() {
return context -> SecurityContextHolder.clearContext();
}
}
@Configuration
public class Resilience4JConfig {
@Bean
public ContextAwareScheduledThreadPoolExecutor contextAwareScheduledThreadPool() {
return ContextAwareScheduledThreadPoolExecutor.newScheduledThreadPool()
.corePoolSize(8)
.contextPropagators(new SecurityContextPropagator()).build();
}
@Bean
public Customizer<Resilience4JCircuitBreakerFactory> globalCustomConfiguration(ContextAwareScheduledThreadPoolExecutor executor) {
CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
.failureRateThreshold(40) //실패 임계치 % 설정
.waitDurationInOpenState(Duration.ofSeconds(4)) //써킷 open 지속 시간
.slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED) //카운트 or 시간 기반으로 설정할 수 있음
.slidingWindowSize(10) // 카운트일 경우 사이즈 만큼의 마지막 요청 횟수로 실패 임계치 계산, 시간일 경우 사이즈 만큼의 seconds 로 call 집계해서 실패 임계치 계산
.build();
TimeLimiterConfig timeLimiterConfig = TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofSeconds(4)) //supplier 서비스(auth-service)의 타임아웃 오류 발생 기준 seconds
.build();
return factory -> {
factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
.timeLimiterConfig(timeLimiterConfig)
.circuitBreakerConfig(circuitBreakerConfig)
.build()
);
factory.configureGroupExecutorService(group -> executor);
factory.configureExecutorService(executor);
};
}
}
Zipkin 연동해서 traceId 를 통해 마이크로서비스 간 요청 로깅을 추적한다.


management:
tracing:
sampling:
probability: 1.0
propagation:
consume: B3
produce: B3_MULTI
zipkin:
tracing:
endpoint: "http://localhost:9411/api/v2/spans"
logging:
level:
com.emotion.emotiondiarydiary.api: DEBUG
pattern:
level: '%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]'
front-end("front-msa" 브랜치) : https://github.com/onlydev7777/emotion-diary-react
back-end : https://github.com/onlydev7777/emotion-diary-msa
inflearn-msa : https://github.com/onlydev7777/springboot-msa-3.0/tree/master