Skip to content

[Feat] 영상 분석 파이프라인 견고화 및 미처리 영상 자동 분석 도입#66

Merged
toychip merged 19 commits into
mainfrom
feat/65-video-analyze-resilience
Apr 22, 2026
Merged

[Feat] 영상 분석 파이프라인 견고화 및 미처리 영상 자동 분석 도입#66
toychip merged 19 commits into
mainfrom
feat/65-video-analyze-resilience

Conversation

@toychip
Copy link
Copy Markdown
Contributor

@toychip toychip commented Apr 17, 2026

관련 이슈


변경 내용

L1 — 자막 추출 실패 분류 정밀화

  • YoutubeTranscriptClient 단일 게이트웨이로 라이브러리 호출 일원화 (자막 추출 + sentinel ping 같은 빈에서)
  • ProxyYoutubeClient 가 HTTP status 코드 분류 후 LinktripException(BAD_GATEWAY_YOUTUBE) 직접 throw
    • 429/403 → IP 차단, 5xx → 일시 오류, 기타 비2xx → 알 수 없음
  • 모호 실패는 isProxyHealthy() 의 sentinel 영상 ping 으로 분류 (sentinel 성공 → 영상 고유 문제 INVALID, sentinel 실패 → 전 프록시 차단 PENDING)
  • 미사용 fallback analyzeFromVideoanalyzeByAiVideoIngestion 리네이밍 + 사유 docstring (토큰 25배)

L2 — webshare 프록시 라운드로빈

  • YouTubeProperties.proxy.usernames: List<String> 으로 확장 (단일 username 제거)
  • ProxyYoutubeClient 가 username 별 HttpClient 캐시 보유, 순회하며 시도
  • 429/403 받으면 다음 프록시, 5xx 는 즉시 throw (큐 컨슈머가 다음 사이클에 자연 재시도)
  • 전 프록시 소진 시에만 최종 throw → consumer 가 PENDING 으로 두고 배치가 자동 재시도
  • 환경변수 YOUTUBE_PROXY_USERNAMEYOUTUBE_PROXY_USERNAMES (콤마 구분 리스트)
  • CI/CD workflow + docker-compose 에서 변수명 동일 적용

L4 — POST 응답에 결과 인라인 반환

  • POST /video/analyze 반환 타입 VideoAnalyzeAcceptResponseVideoAnalyzeResponse (GET 과 동일 shape)
  • COMPLETED 면 schedule 데이터 로드해서 응답에 포함 → 클라이언트 추가 폴링 불필요
  • HTTP status: COMPLETED/INVALID = 200, PENDING/PROCESSING = 202
  • 미사용 VideoAnalyzeAcceptResponse 삭제

L5 — USER/BATCH 우선순위 큐 + source 영속화

  • Source enum 신규 (USER priority 0, BATCH priority 10)
  • InMemoryVideoAnalysisQueueAdapterPriorityBlockingQueue + sequence tiebreaker 로 교체 → priority 보장 + 동일 source 내 FIFO 보장
  • VideoAnalyzeUseCase.analyzeVideo(url, source) 시그니처 확장 (default 제거 → 호출처 명시 강제)
  • 호출처별 source 명시: VideoController → USER, YouTubeCollectService/KeywordAnalyzeService → BATCH
  • VideoAnalysisTask 도메인 + 엔티티에 source 컬럼 추가 (audit 통계 + 재시도 priority 보존)
  • VideoAnalysisRetryJob / ApplicationInitializer 가 재 enqueue 시 task.source 그대로 전달

L6 — 미처리 영상 자동 분석 스케줄러

  • YouTubeVideoPersistencePort.findUnanalyzedVideoIds(limit) 신규 — LEFT JOIN + IS NULL anti-join 패턴
  • VideoAnalysisBackfillScheduler 신규 — 10분 cron 으로 5건씩 BATCH 우선순위 enqueue
  • @ConditionalOnProperty(batch.video-analysis-backfill.enabled=true) 로 환경별 활성화
  • application-prod.yml 에 활성 플래그 추가 (dev 자동 실행 차단)

체크리스트

  • Ktlint
  • 테스트 통과 여부

Summary by CodeRabbit

  • 새로운 기능

    • 미분석 동영상 자동 배치(backfill) 예약 작업 추가
    • 분석 요청 출처(사용자/배치)에 따른 우선순위 처리 도입
  • 개선사항

    • YouTube 프록시: 다중 사용자명 지원 및 순차 회전 방식으로 강화
    • 프록시 상태 건강 검사(센티넬) 추가
    • 동영상 분석 API 응답 구조 개선: 처리 중에는 202, 완료/유효하지 않음은 200으로 응답
    • 인메모리 큐: 우선순위 기반 처리로 변경

toychip added 15 commits April 17, 2026 13:24
@toychip toychip changed the title Feat/65 video analyze resilience [Feat] 영상 분석 파이프라인 견고화 및 미처리 영상 자동 분석 도입 Apr 17, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 17, 2026

📝 Walkthrough

Walkthrough

비디오 분석 파이프라인에 요청 출처(Source) 추적을 도입하고, 우선순위 기반 큐와 프로세스(프록시 사용자명 로테이션, 트랜스크립트 클라이언트, 배치 백필 스케줄러)를 추가하며 HTTP API 응답 계약과 관련 설정/환경 변수를 업데이트했습니다.

Changes

