Skip to content

[✨Feat] 키워드 분석 OpenAI worker 구현#61

Merged
yong203 merged 5 commits into
devfrom
feat/#60
Jul 8, 2026
Merged

[✨Feat] 키워드 분석 OpenAI worker 구현#61
yong203 merged 5 commits into
devfrom
feat/#60

Conversation

@yong203

@yong203 yong203 commented Jul 8, 2026

Copy link
Copy Markdown
Member

작업 내용

  • KEYWORD_ANALYSIS LLM Job을 after-commit 비동기 worker로 실행하도록 연결했습니다.
  • OpenAI 키워드 분석 client를 추가하고, 자기소개서 문항별 최종 작성본(finalAnswer)을 입력으로 키워드를 추출하도록 구현했습니다.
  • 키워드 분석 완료 시 KeywordAnalysisKeyword에 결과를 저장하고, Job은 COMPLETEDKEYWORD_ANALYSIS resultRef로 마킹합니다.
  • provider 실패와 출력 validation 실패는 LlmJob error 정보로 기록하고, KeywordAnalysisFAILED 상태로 전환합니다.
  • 최신 키워드 분석 조회는 COMPLETED 상태일 때만 keywords를 반환하도록 보강했습니다.
  • API/status 문서를 worker 구현 범위에 맞게 갱신했습니다.

관련 이슈

문서 반영

  • 반영한 문서:
    • docs/api/keyword-analysis.md
    • docs/status.md

확인 결과

  • ./gradlew test
  • ./gradlew check
  • 기타: git diff --check
  • 실행하지 못함. 이유:

Summary by CodeRabbit

  • 새 기능
    • 키워드 분석이 비동기 작업으로 실행되고, 완료 시 최신 결과가 저장되며 항목별 점검을 거칩니다.
  • 버그 수정
    • 분석 상태가 완료된 경우에만 키워드를 조회할 수 있게 개선했습니다.
    • 외부 오류/출력 검증 실패/예기치 않은 오류를 각각 적절한 실패 상태로 기록합니다.
  • 문서
    • 키워드 분석 API와 기능 로드맵 상태를 최신 구현 흐름에 맞게 갱신했습니다.
  • 테스트
    • 클라이언트 검증, 작업 이벤트 연동, 워커 실행 시나리오를 추가로 보강했습니다.

@yong203 yong203 linked an issue Jul 8, 2026 that may be closed by this pull request
9 tasks
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 23aa363e-5c9f-47ba-9eb5-afaa327a022f

📥 Commits

Reviewing files that changed from the base of the PR and between ebddb04 and ac41014.

📒 Files selected for processing (5)
  • src/main/java/com/daon/rewrite/keywordanalysis/client/OpenAiKeywordAnalysisClient.java
  • src/main/java/com/daon/rewrite/keywordanalysis/service/KeywordAnalysisJobTransactionService.java
  • src/main/java/com/daon/rewrite/keywordanalysis/service/KeywordAnalysisJobWorker.java
  • src/test/java/com/daon/rewrite/keywordanalysis/client/OpenAiKeywordAnalysisClientTest.java
  • src/test/java/com/daon/rewrite/keywordanalysis/service/KeywordAnalysisJobWorkerTest.java
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/main/java/com/daon/rewrite/keywordanalysis/service/KeywordAnalysisJobWorker.java
  • src/main/java/com/daon/rewrite/keywordanalysis/client/OpenAiKeywordAnalysisClient.java
  • src/main/java/com/daon/rewrite/keywordanalysis/service/KeywordAnalysisJobTransactionService.java

📝 Walkthrough

Walkthrough

KEYWORD_ANALYSIS LLM Job을 처리하는 OpenAI client, worker, transaction service, 이벤트 리스너를 추가했다. 최신 조회는 COMPLETED 상태에서만 키워드를 반환하도록 바뀌었고, 관련 문서와 테스트가 함께 갱신되었다.

Changes

키워드 분석 worker 구현

