Skip to content

in-370-외부-api-호출-bulkhead-패턴-사용 -> develop#223

Open
HanYoonSoo wants to merge 4 commits into
developfrom
in-370-외부-api-호출-bulkhead-패턴-사용

Hidden character warning

The head ref may contain hidden characters: "in-370-\uc678\ubd80-api-\ud638\ucd9c-bulkhead-\ud328\ud134-\uc0ac\uc6a9"
Open

in-370-외부-api-호출-bulkhead-패턴-사용 -> develop#223
HanYoonSoo wants to merge 4 commits into
developfrom
in-370-외부-api-호출-bulkhead-패턴-사용

Conversation

@HanYoonSoo

@HanYoonSoo HanYoonSoo commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Issue


comment

  • Resilience4j Bulkhead/RateLimiter를 도입해 YouTube Data API, YouTube Analytics API, OpenAI 호출의 동시 실행과 순간 호출량을 제한했습니다.
  • 채널 동기화의 YouTube Analytics 영상별 조회를 4개 단위 배치 병렬 처리로 변경하고, 공용 VirtualThread executor를 사용하도록 정리했습니다.
  • 제한 permit 획득 실패를 외부 API 429 예외로 매핑하고, Analytics 영상별 부분 실패는 기존처럼 기본값 또는 빈 결과로 처리되도록 테스트를 추가했습니다.
  • YouTube 동기화는 실제 호출 수가 영상 수에 비례해 커질 수 있어, 이번 PR에서는 호출 개수 자체를 줄이기보다 처리 시간과 서버 보호 사이의 절충안으로 제한값과 대기 시간을 설정했습니다.
  • 추후 실제 운영 부하와 사용자 경험을 보면서 Bulkhead/RateLimiter 수치, 재시도 여부, 비동기 job 전환까지 대화하며 절충안을 찾으면 좋겠습니다.

Summary by CodeRabbit

  • New Features

    • 외부 API 호출에 동시성 제한과 호출 속도 제한이 적용되어, 트래픽이 많을 때도 더 안정적으로 동작합니다.
    • 유튜브 분석 및 검색, 오픈AI 요청 처리에 실패 시 일관된 오류 응답이 추가되었습니다.
    • 트렌드 분석과 채널 분석이 더 빠르게 처리되도록 병렬 처리 방식이 개선되었습니다.
  • Bug Fixes

    • 외부 API 제한 상황에서 적절한 429 오류로 반환되도록 개선했습니다.
    • 일부 외부 호출 실패가 전체 작업을 중단시키지 않도록 처리했습니다.

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Resilience4j의 Bulkhead와 RateLimiter를 유튜브 관련 API 클라이언트 및 OpenAiService에 도입하고, 각각의 fallback 메서드에서 429 예외로 매핑하는 공통 유틸을 추가했다. 유튜브 채널 애널리틱스 조회와 업로드 주기 계산을 가상 스레드 ExecutorService 기반 배치/병렬 처리로 전환했다. 관련 설정(application.yml, ExternalApiExecutorConfig)과 테스트가 함께 추가/수정되었다.

Changes

Resilience4j 도입 및 외부 API 병렬 처리

Layer / File(s) Summary
의존성/Executor/에러 정의 기반
build.gradle, .../config/ExternalApiExecutorConfig.java, .../exception/ErrorDefine.java, .../exception/ExternalApiLimitExceptionMapper.java, .../exception/ExternalApiLimitExceptionMapperTest.java, src/main/resources/application.yml
Actuator/Resilience4j 의존성 추가, 가상 스레드 ExecutorService 빈 등록, 429 에러 코드 및 예외 매핑 유틸/테스트, resilience4j 설정 블록 추가.
외부 API 클라이언트 Bulkhead/RateLimiter 및 Fallback
.../client/YoutubeDataApiClient.java, .../client/YoutubeAnalyticsApiClient.java, .../client/YoutubeSearchApiClient.java, .../openai/service/OpenAiService.java
각 외부 호출 메서드에 @Bulkhead/@ratelimiter 적용, 전용 fallback 메서드가 ExternalApiLimitExceptionMapper로 예외를 변환.
유튜브 채널 애널리틱스 비동기 배치 처리
.../channel/service/sync/YoutubeChannelAnalyticsFetchService.java, .../channel/service/sync/YoutubeChannelAnalyticsFetchServiceTest.java
영상/시청유지율 조회를 chunked 배치+CompletableFuture 병렬 실행으로 전환, 실패 시 기본값/빈 리스트 처리, 동시성/부분실패 테스트 추가.
업로드 주기 병렬 계산
.../brandcollaboration/service/BrandCollaborationService.java
computeAvgUploadDays가 ExecutorService 기반 병렬 계산으로 변경, computeUploadDays 헬퍼 분리.