Cohort / File(s) Summary
설정 및 CI/CD
​.github/workflows/cicd-release.yml, docker/docker-compose.prod.yml, linktrip-bootstrap/src/main/resources/application-prod.yml, linktrip-bootstrap/src/main/resources/application.yml, linktrip-output-http/src/main/kotlin/com/linktrip/output/http/properties/YouTubeProperties.kt
프록시 사용자명 환경변수명 단수→복수(YOUTUBE_PROXY_USERNAMEYOUTUBE_PROXY_USERNAMES) 변경 및 프로퍼티에 헬스체크·배치 백필 플래그 추가
도메인: Source 및 전파
linktrip-application/src/main/kotlin/com/linktrip/application/domain/video/Source.kt, .../VideoAnalysisTask.kt, .../VideoAnalyzeEvent.kt, .../VideoAnalyzeService.kt, .../KeywordAnalyzeService.kt
Source(enum) 추가(USER=0,BATCH=10) 및 VideoAnalysisTask/Event/Service 전반에 source 필드·매개변수 전파
포트/인터페이스 변경
linktrip-application/src/main/kotlin/.../VideoAnalyzeUseCase.kt, .../VideoAnalysisQueuePort.kt, .../YouTubeVideoPersistencePort.kt
analyzeVideo에 source 매개변수 추가, QueuePort.enqueue 시그니처 확장, 미분석 영상 조회 메서드 추가
큐 구현 변경 (우선순위 큐)
linktrip-output-cache/caffeine/src/main/kotlin/.../InMemoryVideoAnalysisQueueAdapter.kt, .../YouTubeVideoCachingAdapter.kt
LinkedBlockingQueue → PriorityBlockingQueue(QueueEntry)로 변경, enqueue에 source 전달, findUnanalyzedVideoIds 위임 추가
프록시 및 트랜스크립트 클라이언트
linktrip-output-http/src/main/kotlin/.../ProxyYoutubeClient.kt, .../YoutubeTranscriptClient.kt, .../VideoAnalyzeAdapter.kt
프록시 사용자명 리스트로 로테이션 지원(생성자 시그니처 변경), 새로운 YoutubeTranscriptClient 도입(트랜스크립트 획득·프록시 헬스체크), VideoAnalyzeAdapter에서 transcriptClient 사용 및 오류 분류 재작성
백필 스케줄러 및 재시도/초기화 변경
linktrip-input-batch/src/main/kotlin/.../VideoAnalysisBackfillScheduler.kt, .../VideoAnalysisRetryJobConfig.kt, linktrip-bootstrap/src/main/kotlin/.../ApplicationInitializer.kt
배치 백필 스케줄러 추가(조건적 등록, cron), retry/initializer에서 enqueue에 source 전달
영속성: 엔티티·쿼리
linktrip-output-persistence/mysql/src/main/kotlin/.../VideoAnalysisTaskEntity.kt, .../YouTubeVideoPersistenceAdapter.kt, .../YouTubeVideoQuerydslRepository.kt
VideoAnalysisTaskEntity에 source 열 추가(열거형 저장), findUnanalyzedVideoIds QueryDSL 구현 및 어댑터 오버라이드
HTTP API 및 DTO/문서 변경
linktrip-input-http/src/main/kotlin/.../VideoController.kt, .../docs/VideoDocs.kt, .../dto/response/VideoAnalyzeAcceptResponse.kt
analyzeVideo 반환 타입 변경(accept→full VideoAnalyzeResponse), 기존 VideoAnalyzeAcceptResponse 삭제, Swagger 문서·컨트롤러 응답 제어 흐름 수정
테스트 업데이트
linktrip-application/src/test/kotlin/.../video/*, linktrip-input-batch/src/test/kotlin/...
광범위한 테스트 수정: source 인자 추가, 우선순위 큐 관련 기대치·스텁 업데이트

Sequence Diagram(s)

sequenceDiagram
    participant Scheduler as VideoAnalysisBackfillScheduler
    participant VideoPersistence as YouTubeVideoPersistencePort
    participant VideoAnalyze as VideoAnalyzeUseCase
    participant Queue as VideoAnalysisQueuePort

    Scheduler->>Scheduler: run() (cron)
    Scheduler->>VideoPersistence: findUnanalyzedVideoIds(limit=5)
    VideoPersistence-->>Scheduler: List<videoId>
    alt 발견됨
        loop 각 videoId
            Scheduler->>VideoAnalyze: analyzeVideo(youtubeUrl, source=BATCH)
            VideoAnalyze->>Queue: enqueue(taskId, youtubeUrl, BATCH)
            Queue-->>VideoAnalyze: queued
        end
    else 없음
        Scheduler->>Scheduler: 로그: 미처리 영상 없음
    end
Loading
sequenceDiagram
    participant Client as ProxyYoutubeClient
    participant Proxies as ProxyList
    participant YouTube as YouTubeServer

    Client->>Client: get(url)
    Client->>Client: executeWithRotation(request)
    loop 각 proxy 사용자명
        Client->>Proxies: 선택된 username
        Client->>YouTube: 요청(프록시 사용)
        alt 2xx
            YouTube-->>Client: 성공
            Client-->>Client: 반환
        else 429/403
            YouTube-->>Client: 차단
            Client->>Client: 기록 후 다음 proxy 시도
        else 5xx/기타 오류
            YouTube-->>Client: 서버 오류
            Client-->>Client: 즉시 예외 던짐
        end
    end
    alt 모두 소진
        Client-->>Client: 차단/네트워크 실패 요약으로 예외 던짐
    end
Loading
sequenceDiagram
    participant Listener as VideoAnalyzeEventListener
    participant QueuePort as VideoAnalysisQueuePort
    participant InMem as InMemoryVideoAnalysisQueueAdapter
    participant PQ as PriorityBlockingQueue

    Listener->>Listener: onEvent(event with source)
    Listener->>QueuePort: enqueue(id, url, source)
    QueuePort->>InMem: enqueue(id, url, source)
    InMem->>InMem: QueueEntry(event, seq++)
    InMem->>PQ: offer(QueueEntry)
    PQ-->>InMem: 저장(우선순위 기반)
    InMem-->>QueuePort: 성공
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.59% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목이 영상 분석 파이프라인 견고화와 미처리 영상 자동 분석이라는 주요 변화를 명확하게 요약하고 있으며, 변경사항의 핵심 의도를 정확하게 반영하고 있습니다.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 feat/65-video-analyze-resilience

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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 (4)
linktrip-bootstrap/src/main/resources/application.yml (1)

47-48: sentinel-video-id를 환경변수로 오버라이드 가능하게 노출 권장

TVM6Nswlfbg가 하드코딩되어 있어 해당 영상이 비공개/삭제되거나 지역 차단될 경우 프록시 헬스체크(isProxyHealthy())가 영구적으로 실패 판정을 내려 모든 모호 실패가 PENDING으로 오분류될 수 있습니다. 환경변수 기본값 패턴으로 두고 운영 중 교체 가능하게 하는 것을 권장합니다.

♻️ 제안 변경
   health-check:
-    sentinel-video-id: TVM6Nswlfbg
+    sentinel-video-id: ${YOUTUBE_HEALTH_SENTINEL_VIDEO_ID:TVM6Nswlfbg}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@linktrip-bootstrap/src/main/resources/application.yml` around lines 47 - 48,
The hardcoded sentinel video id in the configuration
(health-check.sentinel-video-id) should be made configurable via an environment
variable with a default to avoid permanent failures if that video is removed or
region-blocked; change the YAML to read the value from an env var (e.g.
${SENTINEL_VIDEO_ID:TVM6Nswlfbg}) and ensure any code referencing the key (e.g.
isProxyHealthy()) uses the injected value so operators can override
SENTINEL_VIDEO_ID in runtime environments.
linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeVideoPersistenceAdapter.kt (1)

47-47: 운영 팁: anti-join 쿼리에 인덱스 확인 권장

video_analysis_task가 누적되면 youtube_video와의 anti-join 성능이 저하될 수 있습니다. video_analysis_task.youtube_video_id(또는 매칭 기준 컬럼)와 youtube_video 측 ORDER BY 정렬 컬럼(생성일 기준일 경우 created_at)에 인덱스가 존재하는지, 그리고 실제 EXPLAIN 상 인덱스 기반 NOT EXISTS/LEFT JOIN 형태로 풀리는지 확인을 권장합니다. 10분 주기 × 5건 소비 속도 대비 미분석 영상 증가 속도가 빠를 경우, limit 조정이나 주기 단축도 함께 고려해볼 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeVideoPersistenceAdapter.kt`
at line 47, findUnanalyzedVideoIds 구현에서 video_analysis_task와 youtube_video를
anti-join으로 조회할 때 인덱스/쿼리 플랜을 확인하도록 수정하세요:
querydslRepository.findUnanalyzedVideoIds 호출에 사용되는 조인
키(video_analysis_task.youtube_video_id 또는 실제 매칭 컬럼)와 youtube_video의 정렬 컬럼(예:
created_at)에 적절한 인덱스가 존재하는지 DB에 생성하고 EXPLAIN으로 NOT EXISTS/LEFT JOIN이 인덱스를 사용하는지
검증하며, 필요하면 인덱스 추가 또는 limit/스케줄링(주기 단축) 조정 로직을 논의하도록 코멘트를 남기세요.
linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/repository/YouTubeVideoQuerydslRepository.kt (1)

95-105: 대규모 데이터셋에서 정렬/조인 비용에 대한 인덱스 검토 권장

쿼리는 youtube_videocreated_at ASC로 정렬한 뒤 video_analysis_task.youtube_url에 대해 anti-join 하는 구조입니다. 데이터가 쌓이면 다음 두 인덱스의 존재 여부를 확인해 주세요.

  • youtube_video(created_at) — ORDER BY + LIMIT 가 인덱스 스캔으로 풀리게.
  • video_analysis_task(youtube_url) — LEFT JOIN 조건에서 seek 가 가능하게(이미 uk_video_analysis_task_youtube_url 유니크 제약이 있으므로 충족).

둘 다 있다면 LIMIT 5 기준으로 매우 저렴하게 동작하고, 없다면 정렬 cost가 누적됩니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/repository/YouTubeVideoQuerydslRepository.kt`
around lines 95 - 105, The query in findUnanalyzedVideoIds performs ORDER BY
video.createdAt ASC with a LEFT JOIN on analysisTask.youtubeUrl, which will
become expensive at scale; ensure the DB has an index on
youtube_video(created_at) to allow index-ordered LIMIT scans and an index on
video_analysis_task(youtube_url) (the unique constraint
uk_video_analysis_task_youtube_url already satisfies this) so the LEFT
JOIN/anti-join can seek efficiently; add or confirm those indexes at the
database level and document/verify with EXPLAIN that the plan uses the indexes
for LIMIT performance.
linktrip-output-http/src/main/kotlin/com/linktrip/output/http/adapter/VideoAnalyzeAdapter.kt (1)

96-109: LinktripException 생성자가 원인 예외 체이닝을 지원하지 않음.

logger.warn(cause) 로 스택 추적 정보는 남지만, LinktripException 의 현재 생성자는 cause 파라미터를 받지 않습니다. ExceptionHandler 나 상위 로거에서 root cause (TranscriptRetrievalException) 를 추적하려면 생성자를 확장해야 합니다.

현재 코드의 logging-then-throw 패턴은 스택을 보존하는 workaround 이지만, 구조적으로 정확한 예외 체이닝을 원한다면 LinktripException 이 원인 예외를 받는 생성자를 제공해야 합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@linktrip-output-http/src/main/kotlin/com/linktrip/output/http/adapter/VideoAnalyzeAdapter.kt`
around lines 96 - 109, Add a constructor to LinktripException that accepts a
cause (Throwable) and wires it into the super/exception chain, then update
classifyAmbiguousFailure to pass the original TranscriptRetrievalException as
the cause when throwing LinktripException (i.e., replace the current throw
LinktripException(ExceptionCode.BAD_GATEWAY_YOUTUBE, "…") with the new
constructor that includes the cause). Ensure the new LinktripException
constructor preserves message, code (ExceptionCode.BAD_GATEWAY_YOUTUBE) and sets
the cause so exception handlers can access the root
TranscriptRetrievalException.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@linktrip-application/src/main/kotlin/com/linktrip/application/domain/video/VideoAnalyzeService.kt`:
- Around line 20-27: When reactivating a FAILED task we must keep DB source and
queued source consistent: modify the reactivation branch inside the
findByYoutubeUrl handling so that after detecting existing.status ==
VideoAnalysisTaskStatus.FAILED you update the persisted task's source to the
incoming source as well as its status (use
videoAnalysisTaskPersistencePort.updateSource or the existing persistence method
to set source and updateStatus together), then publish
Events.raise(VideoAnalyzeEvent(...)) using the same updated source (not the old
existing.source), and return existing.copy(status =
VideoAnalysisTaskStatus.PENDING, source = source); ensure you touch
VideoAnalyzeEvent, videoAnalysisTaskPersistencePort.updateStatus/updateSource,
and the existing variable to keep DB and queue sources identical.

In
`@linktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/InMemoryVideoAnalysisQueueAdapter.kt`:
- Line 58: The contains method in InMemoryVideoAnalysisQueueAdapter is violating
ktlint (line >120 and needs a block body newline); change the single-line
expression override fun contains(videoAnalysisTaskId: String): Boolean =
queue.any { it.event.videoAnalysisTaskId == videoAnalysisTaskId } into a
properly formatted block body with a newline and a short line length (for
example use override fun contains(videoAnalysisTaskId: String): Boolean { return
queue.any { it.event.videoAnalysisTaskId == videoAnalysisTaskId } }) so the
expression body is on its own line and the line length stays under 120
characters.

In
`@linktrip-output-http/src/main/kotlin/com/linktrip/output/http/adapter/ProxyYoutubeClient.kt`:
- Around line 61-96: In executeWithRotation, HttpClient.send(...) can throw
IOException or InterruptedException which currently aborts the rotation; wrap
the send call in try/catch so that IOException is caught, logged (include
proxy.username and exception), and the loop continues to try the next proxy (do
not throw), while InterruptedException should restore the thread interrupt flag
(Thread.currentThread().interrupt()) and rethrow as a LinktripException to
preserve cancel semantics; also ensure requests set an individual timeout (use
HttpRequest.newBuilder(...).timeout(Duration.ofSeconds(15)) when building the
request passed into executeWithRotation) and that each proxy's HttpClient is
created with a connectTimeout
(HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)) in the code that
constructs proxy.httpClient) so slow or dead proxies don't hang forever.

---

Nitpick comments:
In `@linktrip-bootstrap/src/main/resources/application.yml`:
- Around line 47-48: The hardcoded sentinel video id in the configuration
(health-check.sentinel-video-id) should be made configurable via an environment
variable with a default to avoid permanent failures if that video is removed or
region-blocked; change the YAML to read the value from an env var (e.g.
${SENTINEL_VIDEO_ID:TVM6Nswlfbg}) and ensure any code referencing the key (e.g.
isProxyHealthy()) uses the injected value so operators can override
SENTINEL_VIDEO_ID in runtime environments.

In
`@linktrip-output-http/src/main/kotlin/com/linktrip/output/http/adapter/VideoAnalyzeAdapter.kt`:
- Around line 96-109: Add a constructor to LinktripException that accepts a
cause (Throwable) and wires it into the super/exception chain, then update
classifyAmbiguousFailure to pass the original TranscriptRetrievalException as
the cause when throwing LinktripException (i.e., replace the current throw
LinktripException(ExceptionCode.BAD_GATEWAY_YOUTUBE, "…") with the new
constructor that includes the cause). Ensure the new LinktripException
constructor preserves message, code (ExceptionCode.BAD_GATEWAY_YOUTUBE) and sets
the cause so exception handlers can access the root
TranscriptRetrievalException.

In
`@linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeVideoPersistenceAdapter.kt`:
- Line 47: findUnanalyzedVideoIds 구현에서 video_analysis_task와 youtube_video를
anti-join으로 조회할 때 인덱스/쿼리 플랜을 확인하도록 수정하세요:
querydslRepository.findUnanalyzedVideoIds 호출에 사용되는 조인
키(video_analysis_task.youtube_video_id 또는 실제 매칭 컬럼)와 youtube_video의 정렬 컬럼(예:
created_at)에 적절한 인덱스가 존재하는지 DB에 생성하고 EXPLAIN으로 NOT EXISTS/LEFT JOIN이 인덱스를 사용하는지
검증하며, 필요하면 인덱스 추가 또는 limit/스케줄링(주기 단축) 조정 로직을 논의하도록 코멘트를 남기세요.

In
`@linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/repository/YouTubeVideoQuerydslRepository.kt`:
- Around line 95-105: The query in findUnanalyzedVideoIds performs ORDER BY
video.createdAt ASC with a LEFT JOIN on analysisTask.youtubeUrl, which will
become expensive at scale; ensure the DB has an index on
youtube_video(created_at) to allow index-ordered LIMIT scans and an index on
video_analysis_task(youtube_url) (the unique constraint
uk_video_analysis_task_youtube_url already satisfies this) so the LEFT
JOIN/anti-join can seek efficiently; add or confirm those indexes at the
database level and document/verify with EXPLAIN that the plan uses the indexes
for LIMIT performance.
🪄 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: d17132b5-b70e-4c56-8375-5c107a90fa0f

📥 Commits

Reviewing files that changed from the base of the PR and between 0b1210e and af1dc72.

📒 Files selected for processing (34)
  • .github/workflows/cicd-release.yml
  • docker/docker-compose.prod.yml
  • linktrip-application/src/main/kotlin/com/linktrip/application/domain/video/KeywordAnalyzeService.kt
  • linktrip-application/src/main/kotlin/com/linktrip/application/domain/video/Source.kt
  • linktrip-application/src/main/kotlin/com/linktrip/application/domain/video/VideoAnalysisTask.kt
  • linktrip-application/src/main/kotlin/com/linktrip/application/domain/video/VideoAnalyzeEvent.kt
  • linktrip-application/src/main/kotlin/com/linktrip/application/domain/video/VideoAnalyzeEventListener.kt
  • linktrip-application/src/main/kotlin/com/linktrip/application/domain/video/VideoAnalyzeService.kt
  • linktrip-application/src/main/kotlin/com/linktrip/application/domain/youtube/YouTubeCollectService.kt
  • linktrip-application/src/main/kotlin/com/linktrip/application/port/input/VideoAnalyzeUseCase.kt
  • linktrip-application/src/main/kotlin/com/linktrip/application/port/output/persistence/YouTubeVideoPersistencePort.kt
  • linktrip-application/src/main/kotlin/com/linktrip/application/port/output/queue/VideoAnalysisQueuePort.kt
  • linktrip-application/src/test/kotlin/com/linktrip/application/domain/video/VideoAnalysisQueueConsumerTest.kt
  • linktrip-application/src/test/kotlin/com/linktrip/application/domain/video/VideoAnalysisTaskTest.kt
  • linktrip-application/src/test/kotlin/com/linktrip/application/domain/video/VideoAnalyzeEventListenerTest.kt
  • linktrip-application/src/test/kotlin/com/linktrip/application/domain/video/VideoAnalyzeServiceTest.kt
  • linktrip-application/src/test/kotlin/com/linktrip/application/domain/video/VideoScheduleServiceTest.kt
  • linktrip-application/src/test/kotlin/com/linktrip/application/domain/youtube/YouTubeCollectServiceTest.kt
  • linktrip-bootstrap/src/main/kotlin/com/linktrip/bootstrap/ApplicationInitializer.kt
  • linktrip-bootstrap/src/main/resources/application-prod.yml
  • linktrip-bootstrap/src/main/resources/application.yml
  • linktrip-input-batch/src/main/kotlin/com/linktrip/input/batch/VideoAnalysisBackfillScheduler.kt
  • linktrip-input-batch/src/main/kotlin/com/linktrip/input/batch/VideoAnalysisRetryJobConfig.kt
  • linktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/VideoController.kt
  • linktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/docs/VideoDocs.kt
  • linktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/dto/response/VideoAnalyzeAcceptResponse.kt
  • linktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/InMemoryVideoAnalysisQueueAdapter.kt
  • linktrip-output-http/src/main/kotlin/com/linktrip/output/http/adapter/ProxyYoutubeClient.kt
  • linktrip-output-http/src/main/kotlin/com/linktrip/output/http/adapter/VideoAnalyzeAdapter.kt
  • linktrip-output-http/src/main/kotlin/com/linktrip/output/http/adapter/YoutubeTranscriptClient.kt
  • linktrip-output-http/src/main/kotlin/com/linktrip/output/http/properties/YouTubeProperties.kt
  • linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeVideoPersistenceAdapter.kt
  • linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/entity/VideoAnalysisTaskEntity.kt
  • linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/repository/YouTubeVideoQuerydslRepository.kt
💤 Files with no reviewable changes (1)
  • linktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/dto/response/VideoAnalyzeAcceptResponse.kt
📜 Review details
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2026-03-18T01:08:05.661Z
Learnt from: toychip
Repo: Link-Trip/BackEnd PR: 24
File: linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeVideoPersistenceAdapter.kt:16-28
Timestamp: 2026-03-18T01:08:05.661Z
Learning: In `linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeVideoPersistenceAdapter.kt`, the concurrent-write safety (duplicate videoId in batch, race condition on uk_youtube_video_video_id) is intentionally deferred. The maintainer (toychip) confirmed the system is currently single-server, so this is not a concern yet. When replication is introduced in the future, ShedLock will be used for distributed locking to address this. At that point, in-batch videoId deduplication should also be applied.

Applied to files:

  • linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeVideoPersistenceAdapter.kt
  • linktrip-application/src/main/kotlin/com/linktrip/application/port/output/persistence/YouTubeVideoPersistencePort.kt
  • linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/repository/YouTubeVideoQuerydslRepository.kt
  • linktrip-input-batch/src/main/kotlin/com/linktrip/input/batch/VideoAnalysisBackfillScheduler.kt
  • linktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/InMemoryVideoAnalysisQueueAdapter.kt
📚 Learning: 2026-03-18T01:07:53.575Z
Learnt from: toychip
Repo: Link-Trip/BackEnd PR: 24
File: linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeVideoPersistenceAdapter.kt:16-28
Timestamp: 2026-03-18T01:07:53.575Z
Learning: Apply the same deferred concurrent-write policy to all Kotlin persistence adapters in the MySQL adapter package: ensure writes are coordinated on a single server with ShedLock considered for the replication phase. For YouTubeVideoPersistenceAdapter.kt (and other adapters in this directory), verify that concurrent writes are serialized or properly guarded, document the policy in code comments, and ensure CI checks or deployment gating will catch any regression where multiple instances might attempt a conflicting write during replication.

Applied to files:

  • linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeVideoPersistenceAdapter.kt
📚 Learning: 2026-03-18T01:08:05.661Z
Learnt from: toychip
Repo: Link-Trip/BackEnd PR: 24
File: linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeVideoPersistenceAdapter.kt:16-28
Timestamp: 2026-03-18T01:08:05.661Z
Learning: Similarly, `linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeChannelPersistenceAdapter.kt` likely has the same deferred concurrent-write policy (single server, ShedLock planned for replication phase).

Applied to files:

  • linktrip-application/src/main/kotlin/com/linktrip/application/port/output/persistence/YouTubeVideoPersistencePort.kt
  • linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/entity/VideoAnalysisTaskEntity.kt
  • linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/repository/YouTubeVideoQuerydslRepository.kt
  • linktrip-output-http/src/main/kotlin/com/linktrip/output/http/adapter/ProxyYoutubeClient.kt
  • linktrip-output-http/src/main/kotlin/com/linktrip/output/http/adapter/VideoAnalyzeAdapter.kt
  • linktrip-output-http/src/main/kotlin/com/linktrip/output/http/adapter/YoutubeTranscriptClient.kt
  • linktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/InMemoryVideoAnalysisQueueAdapter.kt
🪛 GitHub Actions: CI - Pull request
linktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/InMemoryVideoAnalysisQueueAdapter.kt

[error] 58-58: ktlint failed: Exceeded max line length (120) (cannot be auto-corrected).


[error] 58-58: ktlint failed: Newline expected before expression body.


[error] 58-58: ktlint failed: Missing newline after '{'.


[error] 1-1: Gradle task failed: ':linktrip-output-cache:caffeine:ktlintMainSourceSetCheck' (KtLint found code style violations).

🔇 Additional comments (27)
linktrip-application/src/test/kotlin/com/linktrip/application/domain/video/VideoScheduleServiceTest.kt (1)

40-40: LGTM!

Source.USER를 fixture에 명시적으로 추가하여 VideoAnalysisTask의 새 필수 속성과 정합됩니다.

linktrip-application/src/main/kotlin/com/linktrip/application/domain/video/VideoAnalyzeEvent.kt (1)

6-6: LGTM!

이벤트 페이로드에 source를 추가하여 리스너/큐까지 원천 정보를 전달하는 전반적 변경과 일치합니다.

linktrip-application/src/main/kotlin/com/linktrip/application/domain/video/KeywordAnalyzeService.kt (1)

67-70: LGTM!

배치 플로우에서 Source.BATCH로 명시 호출하여 우선순위 큐 정책과 정합됩니다.

linktrip-application/src/main/kotlin/com/linktrip/application/domain/video/VideoAnalyzeEventListener.kt (1)

19-22: LGTM!

event.source를 로그와 enqueue 인자 양쪽에 반영한 점이 적절합니다. 운영 시 소스별 큐 투입 추적이 용이해집니다.

linktrip-input-batch/src/main/kotlin/com/linktrip/input/batch/VideoAnalysisRetryJobConfig.kt (1)

59-59: LGTM!

재시도 재투입 시 원래 task.source를 보존하여 USER 요청이 BATCH로 강등되지 않습니다. ApplicationInitializer의 재적재 경로와도 일관됩니다.

docker/docker-compose.prod.yml (1)

22-22: LGTM — 배포 전 시크릿 교체 확인 필요

env 변수명 변경 자체는 CI/CD, application-prod.yml과 정합합니다. 배포 전 호스트 .env 및 GitHub Secrets에서 YOUTUBE_PROXY_USERNAMEYOUTUBE_PROXY_USERNAMES(콤마 구분) 교체가 반드시 선행되어야 누락 시 프록시 인증 실패로 수집 파이프라인 전체가 중단됩니다(이미 PR 체크리스트에 명시된 항목).

linktrip-bootstrap/src/main/kotlin/com/linktrip/bootstrap/ApplicationInitializer.kt (1)

53-53: LGTM!

부팅 시 PENDING 재적재에서 task.source를 그대로 전달하여 재기동 후에도 우선순위가 보존됩니다. DDL 마이그레이션으로 기존 레코드의 source 컬럼이 non-null로 백필되어야 한다는 점(PR 운영 체크리스트)만 배포 전 확인 필요합니다.

linktrip-application/src/main/kotlin/com/linktrip/application/port/input/VideoAnalyzeUseCase.kt (1)

6-11: 시그니처 확장 LGTM

analyzeVideosource: Source 필수 파라미터가 추가되었고, PR 전반의 호출부(USER/BATCH)가 일관되게 업데이트되어 있어 계약 변경이 깔끔합니다.

linktrip-application/src/test/kotlin/com/linktrip/application/domain/video/VideoAnalysisQueueConsumerTest.kt (1)

82-82: VideoAnalyzeEvent에 Source 인자 추가 반영 확인

모든 processAnalysis 호출이 Source.USER로 통일되어 신규 시그니처와 일치합니다. 다만 processAnalysis 자체가 source에 의존하지 않는다면 현재 구성으로 충분하고, 큐 우선순위 동작 검증은 InMemoryVideoAnalysisQueueAdapter 테스트 쪽에서 다루고 있을 것으로 보입니다.

linktrip-application/src/main/kotlin/com/linktrip/application/domain/youtube/YouTubeCollectService.kt (1)

99-102: BATCH source 전달 LGTM

수집 파이프라인에서 생성된 분석 요청이 Source.BATCH로 태깅되어 USER 요청보다 뒤에 처리되도록 한 의도가 명확히 드러납니다. 명명 인자 사용도 가독성에 좋습니다.

linktrip-application/src/main/kotlin/com/linktrip/application/port/output/queue/VideoAnalysisQueuePort.kt (1)

7-11: enqueue 시그니처 확장 LGTM

source 파라미터 추가로 우선순위 큐 어댑터가 VideoAnalyzeEvent(id, url, source)를 구성할 수 있게 되었고, 호출부(리스너/초기화/백필 스케줄러)도 일관되게 업데이트되어 있습니다.

linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeVideoPersistenceAdapter.kt (1)

46-48: findUnanalyzedVideoIds 위임 구현 LGTM

@Transactional(readOnly = true)도 올바르게 적용되어 있고, 실제 anti-join 로직은 Querydsl 레포지토리에 위임하는 구조라 어댑터 책임이 잘 분리되어 있습니다. 백필 스케줄러에서 10분마다 소량(5건) 소비하는 사용 패턴이라 인덱스만 받쳐주면 운영상 이슈는 없을 것으로 보입니다.

linktrip-bootstrap/src/main/resources/application-prod.yml (2)

21-25: 백필 스케줄러 prod 활성화 LGTM

주석이 동작(10분/5건, dev 비활성 사유) 의도를 명확히 설명하고 있어 운영 관점에서 이해하기 좋습니다. batch.video-analysis-backfill.enabled 속성명도 스케줄러의 @ConditionalOnProperty와 일치하는 것으로 보입니다.


19-19: 환경변수 리네이밍이 완전히 적용됨 - 추가 조치 불필요

검증 결과, YOUTUBE_PROXY_USERNAMEYOUTUBE_PROXY_USERNAMES로의 변경이 완전히 적용되었습니다:

  • application-prod.yml, docker-compose.prod.yml, cicd-release.yml 모두 새로운 복수형 이름으로 일관되게 업데이트됨
  • 기존 단일형 변수명은 코드베이스 어디에도 남아 있지 않음
  • GitHub Actions 배포 워크플로우에서 secrets.YOUTUBE_PROXY_USERNAMES가 올바르게 참조되고 EC2에 전달됨

배포 전 추가 검증이 필요하지 않습니다.

linktrip-application/src/main/kotlin/com/linktrip/application/domain/video/Source.kt (1)

1-14: Source enum 도입 LGTM

우선순위 간격(0 ↔ 10)을 벌려둬서 추후 중간 티어(예: RETRY, ADMIN 등) 추가 여지를 확보한 점이 좋습니다. PriorityBlockingQueue에서 동일 priority 처리 시의 FIFO 보장은 별도 시퀀스 타이브레이커로 해결된 것으로 PR 설명에 언급되어 있어 엔트리 비교 로직에 반영되어 있는지만 확인하시면 됩니다.

linktrip-application/src/main/kotlin/com/linktrip/application/port/output/persistence/YouTubeVideoPersistencePort.kt (1)

23-28: 포트 메서드 추가 LGTM

KDoc에 "오래된 것부터 [limit] 건", "stranded 영상 소진용"으로 계약(정렬·용도)이 명시되어 있어 구현체/호출부 모두 기대치가 분명합니다. MySQL 어댑터 구현과도 시그니처가 일치합니다.

linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/entity/VideoAnalysisTaskEntity.kt (1)

36-38: 기존 row 백필 마이그레이션 필수

source 컬럼이 nullable=false로 추가되었습니다. 배포 전 기존 video_analysis_task 레코드에 대해 기본값(예: USER 또는 BATCH)을 채워 넣는 DDL/DML 마이그레이션이 선행되지 않으면 애플리케이션 기동 시 기존 row 조회에서 toDomain()이 NPE를 유발하거나, DDL auto 적용 환경에서는 NOT NULL 제약 추가 자체가 실패할 수 있습니다. PR objectives에 운영 절차로 명시되어 있으니 배포 순서를 재확인해 주세요.

linktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/docs/VideoDocs.kt (1)

65-92: 400 응답의 "자막 없는 영상" 예시는 정확하며 유지해야 함

검토 결과, VideoAnalyzeAdapter.kt:65에서 자막 추출 실패(transcript == null)시 여전히 BAD_REQUEST_VIDEO 예외를 던지고 있으므로, 현재 문서화된 400 응답 예시가 정확합니다. 자막 없는 영상에 대한 예시는 제거할 필요가 없습니다.

linktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/VideoController.kt (1)

54-68: LGTM!

POST /video/analyze 의 응답 계약 전환(상태별 200/202 매핑, COMPLETED 인 경우 schedule 인라인 반환, 그 외엔 빈 리스트로 from(...))이 Swagger/GET /video/schedule 핸들러와 대칭적으로 잘 정리되었습니다. Source.USER 전달도 VideoAnalyzeUseCase 의 새 시그니처와 일치합니다.

linktrip-input-batch/src/main/kotlin/com/linktrip/input/batch/VideoAnalysisBackfillScheduler.kt (1)

29-51: LGTM!

10분 주기 cron, BATCH_SIZE=5 의 보수적인 enqueue, 영상별 try/catch 로 한 건 실패가 전체 루프를 죽이지 않도록 한 구성 모두 적절합니다. ConditionalOnProperty 로 dev 비활성화 분리도 깔끔합니다. 단일 서버 가정 하 분산 락 미적용은 기존 정책과 일치합니다.

linktrip-output-http/src/main/kotlin/com/linktrip/output/http/properties/YouTubeProperties.kt (1)

9-20: LGTM!

usernames: List<String> 로의 전환과 isEnabled() 의 AND 조건, 그리고 HealthCheckProperties.sentinelVideoId 기본값 빈 문자열 처리(YoutubeTranscriptClient.isProxyHealthy() 에서 false 처리) 모두 일관됩니다.

linktrip-application/src/test/kotlin/com/linktrip/application/domain/video/VideoAnalyzeServiceTest.kt (1)

79-103: LGTM!

신규 USER/BATCH 분기와, 특히 BATCH 로 만들어진 FAILED → USER 가 재요청 시 task.source(원본 audit) 과 재시도 이벤트의 source(현재 trigger) 가 분리되는 의도까지 검증한 테스트 구성이 좋습니다.

Also applies to: 171-197

linktrip-application/src/main/kotlin/com/linktrip/application/domain/video/VideoAnalysisTask.kt (1)

13-24: LGTM!

source 필수 필드와 create(youtubeUrl, source) 시그니처 변경, 그리고 백필 쿼리에서 join key 로 활용되는 YOUTUBE_VIDEO_BASE_URL 의 가시성 승격 모두 사용처와 일관됩니다.

Also applies to: 50-62

linktrip-output-http/src/main/kotlin/com/linktrip/output/http/adapter/YoutubeTranscriptClient.kt (2)

45-59: LGTM!

ko 수동 → en 수동 → ko 자동 → en 자동 순 폴백을 runCatching 체인으로 깔끔하게 표현했습니다. listTranscripts(videoId) 의 네트워크 실패는 chain 진입 전에 그대로 전파되고, fetch() 단계 실패는 let 블록 안에서 자연 전파되므로 프록시/HTTP 실패가 무음 처리되지 않는 점도 확인했습니다.


68-85: listTranscripts() 캐싱 동작 확인 결과

thoroldvix/youtube-transcript-api 라이브러리는 빌트인 캐싱 메커니즘이 없으며, 동일 영상에 대해서도 listTranscripts() 호출 시마다 YouTube에 대해 신규 HTTP 요청을 발생시킵니다. Java 11 HttpClient 또는 사용자 정의 클라이언트를 통해 매번 실제 네트워크/프록시를 거치므로, 현재 구현은 의도된 대로 동작합니다.

linktrip-output-http/src/main/kotlin/com/linktrip/output/http/adapter/VideoAnalyzeAdapter.kt (1)

152-182: 비용 증가 의도는 잘 명시했으나, 빌드 실패 우려는 타당하지 않습니다.

analyzeByAiVideoIngestion 의 미사용 상태를 자세히 설명한 docstring (토큰 소비 25배, 자막 추출 불가 시 와이어업 예정)은 좋습니다. 다만 빌드 실패 가능성에 대한 우려는 프로젝트 설정에 맞지 않습니다:

  • detekt 의 style 규칙이 비활성화(style: active: false)되어 있어 UnusedPrivateMember 검사가 작동하지 않음
  • ktlint는 코드 포맷터이며 사용되지 않은 코드를 감지하지 않음
  • Kotlin 컴파일러도 private 멤버의 미사용에 대해 기본적으로 경고하지 않음

@Suppress("unused") 는 필요 없습니다.

			> Likely an incorrect or invalid review comment.
linktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/InMemoryVideoAnalysisQueueAdapter.kt (1)

48-54: LGTM!

queue.take() 블로킹 후 .event로 언래핑하여 포트 계약(VideoAnalyzeEvent?)을 정확히 충족합니다. InterruptedException 발생 시 인터럽트 플래그를 복원하고 null 반환하는 패턴도 적절합니다.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
linktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/InMemoryVideoAnalysisQueueAdapter.kt (1)

48-54: ⚠️ Potential issue | 🔴 Critical

dequeue() 계약과 구현의 불일치 명확히 할 필요.

VideoAnalysisQueuePort.dequeue(): VideoAnalyzeEvent? 시그니처와 소비자의 dequeue() ?: continue 패턴은 큐가 비었을 때 null을 반환하는 비블로킹 동작을 암시합니다. 하지만 실제 구현은 queue.take()로 무한 대기하며, null은 오직 InterruptedException 발생 시에만 반환됩니다.

현재 소비자가 전용 데몬 스레드에서 실행되므로 블로킹 동작이 효율적이긴 하지만, 인터페이스 계약이 실제 동작을 정확히 반영하지 않습니다. 다음 중 하나를 수행하세요:

  1. 블로킹 의도를 명시: 인터페이스 문서 또는 JavaDoc에 "이 메서드는 요소가 사용 가능해질 때까지 블로킹한다"고 명시하고, 소비자 코드의 ?: continue는 인터럽트 발생 시에만 작동함을 문서화
  2. 타임아웃 기반 폴링으로 변경: 현재 설계가 논블로킹 폴링을 원한다면 poll(timeout, unit) 사용 추천
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@linktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/InMemoryVideoAnalysisQueueAdapter.kt`
around lines 48 - 54, InMemoryVideoAnalysisQueueAdapter.dequeue currently blocks
using queue.take(), but the VideoAnalysisQueuePort.dequeue signature and
consumers using "dequeue() ?: continue" imply non‑blocking null-return behavior;
either (A) make the blocking intent explicit by updating
VideoAnalysisQueuePort.dequeue Javadoc to state "blocks until an element is
available (returns null only on interrupt)" and document the consumer's reliance
on interrupt→null, or (B) change the adapter to non‑blocking polling by
replacing queue.take() with queue.poll(timeout, unit) and return null on timeout
while preserving the existing InterruptedException handling (re-interrupt thread
and return null); update any related consumer expectations accordingly (refer to
InMemoryVideoAnalysisQueueAdapter.dequeue, VideoAnalysisQueuePort.dequeue, and
the consumer pattern "dequeue() ?: continue").
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@linktrip-output-http/src/main/kotlin/com/linktrip/output/http/adapter/ProxyYoutubeClient.kt`:
- Around line 81-100: Mask the proxy username before logging anywhere in
ProxyYoutubeClient (e.g., in the logger.warn, logger.debug, logger.info calls
that currently interpolate proxy.username); implement a small helper function
(e.g., maskUsername or maskProxyUser) that redacts most characters and leaves
only a short prefix/suffix or a fixed token, use that helper wherever
proxy.username is logged and avoid putting the raw value into lastNetworkFailure
or any log message—replace direct uses of proxy.username in the logging
statements shown with the masked value.
- Line 75: The loop over proxyClients in ProxyYoutubeClient always starts at
index 0, so fix it by rotating the start index per request: add a per-instance
AtomicInteger (e.g., nextProxyIndex) and on each request getAndIncrement() to
compute a start = abs(nextProxyIndex.getAndIncrement()) % proxyClients.size,
then iterate proxies using an index formula like idx = (start + i) %
proxyClients.size to try each proxy in round-robin order; update the method that
contains the for (proxy in proxyClients) loop to use this wrapped-index
iteration and ensure thread-safety by using the AtomicInteger.

---

Outside diff comments:
In
`@linktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/InMemoryVideoAnalysisQueueAdapter.kt`:
- Around line 48-54: InMemoryVideoAnalysisQueueAdapter.dequeue currently blocks
using queue.take(), but the VideoAnalysisQueuePort.dequeue signature and
consumers using "dequeue() ?: continue" imply non‑blocking null-return behavior;
either (A) make the blocking intent explicit by updating
VideoAnalysisQueuePort.dequeue Javadoc to state "blocks until an element is
available (returns null only on interrupt)" and document the consumer's reliance
on interrupt→null, or (B) change the adapter to non‑blocking polling by
replacing queue.take() with queue.poll(timeout, unit) and return null on timeout
while preserving the existing InterruptedException handling (re-interrupt thread
and return null); update any related consumer expectations accordingly (refer to
InMemoryVideoAnalysisQueueAdapter.dequeue, VideoAnalysisQueuePort.dequeue, and
the consumer pattern "dequeue() ?: continue").
🪄 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: 5afe8f4f-2864-45e6-ad3e-c2e36d9304c4

📥 Commits

Reviewing files that changed from the base of the PR and between af1dc72 and f738855.

📒 Files selected for processing (3)
  • linktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/InMemoryVideoAnalysisQueueAdapter.kt
  • linktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/YouTubeVideoCachingAdapter.kt
  • linktrip-output-http/src/main/kotlin/com/linktrip/output/http/adapter/ProxyYoutubeClient.kt
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: 자동 검증 (ktlint + test)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2026-03-18T01:08:05.661Z
Learnt from: toychip
Repo: Link-Trip/BackEnd PR: 24
File: linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeVideoPersistenceAdapter.kt:16-28
Timestamp: 2026-03-18T01:08:05.661Z
Learning: In `linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeVideoPersistenceAdapter.kt`, the concurrent-write safety (duplicate videoId in batch, race condition on uk_youtube_video_video_id) is intentionally deferred. The maintainer (toychip) confirmed the system is currently single-server, so this is not a concern yet. When replication is introduced in the future, ShedLock will be used for distributed locking to address this. At that point, in-batch videoId deduplication should also be applied.

Applied to files:

  • linktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/YouTubeVideoCachingAdapter.kt
  • linktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/InMemoryVideoAnalysisQueueAdapter.kt
📚 Learning: 2026-03-18T01:08:05.661Z
Learnt from: toychip
Repo: Link-Trip/BackEnd PR: 24
File: linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeVideoPersistenceAdapter.kt:16-28
Timestamp: 2026-03-18T01:08:05.661Z
Learning: Similarly, `linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeChannelPersistenceAdapter.kt` likely has the same deferred concurrent-write policy (single server, ShedLock planned for replication phase).

Applied to files:

  • linktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/YouTubeVideoCachingAdapter.kt
  • linktrip-output-http/src/main/kotlin/com/linktrip/output/http/adapter/ProxyYoutubeClient.kt
  • linktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/InMemoryVideoAnalysisQueueAdapter.kt
🔇 Additional comments (4)
linktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/YouTubeVideoCachingAdapter.kt (1)

42-43: LGTM — 백필 조회를 캐시하지 않는 처리가 적절합니다.

미처리 영상 목록은 스케줄러 실행 시점의 최신 상태가 중요하므로, @Cacheable 없이 delegate로 직접 위임한 구현이 의도와 잘 맞습니다.

linktrip-output-cache/caffeine/src/main/kotlin/com/linktrip/output/cache/caffeine/adapter/InMemoryVideoAnalysisQueueAdapter.kt (1)

29-33: 우선순위 큐 구성 LGTM.

compareBy({ source.priority }, { sequence }) 조합으로 USER 우선, 동일 priority 내 FIFO가 AtomicLong tiebreaker로 보장됩니다. PriorityBlockingQueue 자체는 unbounded라 INITIAL_CAPACITY=16은 초기 힙 크기일 뿐이며 put 시 블로킹되지 않는 점도 의도와 일치합니다.

linktrip-output-http/src/main/kotlin/com/linktrip/output/http/adapter/ProxyYoutubeClient.kt (2)

42-67: 요청 단위 타임아웃 적용 좋습니다.

GET/POST 모두 REQUEST_TIMEOUT을 설정하고 공통 회전 실행 경로로 보내서 느린 프록시가 컨슈머를 오래 점유하는 위험을 줄였습니다.


76-90: 네트워크 실패/인터럽트/타임아웃 처리가 잘 보강되었습니다.

IOException은 다음 프록시로 넘기고, InterruptedException은 interrupt flag를 복원하며, 연결/요청 타임아웃도 분리되어 있어 이전 장애 전파 위험이 줄었습니다.

Also applies to: 123-153

@toychip toychip merged commit 2071cc4 into main Apr 22, 2026
3 checks passed
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.

[Feat] 영상 분석 파이프라인 견고화 및 미처리 영상 자동 분석 도입

1 participant