Layer / File(s) Summary
데이터 계약 및 클라이언트 인터페이스 정의
src/main/java/com/daon/rewrite/keywordanalysis/client/*, src/main/java/com/daon/rewrite/keywordanalysis/service/KeywordAnalysisWork.java
키워드 분석 요청/결과/작업 record와 클라이언트 인터페이스, 예외 타입, OpenAI 응답 DTO를 정의했다.
OpenAI 클라이언트 구현 및 검증
src/main/java/com/daon/rewrite/keywordanalysis/client/OpenAiKeywordAnalysisClient.java, src/test/java/com/daon/rewrite/keywordanalysis/client/OpenAiKeywordAnalysisClientTest.java
ChatClient 호출, 프롬프트 구성, 응답 정규화, 예외 매핑을 구현하고 관련 테스트를 추가했다.
Job 트랜잭션 서비스 및 worker 실행 흐름
src/main/java/com/daon/rewrite/keywordanalysis/service/KeywordAnalysisJobTransactionService.java, src/main/java/com/daon/rewrite/keywordanalysis/service/KeywordAnalysisJobWorker.java, src/main/java/com/daon/rewrite/keywordanalysis/service/KeywordAnalysisJobEventListener.java, src/main/java/com/daon/rewrite/keywordanalysis/repository/KeywordAnalysisKeywordRepository.java
잡 시작/완료/실패 전이, 키워드 결과 저장, after-commit worker 디스패치, 삭제 쿼리 확장을 추가했다.
완료 상태 기반 조회 필터링
src/main/java/com/daon/rewrite/keywordanalysis/service/KeywordAnalysisService.java, src/test/java/com/daon/rewrite/keywordanalysis/service/KeywordAnalysisServiceTest.java
findMyLatestKeywordAnalysis가 완료 상태에서만 저장된 키워드를 반환하도록 변경했다.
worker 통합 테스트 및 문서
src/test/java/com/daon/rewrite/keywordanalysis/service/KeywordAnalysisJobEventIntegrationTest.java, src/test/java/com/daon/rewrite/keywordanalysis/service/KeywordAnalysisJobWorkerTest.java, docs/api/keyword-analysis.md, docs/status.md
worker 성공/실패/재실행 시나리오와 이벤트 디스패치를 검증하고, API/상태 문서를 갱신했다.

Possibly related PRs

  • Rewrite-Team/Rewrite-BE#49: 동일한 LlmJobCreatedEvent 이후 비동기 worker 디스패치 패턴을 다른 잡 타입에 적용한 PR이다.
  • Rewrite-Team/Rewrite-BE#57: 키워드 분석 시작/저장 기반 위에 worker 완료 처리와 조회 필터링이 이어진다.
  • Rewrite-Team/Rewrite-BE#59: findMyLatestKeywordAnalysis 결과와 KeywordAnalysisKeyword 노출 조건을 함께 다룬다.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 키워드 분석 OpenAI worker 구현이라는 핵심 변경을 정확히 요약합니다.
Description check ✅ Passed 필수 섹션인 작업 내용, 관련 이슈, 문서 반영, 확인 결과가 모두 포함되어 있습니다.
Linked Issues check ✅ Passed after-commit dispatch, worker/client 연동, finalAnswer 입력, 결과 저장과 실패 처리, 조회 제한이 이슈 요구사항과 일치합니다.
Out of Scope Changes check ✅ Passed 문서, 테스트, 서비스/클라이언트 추가가 모두 키워드 분석 워커 구현 범위 안에 있습니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

@yong203 yong203 self-assigned this Jul 8, 2026
@yong203 yong203 added the ✨ Feature 새로운 기능 구현 label Jul 8, 2026
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

Backend CI Report

Item Result
Tests ✅ Passed
JaCoCo line coverage 95.45% (1280/1341 lines)
Coverage report ✅ Generated
Codecov upload ✅ Uploaded

View workflow run | View coverage details

@yong203 yong203 marked this pull request as ready for review July 8, 2026 14:20
@codecov

codecov Bot commented Jul 8, 2026

Copy link
Copy Markdown

@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: 5

🧹 Nitpick comments (5)
src/test/java/com/daon/rewrite/keywordanalysis/service/KeywordAnalysisJobWorkerTest.java (1)

192-204: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

이미 완료된 Job 재실행 방지 검증에 상태 불변 확인 추가 고려.

keywordAnalysisClient.shouldHaveNoInteractions()만 검증하고 있어 skip 경로는 확인되지만, KeywordAnalysis/LlmJob 상태가 그대로 COMPLETED로 유지되는지는 별도로 검증하지 않습니다. transactionService.start()가 null을 반환하는 경로 자체는 이 테스트로 충분히 검증되므로 우선순위는 낮습니다.

🤖 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/test/java/com/daon/rewrite/keywordanalysis/service/KeywordAnalysisJobWorkerTest.java`
around lines 192 - 204, In executeSkipsAlreadyCompletedJob, the test only
verifies keywordAnalysisClient is untouched, but it should also assert that the
existing LlmJob/KeywordAnalysis state remains COMPLETED after
worker.execute("job_1"). Update this test to reload the job from
llmJobRepository after execution and verify its status/result fields are
unchanged, so the skip path is covered beyond just no client interaction.
src/main/java/com/daon/rewrite/keywordanalysis/service/KeywordAnalysisJobTransactionService.java (2)

148-166: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

fail()에 keywordAnalysis 상태 검증 누락

complete()keywordAnalysis.getStatus() != PROCESSING일 때 INTERNAL_ERROR를 던지는 반면(Line 118-120), fail()은 동일한 검증 없이 무조건 keywordAnalysis.fail(now)를 호출합니다. job 상태 가드로 인해 현재 구조에서는 실제 불일치가 발생하기 어렵지만, 향후 코드 변경(예: 동시 실행 경로 추가) 시 이미 COMPLETED인 분석을 FAILED로 되돌릴 위험이 있어 대칭적으로 방어 검증을 추가하는 것이 안전합니다.

♻️ 제안
         KeywordAnalysis keywordAnalysis = keywordAnalysisRepository.findByCoverLetterId(job.getTargetId())
                 .orElseThrow(() -> new BusinessException(ErrorCode.INTERNAL_ERROR));
+        if (keywordAnalysis.getStatus() != KeywordAnalysisStatus.PROCESSING) {
+            throw new BusinessException(ErrorCode.INTERNAL_ERROR);
+        }
         Instant now = Instant.now(clock);

178-190: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

LGTM, 사소한 중복 제안

errorCode/errorMessage는 동일한 조건 분기를 반복합니다. 필요 시 Reason을 키로 하는 맵이나 record 하나로 묶어 중복을 줄일 수 있으나, 현재 크기에서는 실익이 크지 않습니다.

🤖 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/daon/rewrite/keywordanalysis/service/KeywordAnalysisJobTransactionService.java`
around lines 178 - 190, Both errorCode and errorMessage repeat the same Reason
check, so if you choose to refactor, centralize the mapping in
KeywordAnalysisJobTransactionService by using a single helper or a
Reason-to-result structure instead of duplicating the OUTPUT_VALIDATION_FAILED
branch in two methods. Keep the existing behavior for
KeywordAnalysisClientException.Reason.OUTPUT_VALIDATION_FAILED versus the
default provider case, but make the lookup shared so future changes only need to
be updated in one place.
src/main/java/com/daon/rewrite/keywordanalysis/client/OpenAiKeywordAnalysisClient.java (2)

43-52: 🩺 Stability & Availability | 🔵 Trivial

외부 OpenAI 호출에 대한 타임아웃 설정 확인 필요.

chatClient.prompt()...call()은 OpenAI 응답을 동기적으로 기다리는 블로킹 호출입니다. 이 파일에서는 타임아웃 설정이 보이지 않으므로, worker 스레드가 provider 응답 지연 시 무한정 블로킹되지 않도록 application.yml(또는 RestClient/WebClient 설정)에 명시적인 connect/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/daon/rewrite/keywordanalysis/client/OpenAiKeywordAnalysisClient.java`
around lines 43 - 52, The OpenAI call in OpenAiKeywordAnalysisClient is a
blocking chatClient.prompt().call() and currently has no visible timeout
protection. Verify that the underlying client configuration adds explicit
connect/read timeouts in application.yml or the RestClient/WebClient setup used
by OpenAiKeywordAnalysisClient, and ensure the provider call cannot block worker
threads indefinitely. If no timeout is configured, add one at the client
configuration layer rather than in the response handling code.

33-53: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Spring AI 2.0.0 GA와 validateSchema() 적용 검토

spring-ai-starter-model-openai가 아직 2.0.0-M8이므로 GA로 올리고, entity(OpenAiKeywordAnalysisResponse.class, spec -> spec.validateSchema())를 쓰면 스키마 불일치 시 모델 재시도를 유도해 지금처럼 곧바로 OUTPUT_VALIDATION_FAILED로 떨어지는 빈도를 줄일 수 있습니다.

🤖 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/daon/rewrite/keywordanalysis/client/OpenAiKeywordAnalysisClient.java`
around lines 33 - 53, The OpenAI response parsing in
OpenAiKeywordAnalysisClient.analyze() is still using the plain
entity(OpenAiKeywordAnalysisResponse.class) path, so schema mismatches fail
immediately and the dependency is on the M8 starter. Update the Spring AI OpenAI
starter to the GA 2.0.0 release, then change the ChatClient call in analyze() to
use entity(..., spec -> spec.validateSchema()) so schema validation can trigger
model retry behavior before falling back to
KeywordAnalysisClientException.outputValidationFailed.
🤖 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
`@src/main/java/com/daon/rewrite/keywordanalysis/client/OpenAiKeywordAnalysisClient.java`:
- Around line 62-77: The validateAndNormalize method in
OpenAiKeywordAnalysisClient is rejecting responses with more than
MAX_KEYWORD_COUNT keywords instead of trimming them. Remove the size-based
outputValidationFailed check for oversized keyword lists, keep the null/empty
validation, and after normalizing/sorting by KeywordAnalysisResult.importance in
descending order, apply limit(MAX_KEYWORD_COUNT) so only the top 20 are
returned.

In
`@src/main/java/com/daon/rewrite/keywordanalysis/service/KeywordAnalysisJobEventListener.java`:
- Around line 20-24: `KeywordAnalysisJobEventListener.handle()` currently lets
exceptions from `dispatch`/`KeywordAnalysisJobWorker.execute()` escape the
`@Async` boundary, so they are only handled by
`SimpleAsyncUncaughtExceptionHandler` and the `LlmJob` can remain stuck. Update
`handle()` to catch unexpected runtime exceptions around the dispatch path, then
route them through the same failure handling used for job execution: mark the
`LlmJob` as failed (or otherwise transition it out of `PENDING`/`PROCESSING`)
and add a shared logging/alerting path. Use the `handle`, `dispatch`, and
`KeywordAnalysisJobWorker.execute` symbols to keep the fix aligned with the
async event flow.

In
`@src/main/java/com/daon/rewrite/keywordanalysis/service/KeywordAnalysisJobTransactionService.java`:
- Line 59: The `KeywordAnalysisJobWorker.execute()` flow only handles
`KeywordAnalysisClientException`, so `BusinessException` and other unexpected
errors can escape and leave the job in `PENDING`. Update `execute()` to catch
generic exceptions as well, then route them through the existing `fail()` path
or at least log them clearly and mark the job as failed. Use the existing
`fail()` logic and `KeywordAnalysisJobWorker`/`start()` entry points to keep the
status transition consistent.

In
`@src/main/java/com/daon/rewrite/keywordanalysis/service/KeywordAnalysisJobWorker.java`:
- Around line 18-30: `KeywordAnalysisJobWorker.execute` only handles
`KeywordAnalysisClientException`, so failures from
`transactionService.start(jobId)` or other runtime exceptions can escape and
leave the job stuck. Move the start/analysis/complete flow into a single `try`
block in `execute`, and add a catch path that routes any `BusinessException` or
unexpected exception through `transactionService.fail(jobId, ...)` as well,
using the existing `KeywordAnalysisJobWorker`, `transactionService`, and
`keywordAnalysisClient` flow to keep all errors on the same failure path.

---

Nitpick comments:
In
`@src/main/java/com/daon/rewrite/keywordanalysis/client/OpenAiKeywordAnalysisClient.java`:
- Around line 43-52: The OpenAI call in OpenAiKeywordAnalysisClient is a
blocking chatClient.prompt().call() and currently has no visible timeout
protection. Verify that the underlying client configuration adds explicit
connect/read timeouts in application.yml or the RestClient/WebClient setup used
by OpenAiKeywordAnalysisClient, and ensure the provider call cannot block worker
threads indefinitely. If no timeout is configured, add one at the client
configuration layer rather than in the response handling code.
- Around line 33-53: The OpenAI response parsing in
OpenAiKeywordAnalysisClient.analyze() is still using the plain
entity(OpenAiKeywordAnalysisResponse.class) path, so schema mismatches fail
immediately and the dependency is on the M8 starter. Update the Spring AI OpenAI
starter to the GA 2.0.0 release, then change the ChatClient call in analyze() to
use entity(..., spec -> spec.validateSchema()) so schema validation can trigger
model retry behavior before falling back to
KeywordAnalysisClientException.outputValidationFailed.

In
`@src/main/java/com/daon/rewrite/keywordanalysis/service/KeywordAnalysisJobTransactionService.java`:
- Around line 178-190: Both errorCode and errorMessage repeat the same Reason
check, so if you choose to refactor, centralize the mapping in
KeywordAnalysisJobTransactionService by using a single helper or a
Reason-to-result structure instead of duplicating the OUTPUT_VALIDATION_FAILED
branch in two methods. Keep the existing behavior for
KeywordAnalysisClientException.Reason.OUTPUT_VALIDATION_FAILED versus the
default provider case, but make the lookup shared so future changes only need to
be updated in one place.

In
`@src/test/java/com/daon/rewrite/keywordanalysis/service/KeywordAnalysisJobWorkerTest.java`:
- Around line 192-204: In executeSkipsAlreadyCompletedJob, the test only
verifies keywordAnalysisClient is untouched, but it should also assert that the
existing LlmJob/KeywordAnalysis state remains COMPLETED after
worker.execute("job_1"). Update this test to reload the job from
llmJobRepository after execution and verify its status/result fields are
unchanged, so the skip path is covered beyond just no client interaction.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 617623ae-e0ab-4743-a556-0eccf02fe1da

📥 Commits

Reviewing files that changed from the base of the PR and between 68a8456 and ebddb04.

📒 Files selected for processing (19)
  • docs/api/keyword-analysis.md
  • docs/status.md
  • src/main/java/com/daon/rewrite/keywordanalysis/client/KeywordAnalysisAnswer.java
  • src/main/java/com/daon/rewrite/keywordanalysis/client/KeywordAnalysisClient.java
  • src/main/java/com/daon/rewrite/keywordanalysis/client/KeywordAnalysisClientException.java
  • src/main/java/com/daon/rewrite/keywordanalysis/client/KeywordAnalysisRequest.java
  • src/main/java/com/daon/rewrite/keywordanalysis/client/KeywordAnalysisResult.java
  • src/main/java/com/daon/rewrite/keywordanalysis/client/OpenAiKeywordAnalysisClient.java
  • src/main/java/com/daon/rewrite/keywordanalysis/client/OpenAiKeywordAnalysisResponse.java
  • src/main/java/com/daon/rewrite/keywordanalysis/repository/KeywordAnalysisKeywordRepository.java
  • src/main/java/com/daon/rewrite/keywordanalysis/service/KeywordAnalysisJobEventListener.java
  • src/main/java/com/daon/rewrite/keywordanalysis/service/KeywordAnalysisJobTransactionService.java
  • src/main/java/com/daon/rewrite/keywordanalysis/service/KeywordAnalysisJobWorker.java
  • src/main/java/com/daon/rewrite/keywordanalysis/service/KeywordAnalysisService.java
  • src/main/java/com/daon/rewrite/keywordanalysis/service/KeywordAnalysisWork.java
  • src/test/java/com/daon/rewrite/keywordanalysis/client/OpenAiKeywordAnalysisClientTest.java
  • src/test/java/com/daon/rewrite/keywordanalysis/service/KeywordAnalysisJobEventIntegrationTest.java
  • src/test/java/com/daon/rewrite/keywordanalysis/service/KeywordAnalysisJobWorkerTest.java
  • src/test/java/com/daon/rewrite/keywordanalysis/service/KeywordAnalysisServiceTest.java

@yong203 yong203 merged commit e176abd into dev Jul 8, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 새로운 기능 구현

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[✨Feat] 키워드 분석 OpenAI worker 구현

1 participant