VideoServiceTest 인증 방식 변경

Layer / File(s) Summary
UUID 기반 인증 및 단일 인자 호출 전환
src/test/java/.../VideoServiceTest.java
사용자 식별자를 UUID로 전환하고 getDropPoints/getRetention 호출을 videoId 단일 인자로 통일.

AnalyticsCalculatorTest 인자 타입 정리

Layer / File(s) Summary
formatTime 인자 타입 수정
src/test/java/.../AnalyticsCalculatorTest.java
formatTime 호출의 double 리터럴을 정수 리터럴로 수정.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant YoutubeDataApiClient
  participant Resilience4j
  participant ExternalApiLimitExceptionMapper
  Client->>YoutubeDataApiClient: getYoutubeChannels(...)
  YoutubeDataApiClient->>Resilience4j: Bulkhead/RateLimiter 확인
  alt 제한 초과
    Resilience4j-->>YoutubeDataApiClient: BulkheadFullException/RequestNotPermitted
    YoutubeDataApiClient->>ExternalApiLimitExceptionMapper: toRuntimeException(throwable)
    ExternalApiLimitExceptionMapper-->>Client: ApiException(EXTERNAL_API_RATE_LIMITED)
  else 정상
    YoutubeDataApiClient-->>Client: 결과 반환
  end
Loading
sequenceDiagram
  participant Service as YoutubeChannelAnalyticsFetchService
  participant Executor as externalApiExecutor
  participant ApiClient as YoutubeAnalyticsApiClient
  Service->>Service: chunked(videos, BATCH_SIZE)
  loop 배치마다
    Service->>Executor: supplyAsync(fetchVideoAnalyticsForVideo)
    Executor->>ApiClient: getYoutubeAnalytics(...)
    ApiClient-->>Executor: 결과 또는 예외
  end
  Executor-->>Service: join 결과 취합 및 필터링
Loading

Possibly related PRs

  • JECT-Study/inflace-4th-server#130: BrandCollaborationService.search에서 youtubeSearchApiClient.search 호출부가 확장된 시그니처와 호환되도록 맞춘 작업으로 직접 연결됩니다.
  • JECT-Study/inflace-4th-server#199: 두 PR 모두 YoutubeChannelAnalyticsFetchService의 외부 API 호출 흐름을 다루며 본 PR의 배치/병렬 실행 변경과 직접 관련됩니다.

Suggested reviewers: youcastle03, MINUUUUUUUUUUUU

Poem

토끼는 코드밭을 깡총깡총 뛰며 🐇
Bulkhead 담장 너머 API를 지켜보네
가상 스레드 타고 배치로 달려가
429 만나도 폴백으로 사뿐히 착지
오늘도 안전하게, 병렬로 무사히!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목이 외부 API 호출에 Bulkhead 패턴을 적용한다는 핵심 변경을 잘 반영합니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch in-370-외부-api-호출-bulkhead-패턴-사용

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@HanYoonSoo HanYoonSoo self-assigned this Jul 8, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/main/java/com/example/inflace/global/client/YoutubeAnalyticsApiClient.java (1)

98-107: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Fallback 로그 메시지가 API 호출 실패 상황에서 오해의 소지가 있습니다.

youtubeAnalyticsFallback은 Bulkhead/RateLimiter 거부뿐 아니라 API 호출 자체가 실패(4xx/5xx)한 경우에도 호출됩니다. 현재 로그 메시지 "blocked or failed before execution"는 후자의 경우 실제로는 API 호출이 실행되었음에도 "before execution"으로 표현되어 디버깅 시 혼란을 줄 수 있습니다.

