From 3859dd081c0d50fbbdbead762963cc613109bc76 Mon Sep 17 00:00:00 2001 From: wonyongChoi05 Date: Sat, 11 Nov 2023 17:02:55 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=84=9C=ED=82=B7=EB=B8=8C=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=BB=A4=EA=B0=80=20=EC=97=B4=EB=A0=A4=EC=9E=88?= =?UTF-8?q?=EB=8B=A4=EB=A9=B4=20fallback=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=EB=A5=BC=20=EC=8B=A4=ED=96=89=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 12 ++++-- .../client/WebClientGithubClient.java | 14 ++++++- .../aspect/WebClientCircuitBreakerAspect.java | 37 +++++++++++++++++++ .../config/CircuitRecordFailurePredicate.java | 16 ++++++++ .../CircuitBreakerInvalidException.java | 14 +++++++ src/main/resources/application-local.yml | 2 +- src/main/resources/application.yml | 8 ++-- 7 files changed, 94 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/integrated/techhub/resilience4j/circuitbreaker/aspect/WebClientCircuitBreakerAspect.java create mode 100644 src/main/java/com/integrated/techhub/resilience4j/circuitbreaker/config/CircuitRecordFailurePredicate.java create mode 100644 src/main/java/com/integrated/techhub/resilience4j/circuitbreaker/exception/CircuitBreakerInvalidException.java diff --git a/build.gradle b/build.gradle index 2c03d20..0e961f0 100644 --- a/build.gradle +++ b/build.gradle @@ -27,25 +27,29 @@ repositories { } dependencies { + implementation("org.springframework.boot:spring-boot-starter") + implementation("org.springframework.boot:spring-boot-starter-web") implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' - implementation 'org.springframework.boot:spring-boot-starter-web' - annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" implementation 'org.mindrot:jbcrypt:0.4' // Resilience4j-CircuitBreaker - implementation 'io.github.resilience4j:resilience4j-spring-boot3' + implementation "org.springframework.boot:spring-boot-starter-aop" + implementation group: 'io.github.resilience4j', name: 'resilience4j-spring-boot3', version: '2.1.0' + // WebClient implementation 'org.springframework.boot:spring-boot-starter-webflux' //redis implementation 'org.springframework.boot:spring-boot-starter-data-redis' - implementation("it.ozimov:embedded-redis:0.7.2") + implementation "it.ozimov:embedded-redis:0.7.2" compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' + + annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" annotationProcessor 'org.projectlombok:lombok' // JWT diff --git a/src/main/java/com/integrated/techhub/auth/application/client/WebClientGithubClient.java b/src/main/java/com/integrated/techhub/auth/application/client/WebClientGithubClient.java index 4f5559c..22c9e63 100644 --- a/src/main/java/com/integrated/techhub/auth/application/client/WebClientGithubClient.java +++ b/src/main/java/com/integrated/techhub/auth/application/client/WebClientGithubClient.java @@ -5,7 +5,10 @@ import com.integrated.techhub.auth.application.client.dto.response.GithubPrInfoResponse; import com.integrated.techhub.auth.application.client.dto.response.OAuthGithubUsernameResponse; import com.integrated.techhub.auth.application.client.dto.response.OAuthTokensResponse; +import com.integrated.techhub.resilience4j.circuitbreaker.exception.CircuitBreakerInvalidException; +import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.BodyInserters; @@ -19,6 +22,7 @@ @Component @RequiredArgsConstructor +@Slf4j public class WebClientGithubClient implements GithubClient { private static final int LAST_PAGE = 40; @@ -28,6 +32,7 @@ public class WebClientGithubClient implements GithubClient { private final GithubClientProperties githubClientProperties; @Override + @CircuitBreaker(name = "webClientGithubClient", fallbackMethod = "throwBadGateway") public OAuthTokensResponse getGithubTokens(final String code) { final String clientId = githubClientProperties.clientId(); final String clientSecret = githubClientProperties.clientSecret(); @@ -40,6 +45,7 @@ public OAuthTokensResponse getGithubTokens(final String code) { } @Override + @CircuitBreaker(name = "webClientGithubClient", fallbackMethod = "throwBadGateway") public OAuthTokensResponse getNewAccessToken(final String refreshToken) { final String clientId = githubClientProperties.clientId(); final String clientSecret = githubClientProperties.clientSecret(); @@ -53,6 +59,7 @@ public OAuthTokensResponse getNewAccessToken(final String refreshToken) { @Override @Deprecated + @CircuitBreaker(name = "webClientGithubClient", fallbackMethod = "throwBadGateway") public OAuthGithubUsernameResponse getGithubUsername(final String accessToken) { final HttpHeaders headers = new HttpHeaders(); headers.setBearerAuth(accessToken); @@ -69,11 +76,12 @@ public OAuthGithubUsernameResponse getGithubUsername(final String accessToken) { * 인증된 유저 기준 시간당 5,000회 * using: 사용자가 직접 요청하는 동기화 API * */ + @Override + @CircuitBreaker(name = "webClientGithubClient", fallbackMethod = "fallback") public List getPrsByRepoName(final String accessToken, final String repo) { final List responses = new ArrayList<>(); final List prRequestUrls = createPrApiRequestUrls(repo, LAST_PAGE); - Flux.fromIterable(prRequestUrls) .flatMap(url -> fetchPrs(accessToken, url)) .collectList() @@ -100,4 +108,8 @@ private Flux> fetchPrs(final String accessToken, fina .flux(); } + private List fallback(Throwable t) { + throw new CircuitBreakerInvalidException(); + } + } diff --git a/src/main/java/com/integrated/techhub/resilience4j/circuitbreaker/aspect/WebClientCircuitBreakerAspect.java b/src/main/java/com/integrated/techhub/resilience4j/circuitbreaker/aspect/WebClientCircuitBreakerAspect.java new file mode 100644 index 0000000..6de161b --- /dev/null +++ b/src/main/java/com/integrated/techhub/resilience4j/circuitbreaker/aspect/WebClientCircuitBreakerAspect.java @@ -0,0 +1,37 @@ +package com.integrated.techhub.resilience4j.circuitbreaker.aspect; + +import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry; +import lombok.RequiredArgsConstructor; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.stereotype.Component; + +import java.net.URI; +import java.util.Optional; + +@Aspect +@Component +@RequiredArgsConstructor +public class WebClientCircuitBreakerAspect { + + private final CircuitBreakerRegistry registry; + + @Around("execution(* org.springframework.web.reactive.function.client.WebClient.*(..)) && args(url,..)") + public Object aspect(ProceedingJoinPoint pjp, String url) throws Throwable { + return aspect(pjp, new URI(url)); + } + + @Around("execution(* org.springframework.web.reactive.function.client.WebClient.*(..)) && args(uri,..)") + public Object aspect(ProceedingJoinPoint pjp, URI uri) throws Throwable { + return registry.circuitBreaker(findHost(uri)) + .executeCheckedSupplier(pjp::proceed); + } + + private String findHost(URI uri) { + return Optional.ofNullable(uri) + .map(URI::getHost) + .orElse("default"); + } + +} diff --git a/src/main/java/com/integrated/techhub/resilience4j/circuitbreaker/config/CircuitRecordFailurePredicate.java b/src/main/java/com/integrated/techhub/resilience4j/circuitbreaker/config/CircuitRecordFailurePredicate.java new file mode 100644 index 0000000..79b93aa --- /dev/null +++ b/src/main/java/com/integrated/techhub/resilience4j/circuitbreaker/config/CircuitRecordFailurePredicate.java @@ -0,0 +1,16 @@ +package com.integrated.techhub.resilience4j.circuitbreaker.config; + +import com.integrated.techhub.common.exception.TechHubException; + +import java.util.function.Predicate; + +public class CircuitRecordFailurePredicate implements Predicate { + + @Override + public boolean test(Throwable throwable) { + if (throwable instanceof TechHubException) { + return false; + } + return true; + } +} diff --git a/src/main/java/com/integrated/techhub/resilience4j/circuitbreaker/exception/CircuitBreakerInvalidException.java b/src/main/java/com/integrated/techhub/resilience4j/circuitbreaker/exception/CircuitBreakerInvalidException.java new file mode 100644 index 0000000..d13b0b3 --- /dev/null +++ b/src/main/java/com/integrated/techhub/resilience4j/circuitbreaker/exception/CircuitBreakerInvalidException.java @@ -0,0 +1,14 @@ +package com.integrated.techhub.resilience4j.circuitbreaker.exception; + +import com.integrated.techhub.common.exception.ErrorCode; +import com.integrated.techhub.common.exception.TechHubException; + +import static org.springframework.http.HttpStatus.*; + +public class CircuitBreakerInvalidException extends TechHubException { + + public CircuitBreakerInvalidException() { + super(new ErrorCode(NOT_FOUND, "깃허브 서버가 불안정합니다. 다른 API를 사용해주세요")); + } + +} diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 362eea6..c6cd6ab 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -20,7 +20,7 @@ spring: database-platform: org.hibernate.dialect.MySQL57Dialect generate-ddl: true hibernate: - ddl-auto: update + ddl-auto: none properties: hibernate: format_sql: true diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 557f26b..51804cd 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -60,16 +60,18 @@ resilience4j: circuitbreaker: configs: default: + sliding-window-type: COUNT_BASED registerHealthIndicator: true - slidingWindowSize: 10 - minimumNumberOfCalls: 5 + slidingWindowSize: 3 + minimumNumberOfCalls: 1 permittedNumberOfCallsInHalfOpenState: 3 automaticTransitionFromOpenToHalfOpenEnabled: true waitDurationInOpenState: 5s failureRateThreshold: 50 eventConsumerBufferSize: 10 + recordFailurePredicate: com.integrated.techhub.resilience4j.circuitbreaker.config.CircuitRecordFailurePredicate instances: - orderService: + webClientGithubClient: baseConfig: default timelimiter: configs: