Skip to content

Commit 6fc58ce

Browse files
authored
✨ Feature - WebClient에 Fallback과 CircuitBreaker를 추가한다
✨ Feature - WebClient에 Fallback과 CircuitBreaker를 추가한다
2 parents 71f35d8 + 3b0222c commit 6fc58ce

5 files changed

Lines changed: 52 additions & 9 deletions

File tree

build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ dependencies {
9595
implementation 'org.springframework.boot:spring-boot-starter-webflux'
9696

9797
implementation 'io.micrometer:context-propagation:1.1.1'
98+
99+
//resilience4j
100+
implementation 'io.github.resilience4j:resilience4j-spring-boot3'
101+
implementation 'io.github.resilience4j:resilience4j-reactor'
98102
}
99103

100104
tasks.named('test') {

src/main/java/sopt/comfit/auth/kakao/service/KakaoAuthService.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,6 @@ public class KakaoAuthService {
2929
@Value("${kakao.client-id}") String clientId;
3030
@Value("${kakao.client-secret}") String clientSecret;
3131
@Value("${kakao.redirect-uri}") String redirectUri;
32-
private final JwtUtil jwtUtil;
33-
private final UserRepository userRepository;
34-
private final UniversityRepository universityRepository;
3532
private final AuthService authService;
3633

3734
public LoginQueryDto getKakaoUserInfoByCode(String code) {

src/main/java/sopt/comfit/experience/domain/Experience.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import sopt.comfit.user.domain.User;
1313

1414
import java.time.LocalDate;
15+
import java.time.ZoneId;
1516

1617
@Slf4j
1718
@Entity
@@ -114,12 +115,14 @@ public void activateDefault() {
114115

115116
//validate 메서드
116117
public static void validateDate(LocalDate startAt, LocalDate endAt) {
118+
LocalDate today = LocalDate.now(ZoneId.of("Asia/Seoul"));
119+
117120
if (endAt.isBefore(startAt)) {
118121
log.warn("종료일시가 시작일시보다 빠를 수 없습니다. startAt: {}, endAt: {}", startAt, endAt);
119122
throw BaseException.type(ExperienceErrorCode.END_DATE_BEFORE_START_DATE);
120123
}
121124

122-
if (startAt.isAfter(LocalDate.now()) || endAt.isAfter(LocalDate.now())) {
125+
if (startAt.isAfter(today) || endAt.isAfter(today)) {
123126
log.warn("종료일시 및 시작일시는 미래 날짜가 될 수 없습니다. startAt: {}, endAt: {}", startAt, endAt);
124127
throw BaseException.type(ExperienceErrorCode.NOT_ALLOWED_FUTURE_DATE);
125128
}

src/main/java/sopt/comfit/report/exception/AIReportErrorCode.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,14 @@ public enum AIReportErrorCode implements ErrorCode {
1111

1212
AI_RESPONSE_EMPTY(HttpStatus.BAD_GATEWAY, "AI_502_001", "AI 응답이 비어있습니다"),
1313
AI_RESPONSE_PARSE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "AI_500_001", "AI 응답 파싱에 실패했습니다"),
14-
AI_REPORT_NOT_FOUND(HttpStatus.NOT_FOUND, "AI_404_001", "AI 리포트를 찾을 수 없습니다");
15-
14+
AI_REPORT_NOT_FOUND(HttpStatus.NOT_FOUND, "AI_404_001", "AI 리포트를 찾을 수 없습니다"),
15+
AI_SERVICE_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE, "AI_503_001", "AI 서비스를 현재 이용할 수 없습니다."),
16+
AI_CIRCUIT_OPEN(HttpStatus.SERVICE_UNAVAILABLE, "AI_503_002", "AI 서비스 일시 중단됨"),
17+
AI_AUTH_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "AI_500_002", "AI 서비스 인증 실패"),
18+
AI_RATE_LIMITED(HttpStatus.TOO_MANY_REQUESTS, "AI_429_001", "AI 요청 한도 초과"),
19+
AI_SERVER_ERROR(HttpStatus.BAD_GATEWAY, "AI_502_002", "AI 서버 오류"),
1620

21+
;
1722
private final HttpStatus status;
1823
private final String prefix;
1924
private final String message;
Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
package sopt.comfit.report.infra.webclient;
22

3+
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
4+
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
5+
import io.github.resilience4j.retry.annotation.Retry;
6+
import io.github.resilience4j.timelimiter.annotation.TimeLimiter;
37
import lombok.RequiredArgsConstructor;
48
import lombok.extern.slf4j.Slf4j;
59
import org.springframework.stereotype.Component;
610
import org.springframework.web.reactive.function.client.WebClient;
711
import reactor.core.publisher.Mono;
12+
import sopt.comfit.global.exception.BaseException;
13+
import sopt.comfit.report.exception.AIReportErrorCode;
814
import sopt.comfit.report.infra.dto.CreateReportAiRequestDto;
915
import sopt.comfit.report.infra.dto.CreateReportAiResponseDto;
1016

@@ -14,21 +20,49 @@
1420
public class AiWebClient {
1521

1622
private final WebClient webClient;
23+
private final CircuitBreakerRegistry circuitBreakerRegistry;
1724

25+
@io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker(name = "openai", fallbackMethod = "createReportFallback")
26+
@Retry(name = "openai")
27+
@TimeLimiter(name = "openai")
1828
public Mono<CreateReportAiResponseDto> createReport(CreateReportAiRequestDto request){
1929
long startTIme = System.currentTimeMillis();
2030

2131
return webClient.post()
2232
.uri("/v1/chat/completions")
2333
.bodyValue(request)
2434
.retrieve()
35+
.onStatus(status -> status.value() == 401,
36+
response -> Mono.error(BaseException.type(AIReportErrorCode.AI_AUTH_FAILED)))
37+
.onStatus(status -> status.value() == 429,
38+
response -> Mono.error(BaseException.type(AIReportErrorCode.AI_RATE_LIMITED)))
39+
.onStatus(status -> status.value() >= 500,
40+
response -> Mono.error(BaseException.type(AIReportErrorCode.AI_SERVER_ERROR)))
2541
.bodyToMono(CreateReportAiResponseDto.class)
2642
.doOnSubscribe(subscription ->
27-
log.info("OpenAI API 호출 시작 (WebClient)"))
43+
log.info("OpenAI API 호출 시작"))
2844
.doOnSuccess(response ->
29-
log.info("OpenAI API 호출 완료(WebClient) - duration : {} ms",
45+
log.info("OpenAI API 호출 완료 - duration: {}ms",
3046
System.currentTimeMillis() - startTIme))
3147
.doOnError(error ->
32-
log.error("OpenAI API 호출 실패 (WebClient)", error));
48+
log.error("OpenAI API 호출 실패", error));
49+
}
50+
51+
52+
private Mono<CreateReportAiResponseDto> createReportFallback(
53+
CreateReportAiRequestDto request, Throwable t) {
54+
55+
CircuitBreaker cb = circuitBreakerRegistry.circuitBreaker("openai");
56+
57+
if (cb.getState() == CircuitBreaker.State.OPEN) {
58+
return Mono.error(BaseException.type(AIReportErrorCode.AI_SERVICE_UNAVAILABLE));
59+
}
60+
61+
if (t instanceof BaseException) {
62+
return Mono.error(t); // 401, 429 등 그대로
63+
}
64+
65+
log.warn("OpenAI fallback 실행 - 원인: {}", t.getMessage());
66+
return Mono.error(BaseException.type(AIReportErrorCode.AI_SERVICE_UNAVAILABLE));
3367
}
3468
}

0 commit comments

Comments
 (0)