🔧 제안: 로그 메시지 개선
     log.warn("YouTube Analytics API call blocked or failed before execution. googleId={} metrics={} dimensions={}",
             googleId, request.formattedMetricsList(), request.dimensions(), throwable);
-    log.warn("YouTube Analytics API call blocked or failed before execution. googleId={} metrics={} dimensions={}",
-            googleId, request.formattedMetricsList(), request.dimensions(), throwable);
+    log.warn("YouTube Analytics API call rejected or failed. googleId={} metrics={} dimensions={}",
+            googleId, request.formattedMetricsList(), request.dimensions(), throwable);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/example/inflace/global/client/YoutubeAnalyticsApiClient.java`
around lines 98 - 107, The fallback log message in youtubeAnalyticsFallback is
misleading because it says the call was “blocked or failed before execution”
even when the API request actually ran and then failed. Update the log text in
YoutubeAnalyticsApiClient.youtubeAnalyticsFallback to clearly distinguish
pre-execution rejections from downstream API failures, while still logging
googleId, request.formattedMetricsList(), request.dimensions(), and the
throwable for debugging.
src/main/java/com/example/inflace/domain/channel/service/sync/YoutubeChannelAnalyticsFetchService.java (1)

613-619: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

chunked 메서드가 YoutubeDataApiClient에도 동일하게 존재하여 중복됩니다.

그래프 컨텍스트에 따르면 YoutubeDataApiClient.java의 613-619번 라인에 동일한 구현의 private chunked 메서드가 있습니다. 두 클래스에서 같은 유틸리티 로직이 중복되고 있으므로 공용 유틸 클래스로 추출을 고려해 보세요.

♻️ 제안: 공용 유틸 클래스로 추출
-    private <T> List<List<T>> chunked(List<T> values, int size) {
-        List<List<T>> chunks = new ArrayList<>();
-        for (int start = 0; start < values.size(); start += size) {
-            chunks.add(values.subList(start, Math.min(values.size(), start + size)));
-        }
-        return chunks;
-    }
// 예: src/main/java/com/example/inflace/global/util/ListUtils.java
public final class ListUtils {
    private ListUtils() {}
    
    public static <T> List<List<T>> chunked(List<T> values, int size) {
        List<List<T>> chunks = new ArrayList<>();
        for (int start = 0; start < values.size(); start += size) {
            chunks.add(values.subList(start, Math.min(values.size(), start + size)));
        }
        return chunks;
    }
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/example/inflace/domain/channel/service/sync/YoutubeChannelAnalyticsFetchService.java`
around lines 613 - 619, `chunked` 로직이 `YoutubeChannelAnalyticsFetchService`와
`YoutubeDataApiClient`에 중복되어 있으니, 두 곳의 private 메서드를 공용 유틸로 합치세요. `chunked`를
`ListUtils` 같은 재사용 가능한 클래스의 static 메서드로 추출하고,
`YoutubeChannelAnalyticsFetchService`와 `YoutubeDataApiClient`는 그 공용 메서드를 호출하도록
변경해 중복 구현을 제거하세요.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@build.gradle`:
- Around line 28-29: Update the resilience4j dependency to the latest stable
release by changing the io.github.resilience4j:resilience4j-spring-boot3 version
from 2.2.0 to 2.4.0 in build.gradle. Keep the existing Spring Boot actuator
dependency unchanged and make sure the version bump is applied wherever the
resilience4j artifact is declared.

In
`@src/main/java/com/example/inflace/domain/brandcollaboration/service/BrandCollaborationService.java`:
- Around line 275-294: `computeAvgUploadDays` is waiting indefinitely on YouTube
requests, so add an actual response timeout instead of relying on the
rate-limiter setting alone. Update the async flow around
`CompletableFuture.supplyAsync(...)` and `CompletableFuture::join` to use
`orTimeout()` (or equivalent) for the upload-day computation, and configure the
`RestClient` used by `computeUploadDays` with a read timeout so slow HTTP
responses fail fast. Keep the existing exception handling/logging in
`BrandCollaborationService` to surface timeout failures clearly.

In `@src/main/java/com/example/inflace/global/client/YoutubeDataApiClient.java`:
- Around line 90-91: The current `@RateLimiter` on YoutubeDataApiClient only
limits each method invocation once, so it does not control the individual API
requests made inside the pagination/chunk loops. Review getMyVideoIds and
getYoutubeVideos and move rate limiting to the actual YouTube API call points
inside those loops, or switch to HTTP-client-level throttling, while keeping
`@Bulkhead` on the outer methods for concurrency control. Ensure the
fallbackMethod wiring still works after refactoring and that each remote call is
counted against the limiter rather than the enclosing method.

---

Nitpick comments:
In
`@src/main/java/com/example/inflace/domain/channel/service/sync/YoutubeChannelAnalyticsFetchService.java`:
- Around line 613-619: `chunked` 로직이 `YoutubeChannelAnalyticsFetchService`와
`YoutubeDataApiClient`에 중복되어 있으니, 두 곳의 private 메서드를 공용 유틸로 합치세요. `chunked`를
`ListUtils` 같은 재사용 가능한 클래스의 static 메서드로 추출하고,
`YoutubeChannelAnalyticsFetchService`와 `YoutubeDataApiClient`는 그 공용 메서드를 호출하도록
변경해 중복 구현을 제거하세요.

In
`@src/main/java/com/example/inflace/global/client/YoutubeAnalyticsApiClient.java`:
- Around line 98-107: The fallback log message in youtubeAnalyticsFallback is
misleading because it says the call was “blocked or failed before execution”
even when the API request actually ran and then failed. Update the log text in
YoutubeAnalyticsApiClient.youtubeAnalyticsFallback to clearly distinguish
pre-execution rejections from downstream API failures, while still logging
googleId, request.formattedMetricsList(), request.dimensions(), and the
throwable for debugging.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d20eef6f-4ac0-4735-9e4c-49f7b89a4081

📥 Commits

Reviewing files that changed from the base of the PR and between c4690f3 and 4abca99.

📒 Files selected for processing (15)
  • build.gradle
  • src/main/java/com/example/inflace/domain/brandcollaboration/service/BrandCollaborationService.java
  • src/main/java/com/example/inflace/domain/channel/service/sync/YoutubeChannelAnalyticsFetchService.java
  • src/main/java/com/example/inflace/global/client/YoutubeAnalyticsApiClient.java
  • src/main/java/com/example/inflace/global/client/YoutubeDataApiClient.java
  • src/main/java/com/example/inflace/global/client/YoutubeSearchApiClient.java
  • src/main/java/com/example/inflace/global/config/ExternalApiExecutorConfig.java
  • src/main/java/com/example/inflace/global/exception/ErrorDefine.java
  • src/main/java/com/example/inflace/global/exception/ExternalApiLimitExceptionMapper.java
  • src/main/java/com/example/inflace/infra/openai/service/OpenAiService.java
  • src/main/resources/application.yml
  • src/test/java/com/example/inflace/domain/channel/service/sync/YoutubeChannelAnalyticsFetchServiceTest.java
  • src/test/java/com/example/inflace/domain/video/service/VideoServiceTest.java
  • src/test/java/com/example/inflace/global/exception/ExternalApiLimitExceptionMapperTest.java
  • src/test/java/com/example/inflace/global/util/AnalyticsCalculatorTest.java

Comment thread build.gradle
Comment on lines +28 to +29
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.github.resilience4j:resilience4j-spring-boot3:2.2.0'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

What is the latest stable version of resilience4j-spring-boot3? Is version 2.2.0 the latest?

💡 Result:

The latest stable version of resilience4j-spring-boot3 is 2.4.0, which was released on March 14, 2026 [1][2]. Therefore, version 2.2.0 is not the latest version [1][3]. You can find the latest releases in the Maven Central repository [4][1].

Citations:


Resilience4j 버전을 최신 안정판으로 올려주세요.

io.github.resilience4j:resilience4j-spring-boot3의 최신 안정 버전은 2.4.0이므로, 현재 2.2.0은 구버전입니다. 보안 패치와 버그 수정을 반영하려면 업그레이드가 필요합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@build.gradle` around lines 28 - 29, Update the resilience4j dependency to the
latest stable release by changing the
io.github.resilience4j:resilience4j-spring-boot3 version from 2.2.0 to 2.4.0 in
build.gradle. Keep the existing Spring Boot actuator dependency unchanged and
make sure the version bump is applied wherever the resilience4j artifact is
declared.

