diff --git a/common/circuit-breaker/build.gradle b/common/resilience4j/build.gradle similarity index 100% rename from common/circuit-breaker/build.gradle rename to common/resilience4j/build.gradle diff --git a/common/circuit-breaker/src/main/java/circuit/config/CircuitBreakerEventConfig.java b/common/resilience4j/src/main/java/resilience4j/config/CircuitBreakerEventConfig.java similarity index 98% rename from common/circuit-breaker/src/main/java/circuit/config/CircuitBreakerEventConfig.java rename to common/resilience4j/src/main/java/resilience4j/config/CircuitBreakerEventConfig.java index 4ff27428..86183e36 100644 --- a/common/circuit-breaker/src/main/java/circuit/config/CircuitBreakerEventConfig.java +++ b/common/resilience4j/src/main/java/resilience4j/config/CircuitBreakerEventConfig.java @@ -1,4 +1,4 @@ -package circuit.config; +package resilience4j.config; import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry; import io.github.resilience4j.circuitbreaker.event.CircuitBreakerOnErrorEvent; diff --git a/common/resilience4j/src/main/java/resilience4j/config/RetryEventConfig.java b/common/resilience4j/src/main/java/resilience4j/config/RetryEventConfig.java new file mode 100644 index 00000000..42f0dcc4 --- /dev/null +++ b/common/resilience4j/src/main/java/resilience4j/config/RetryEventConfig.java @@ -0,0 +1,30 @@ +package resilience4j.config; + +import io.github.resilience4j.retry.RetryRegistry; +import io.github.resilience4j.retry.event.RetryOnRetryEvent; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; + +@Slf4j +@Configuration +@RequiredArgsConstructor +public class RetryEventConfig { + + private final RetryRegistry retryRegistry; + + @PostConstruct + public void registerRetryEventListeners() { + retryRegistry.getAllRetries().forEach(retry -> { + retry.getEventPublisher() + .onRetry(this::logRetry); + }); + } + + private void logRetry(RetryOnRetryEvent event) { + log.info("Retry for '{}' attempt number: {}", + event.getName(), + event.getNumberOfRetryAttempts()); + } +} diff --git a/common/circuit-breaker/src/main/resources/application-circuit-breaker.yml b/common/resilience4j/src/main/resources/application-circuit-breaker.yml similarity index 73% rename from common/circuit-breaker/src/main/resources/application-circuit-breaker.yml rename to common/resilience4j/src/main/resources/application-circuit-breaker.yml index 484c1388..675c0a13 100644 --- a/common/circuit-breaker/src/main/resources/application-circuit-breaker.yml +++ b/common/resilience4j/src/main/resources/application-circuit-breaker.yml @@ -4,7 +4,18 @@ spring: circuitbreaker: enabled resilience4j: + retry: + retry-aspect-order: 2 + configs: + default: + max-attempts: 3 + wait-duration: 500ms + retry-exceptions: + - feign.RetryableException + - feign.FeignException.ServiceUnavailable + circuitbreaker: + circuit-breaker-aspect-order: 1 configs: default: registerHealthIndicator: true @@ -18,10 +29,6 @@ resilience4j: permittedNumberOfCallsInHalfOpenState: 3 recordSlowCalls: true record-exceptions: - - java.util.concurrent.TimeoutException - - java.net.SocketTimeoutException - - java.net.UnknownHostException - - java.net.ConnectException - feign.RetryableException - feign.FeignException.GatewayTimeout - feign.FeignException.BadGateway diff --git a/gateway/src/main/java/com/ticketPing/gateway/infrastructure/client/AuthWebClient.java b/gateway/src/main/java/com/ticketPing/gateway/infrastructure/client/AuthWebClient.java index 26cd113f..b8af3fd8 100644 --- a/gateway/src/main/java/com/ticketPing/gateway/infrastructure/client/AuthWebClient.java +++ b/gateway/src/main/java/com/ticketPing/gateway/infrastructure/client/AuthWebClient.java @@ -3,12 +3,11 @@ import auth.UserCacheDto; import com.ticketPing.gateway.application.client.AuthClient; import com.ticketPing.gateway.common.exception.CircuitBreakerErrorCase; -import com.ticketPing.gateway.common.exception.SecurityErrorCase; import exception.ApplicationException; import io.github.resilience4j.circuitbreaker.CallNotPermittedException; import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; +import io.github.resilience4j.retry.annotation.Retry; import io.netty.channel.ChannelOption; -import lombok.extern.slf4j.Slf4j; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.stereotype.Component; @@ -20,7 +19,6 @@ import java.time.Duration; -@Slf4j @Component public class AuthWebClient implements AuthClient { @@ -35,6 +33,7 @@ public AuthWebClient(WebClient.Builder webClientBuilder) { .build(); } + @Retry(name = "authServiceRetry") @CircuitBreaker(name = "authServiceCircuitBreaker", fallbackMethod = "validateTokenFallback") public Mono validateToken(String token) { return webClient.post() @@ -42,13 +41,7 @@ public Mono validateToken(String token) { .header("Authorization", token) .retrieve() .bodyToMono(new ParameterizedTypeReference>() {}) - .flatMap(response -> { - if (response.getData() != null) { - return Mono.just(response.getData()); - } else { - return Mono.error(new ApplicationException(SecurityErrorCase.USER_CACHE_IS_NULL)); - } - }); + .flatMap(response -> Mono.just(response.getData())); } private Mono validateTokenFallback(String token, Throwable ex) { diff --git a/gateway/src/main/java/com/ticketPing/gateway/infrastructure/config/RetryEventConfig.java b/gateway/src/main/java/com/ticketPing/gateway/infrastructure/config/RetryEventConfig.java new file mode 100644 index 00000000..6ff36fa9 --- /dev/null +++ b/gateway/src/main/java/com/ticketPing/gateway/infrastructure/config/RetryEventConfig.java @@ -0,0 +1,30 @@ +package com.ticketPing.gateway.infrastructure.config; + +import io.github.resilience4j.retry.RetryRegistry; +import io.github.resilience4j.retry.event.RetryOnRetryEvent; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; + +@Slf4j +@Configuration +@RequiredArgsConstructor +public class RetryEventConfig { + + private final RetryRegistry retryRegistry; + + @PostConstruct + public void registerRetryEventListeners() { + retryRegistry.getAllRetries().forEach(retry -> { + retry.getEventPublisher() + .onRetry(this::logRetry); + }); + } + + private void logRetry(RetryOnRetryEvent event) { + log.info("Retry for '{}' attempt number: {}", + event.getName(), + event.getNumberOfRetryAttempts()); + } +} diff --git a/gateway/src/main/java/com/ticketPing/gateway/infrastructure/filter/JwtFilter.java b/gateway/src/main/java/com/ticketPing/gateway/infrastructure/filter/JwtFilter.java index 428631a8..2cf0eee7 100644 --- a/gateway/src/main/java/com/ticketPing/gateway/infrastructure/filter/JwtFilter.java +++ b/gateway/src/main/java/com/ticketPing/gateway/infrastructure/filter/JwtFilter.java @@ -1,5 +1,7 @@ package com.ticketPing.gateway.infrastructure.filter; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.ticketPing.gateway.application.client.AuthClient; import exception.ApplicationException; import lombok.RequiredArgsConstructor; @@ -56,15 +58,33 @@ public Mono load(ServerWebExchange exchange) { return Mono.just((SecurityContext) new SecurityContextImpl(authentication)); }) - .onErrorResume(ApplicationException.class, e -> { - exchange.getResponse().setStatusCode(HttpStatus.SERVICE_UNAVAILABLE); - DataBuffer buffer = exchange.getResponse() - .bufferFactory() - .wrap(e.getMessage().getBytes(StandardCharsets.UTF_8)); - return exchange.getResponse().writeWith(Mono.just(buffer)) - .then(Mono.empty()); - }) - .onErrorResume(WebClientResponseException.Unauthorized.class, e -> Mono.empty()); + .onErrorResume(ApplicationException.class, e -> handleErrorResponse(exchange, e.getMessage(), HttpStatus.SERVICE_UNAVAILABLE)) + .onErrorResume(WebClientResponseException.Unauthorized.class, e -> { + exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); + return extractMessageFromResponse(e) + .flatMap(message -> handleErrorResponse(exchange, message, HttpStatus.UNAUTHORIZED)); + }); + } + + private Mono handleErrorResponse(ServerWebExchange exchange, String message, HttpStatus status) { + exchange.getResponse().setStatusCode(status); + DataBuffer buffer = exchange.getResponse() + .bufferFactory() + .wrap(message.getBytes(StandardCharsets.UTF_8)); + return exchange.getResponse().writeWith(Mono.just(buffer)).then(Mono.empty()); + } + + private Mono extractMessageFromResponse(WebClientResponseException e) { + String responseBody = e.getResponseBodyAsString(); + + try { + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode jsonNode = objectMapper.readTree(responseBody); + String message = jsonNode.path("message").asText(""); + return Mono.just(message); + } catch (Exception ex) { + return Mono.just(""); + } } } \ No newline at end of file diff --git a/gateway/src/main/resources/application.yml b/gateway/src/main/resources/application.yml index f4ddc3ce..2495a4e1 100644 --- a/gateway/src/main/resources/application.yml +++ b/gateway/src/main/resources/application.yml @@ -33,11 +33,24 @@ token-value: secret-key: ${USER_TOKEN_SECRET_KEY} resilience4j: + retry: + retry-aspect-order: 2 + configs: + default: + max-attempts: 3 + wait-duration: 500ms + authServiceRetry: + retry-exceptions: + - org.springframework.web.reactive.function.client.WebClientRequestException + - org.springframework.web.reactive.function.client.WebClientResponseException.ServiceUnavailable + timelimiter: configs: default: timeout-duration: 15s + circuitbreaker: + circuit-breaker-aspect-order: 1 configs: default: registerHealthIndicator: true diff --git a/services/auth/build.gradle b/services/auth/build.gradle index 882ae4ca..95d49e95 100644 --- a/services/auth/build.gradle +++ b/services/auth/build.gradle @@ -39,9 +39,9 @@ dependencies { implementation project(':common:dtos') implementation project(':common:caching') implementation project(':common:monitoring') - implementation project(':common:circuit-breaker') + implementation project(':common:resilience4j') - // MVC + // MVC implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' diff --git a/services/auth/src/main/java/com/ticketPing/auth/AuthApplication.java b/services/auth/src/main/java/com/ticketPing/auth/AuthApplication.java index 07207ca8..f4c42600 100644 --- a/services/auth/src/main/java/com/ticketPing/auth/AuthApplication.java +++ b/services/auth/src/main/java/com/ticketPing/auth/AuthApplication.java @@ -7,7 +7,7 @@ @SpringBootApplication @EnableFeignClients -@ComponentScan(basePackages = {"com.ticketPing.auth", "aop", "exception", "caching", "circuit"}) +@ComponentScan(basePackages = {"com.ticketPing.auth", "aop", "exception", "caching", "resilience4j"}) public class AuthApplication { public static void main(String[] args) { SpringApplication.run(AuthApplication.class, args); diff --git a/services/auth/src/main/java/com/ticketPing/auth/infrastructure/client/UserFeignClient.java b/services/auth/src/main/java/com/ticketPing/auth/infrastructure/client/UserFeignClient.java index c07591ce..74652f44 100644 --- a/services/auth/src/main/java/com/ticketPing/auth/infrastructure/client/UserFeignClient.java +++ b/services/auth/src/main/java/com/ticketPing/auth/infrastructure/client/UserFeignClient.java @@ -8,6 +8,7 @@ import feign.RetryableException; import io.github.resilience4j.circuitbreaker.CallNotPermittedException; import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; +import io.github.resilience4j.retry.annotation.Retry; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -18,6 +19,7 @@ @FeignClient(name = "user", configuration = CustomFeignConfig.class) public interface UserFeignClient extends UserClient { @GetMapping("/api/v1/users/login") + @Retry(name = "userServiceRetry") @CircuitBreaker(name = "userServiceCircuitBreaker", fallbackMethod = "fallbackForUserService") CommonResponse getUserByEmailAndPassword(@RequestBody UserLookupRequest userLookupRequest); diff --git a/services/auth/src/main/resources/application.yml b/services/auth/src/main/resources/application.yml index 02a8b409..e66129a3 100644 --- a/services/auth/src/main/resources/application.yml +++ b/services/auth/src/main/resources/application.yml @@ -20,6 +20,11 @@ jwt: expiration: 604800000 # 7일 resilience4j: + retry: + instances: + userServiceRetry: + baseConfig: default + circuitbreaker: instances: userServiceCircuitBreaker: diff --git a/services/order/build.gradle b/services/order/build.gradle index bff8500e..a7f765c5 100644 --- a/services/order/build.gradle +++ b/services/order/build.gradle @@ -40,9 +40,9 @@ dependencies { implementation project(':common:rdb') implementation project(':common:messaging') implementation project(':common:monitoring') - implementation project(':common:circuit-breaker') + implementation project(':common:resilience4j') - // MVC + // MVC implementation 'org.springframework.boot:spring-boot-starter-web' // Cloud diff --git a/services/order/src/main/java/com/ticketPing/order/OrderApplication.java b/services/order/src/main/java/com/ticketPing/order/OrderApplication.java index 1eff9a35..bdec7eb6 100644 --- a/services/order/src/main/java/com/ticketPing/order/OrderApplication.java +++ b/services/order/src/main/java/com/ticketPing/order/OrderApplication.java @@ -7,7 +7,7 @@ @SpringBootApplication @EnableFeignClients -@ComponentScan(basePackages = {"com.ticketPing.order", "aop", "exception", "audit", "messaging", "circuit"}) +@ComponentScan(basePackages = {"com.ticketPing.order", "aop", "exception", "audit", "messaging", "resilience4j"}) public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); diff --git a/services/order/src/main/java/com/ticketPing/order/infrastructure/client/PaymentFeignClient.java b/services/order/src/main/java/com/ticketPing/order/infrastructure/client/PaymentFeignClient.java index eee471b3..f54ad803 100644 --- a/services/order/src/main/java/com/ticketPing/order/infrastructure/client/PaymentFeignClient.java +++ b/services/order/src/main/java/com/ticketPing/order/infrastructure/client/PaymentFeignClient.java @@ -3,6 +3,7 @@ import com.ticketPing.order.application.client.PaymentClient; import com.ticketPing.order.infrastructure.config.CustomFeignConfig; import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; +import io.github.resilience4j.retry.annotation.Retry; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -18,6 +19,7 @@ @FeignClient(name = "payment", configuration = CustomFeignConfig.class) public interface PaymentFeignClient extends PaymentClient { @GetMapping("/api/v1/payments/completed") + @Retry(name = "paymentServiceRetry") @CircuitBreaker(name = "paymentServiceCircuitBreaker", fallbackMethod = "fallbackForPaymentService") ResponseEntity> getCompletedPaymentByOrderId(@RequestParam("orderId") UUID orderId); diff --git a/services/order/src/main/java/com/ticketPing/order/infrastructure/client/PerformanceFeignClient.java b/services/order/src/main/java/com/ticketPing/order/infrastructure/client/PerformanceFeignClient.java index 16f74a8c..3287413c 100644 --- a/services/order/src/main/java/com/ticketPing/order/infrastructure/client/PerformanceFeignClient.java +++ b/services/order/src/main/java/com/ticketPing/order/infrastructure/client/PerformanceFeignClient.java @@ -4,6 +4,7 @@ import com.ticketPing.order.application.client.PerformanceClient; import com.ticketPing.order.infrastructure.config.CustomFeignConfig; import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; +import io.github.resilience4j.retry.annotation.Retry; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -19,12 +20,14 @@ public interface PerformanceFeignClient extends PerformanceClient { @GetMapping("/api/v1/client/seats/{seatId}/order-info") + @Retry(name = "performanceServiceRetry") @CircuitBreaker(name = "performanceServiceCircuitBreaker", fallbackMethod = "fallbackForGetOrderInfo") ResponseEntity> getOrderInfo(@RequestHeader("X_USER_ID") UUID userId, @RequestParam("scheduleId") UUID scheduleId, @PathVariable("seatId") UUID seatId); @PostMapping("/api/v1/client/seats/{seatId}/extend-ttl") + @Retry(name = "performanceServiceRetry") @CircuitBreaker(name = "performanceServiceCircuitBreaker", fallbackMethod = "fallbackForExtendTTL") ResponseEntity> extendPreReserveTTL(@RequestParam("scheduleId") UUID scheduleId, @PathVariable("seatId") UUID seatId); diff --git a/services/order/src/main/java/com/ticketPing/order/presentation/controller/OrderController.java b/services/order/src/main/java/com/ticketPing/order/presentation/controller/OrderController.java index f8bda59d..ead51cca 100644 --- a/services/order/src/main/java/com/ticketPing/order/presentation/controller/OrderController.java +++ b/services/order/src/main/java/com/ticketPing/order/presentation/controller/OrderController.java @@ -1,5 +1,7 @@ package com.ticketPing.order.presentation.controller; +import com.ticketPing.order.application.client.PaymentClient; +import com.ticketPing.order.application.client.PerformanceClient; import com.ticketPing.order.application.dtos.OrderResponse; import com.ticketPing.order.application.service.OrderService; import com.ticketPing.order.presentation.request.CreateOrderRequest; @@ -22,6 +24,15 @@ public class OrderController { private final OrderService orderService; + private final PaymentClient performanceClient; + + @PostMapping("/test") + public ResponseEntity> test() { + performanceClient.getCompletedPaymentByOrderId(UUID.randomUUID()); + return ResponseEntity + .status(200) + .body(success()); + } @Operation(summary = "예매 좌석 생성") @PostMapping diff --git a/services/order/src/main/resources/application.yml b/services/order/src/main/resources/application.yml index a5693fe0..7e94f756 100644 --- a/services/order/src/main/resources/application.yml +++ b/services/order/src/main/resources/application.yml @@ -20,6 +20,13 @@ server: port: 10013 resilience4j: + retry: + instances: + performanceServiceRetry: + baseConfig: default + paymentServiceRetry: + baseConfig: default + circuitbreaker: instances: performanceServiceCircuitBreaker: diff --git a/settings.gradle b/settings.gradle index da4bb1cb..93e4cb97 100644 --- a/settings.gradle +++ b/settings.gradle @@ -9,7 +9,7 @@ include ("common:rdb") include ("common:caching") include ("common:messaging") include ("common:monitoring") -include ("common:circuit-breaker") +include ("common:resilience4j") include ("services:auth") include ("services:user")