Comment on lines +275 to 294
List<CompletableFuture<Double>> futures = channelMap.values().stream()
.filter(c -> c.contentDetails() != null && c.contentDetails().relatedPlaylists() != null)
.map(c -> c.contentDetails().relatedPlaylists().uploads())
.filter(StringUtils::hasText)
.mapToDouble(playlistId -> {
List<Instant> instants = youtubeDataApiClient
.getRecentUploadDates(playlistId, RECENT_UPLOADS_COUNT)
.stream().map(Instant::parse).sorted().toList();
if (instants.size() < 2) return Double.NaN;
long spanSeconds = instants.getLast().getEpochSecond() - instants.getFirst().getEpochSecond();
return spanSeconds / 86400.0 / (instants.size() - 1);
})
.map(playlistId -> CompletableFuture
.supplyAsync(() -> computeUploadDays(playlistId), externalApiExecutor)
.exceptionally(exception -> {
log.warn("Failed to compute average upload days. playlistId={}", playlistId, exception);
return Double.NaN;
}))
.toList();

OptionalDouble avg = futures.stream()
.map(CompletableFuture::join)
.mapToDouble(Double::doubleValue)
.filter(d -> !Double.isNaN(d) && d > 0)
.average();

return avg.isPresent() ? avg.getAsDouble() : null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## file list\n'
git ls-files | rg '^src/main/java/com/example/inflace/domain/brandcollaboration/service/BrandCollaborationService\.java$|^src/main/java/.*RestClient.*|^src/main/java/.*YouTube.*|^src/main/java/.*Executor.*|^src/main/java/.*Http.*|^src/main/resources/.*application.*'

printf '\n## outline BrandCollaborationService\n'
ast-grep outline src/main/java/com/example/inflace/domain/brandcollaboration/service/BrandCollaborationService.java --view expanded || true

printf '\n## relevant snippets\n'
sed -n '1,220p' src/main/java/com/example/inflace/domain/brandcollaboration/service/BrandCollaborationService.java
printf '\n--- middle ---\n'
sed -n '220,360p' src/main/java/com/example/inflace/domain/brandcollaboration/service/BrandCollaborationService.java

printf '\n## timeout/search terms\n'
rg -n --hidden --glob '!**/target/**' --glob '!**/build/**' \
  'orTimeout|completeOnTimeout|readTimeout|connectTimeout|timeout|RestClient|WebClient|HttpClient|ExecutorService|externalApiExecutor|computeUploadDays|analyzeTrends' \
  src/main/java src/main/resources

Repository: JECT-Study/inflace-4th-server

Length of output: 25367


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## BrandCollaborationService outline\n'
ast-grep outline src/main/java/com/example/inflace/domain/brandcollaboration/service/BrandCollaborationService.java --view expanded || true

printf '\n## BrandCollaborationService relevant lines\n'
sed -n '240,320p' src/main/java/com/example/inflace/domain/brandcollaboration/service/BrandCollaborationService.java

printf '\n## search for timeout and async config\n'
rg -n --hidden --glob '!**/target/**' --glob '!**/build/**' \
  'orTimeout|completeOnTimeout|readTimeout|connectTimeout|timeout|externalApiExecutor|computeUploadDays|analyzeTrends|RestClient|WebClient|HttpClient|ExecutorService' \
  src/main/java src/main/resources

Repository: JECT-Study/inflace-4th-server

Length of output: 11566


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## RestClientConfig\n'
sed -n '1,120p' src/main/java/com/example/inflace/global/config/RestClientConfig.java

printf '\n## YoutubeDataApiClient\n'
sed -n '1,260p' src/main/java/com/example/inflace/global/client/YoutubeDataApiClient.java

printf '\n## YoutubeSearchApiClient\n'
sed -n '1,220p' src/main/java/com/example/inflace/global/client/YoutubeSearchApiClient.java

printf '\n## GoogleApiClient\n'
sed -n '1,220p' src/main/java/com/example/inflace/global/client/GoogleApiClient.java

printf '\n## any timeout-related config files\n'
rg -n --hidden --glob '!**/target/**' --glob '!**/build/**' \
  'setReadTimeout|setConnectTimeout|readTimeout|connectTimeout|timeout-duration|timeout\(' \
  src/main/java src/main/resources

Repository: JECT-Study/inflace-4th-server

Length of output: 18032


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## RestClientConfig\n'
sed -n '1,120p' src/main/java/com/example/inflace/global/config/RestClientConfig.java

printf '\n## YoutubeDataApiClient\n'
sed -n '1,260p' src/main/java/com/example/inflace/global/client/YoutubeDataApiClient.java

printf '\n## YoutubeSearchApiClient\n'
sed -n '1,220p' src/main/java/com/example/inflace/global/client/YoutubeSearchApiClient.java

printf '\n## GoogleApiClient\n'
sed -n '1,220p' src/main/java/com/example/inflace/global/client/GoogleApiClient.java

printf '\n## timeout-related settings\n'
rg -n --hidden --glob '!**/target/**' --glob '!**/build/**' \
  'setReadTimeout|setConnectTimeout|readTimeout|connectTimeout|timeout-duration|timeout\(' \
  src/main/java src/main/resources

Repository: JECT-Study/inflace-4th-server

Length of output: 18024


computeAvgUploadDays에 응답 제한을 두세요.
CompletableFuture::join()RestClient.builder().build()로 만든 YouTube 호출을 그대로 기다립니다. application.ymltimeout-duration은 rate limiter 대기 제한이라 HTTP 응답 지연을 막지 못하므로, orTimeout()RestClient read timeout을 함께 넣어야 합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/example/inflace/domain/brandcollaboration/service/BrandCollaborationService.java`
around lines 275 - 294, `computeAvgUploadDays` is waiting indefinitely on
YouTube requests, so add an actual response timeout instead of relying on the
rate-limiter setting alone. Update the async flow around
`CompletableFuture.supplyAsync(...)` and `CompletableFuture::join` to use
`orTimeout()` (or equivalent) for the upload-day computation, and configure the
`RestClient` used by `computeUploadDays` with a read timeout so slow HTTP
responses fail fast. Keep the existing exception handling/logging in
`BrandCollaborationService` to surface timeout failures clearly.

Comment on lines +90 to +91
@Bulkhead(name = "youtube-data", fallbackMethod = "youtubeDataStringListFallback")
@RateLimiter(name = "youtube-data", fallbackMethod = "youtubeDataStringListFallback")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

RateLimiter가 내부 루프의 개별 API 호출을 정확히 제어하지 못합니다.

getMyVideoIds(라인 92)는 페이지네이션 while 루프에서 API를 여러 번 호출하지만, @RateLimiter는 메서드 호출당 1 permit만 소비합니다. getYoutubeVideos(라인 141)도 청크 루프에서 동일한 패턴입니다.

예시: 500개 영상이 있는 플레이리스트에 getMyVideoIds를 호출하면 10개 API 호출이 발생하지만 rate limiter에는 1 permit만 카운트됩니다. rate limit이 20/s일 때 실제 API 호출 속도는 200/s에 도달할 수 있어 YouTube Data API 할당량(10,000 units/day)을 50초 이내에 소진할 수 있습니다.

@Bulkhead는 동기 호출이므로 동시 HTTP 요청을 16개로 제한하는 데 효과적이지만, @RateLimiter는 버스트 속도를 실제 API 호출 단위가 아닌 메서드 호출 단위로만 제한합니다.

후속 작업에서 HTTP 클라이언트 레벨 rate limiting 또는 루프 내 호출당 rate limiter 적용을 검토하세요.

Also applies to: 139-140

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/example/inflace/global/client/YoutubeDataApiClient.java`
around lines 90 - 91, The current `@RateLimiter` on YoutubeDataApiClient only
limits each method invocation once, so it does not control the individual API
requests made inside the pagination/chunk loops. Review getMyVideoIds and
getYoutubeVideos and move rate limiting to the actual YouTube API call points
inside those loops, or switch to HTTP-client-level throttling, while keeping
`@Bulkhead` on the outer methods for concurrency control. Ensure the
fallbackMethod wiring still works after refactoring and that each remote call is
counted against the limiter rather than the enclosing method.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants