[OT-296][FEAT]: workflow 작성 및 서버 간 통신 연결 설정#166
Conversation
Walkthrough새 GitHub Actions 배포 워크플로우를 추가하고, Spring에서 이벤트 기반 비동기 AI 태깅 흐름을 도입했으며, AI 타임아웃 및 머신 컨테이너의 모델 복사·로딩 경로를 명시화했습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant BackOfficeContentsService as BackOffice
participant EventPublisher as Publisher
participant AITaggingAsyncService as TaggingService
participant AiClient
participant AIServer as AI
Client->>BackOffice: createContentsUpload(request)
BackOffice->>BackOffice: linkTags()
BackOffice->>Publisher: publish(AiTaggingRequestedEvent)
Publisher->>TaggingService: handleAiTagging(event) (AFTER_COMMIT, `@Async`)
TaggingService->>AiClient: requestTags(description)
AiClient->>AI: POST /tagging (configured timeout)
AI-->>AiClient: tags response
AiClient-->>TaggingService: return tags
TaggingService->>TaggingService: map -> dedupe -> persist via MediaMoodTagAppend
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
📝 Coding Plan
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/machine/app/services/tagging.py (1)
28-30:⚠️ Potential issue | 🟠 Major모델 로딩 실패를 삼키면 비정상 상태로 서비스가 기동됩니다.
지금은 예외를 로그만 남기고 넘겨서, 서버는 정상처럼 뜨지만 요청 시점에만 실패합니다. 로딩 실패 시 기동 자체를 실패시키는 편이 안전합니다.
💡 제안 수정안 (fail-fast)
except Exception as e: - logger.error(f"모델 로딩 실패. 경로에 모델 파일이 있는지 확인하세요: {e}") + logger.exception(f"모델 로딩 실패. 경로에 모델 파일이 있는지 확인하세요: {e}") + raise RuntimeError(f"태깅 모델 초기화 실패: {self.model_path}") from e🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/machine/app/services/tagging.py` around lines 28 - 30, The current except block around model loading that only calls logger.error("모델 로딩 실패... {e}") swallows startup failures; change it to fail-fast by either re-raising the caught exception or terminating the process after logging so the service does not start in a broken state. Locate the except Exception as e block (the model loading code in apps/machine/app/services/tagging.py that calls logger.error) and replace the silent swallow with either: 1) logger.error(...); raise or 2) logger.error(...); import sys; sys.exit(1). Optionally wrap the original exception in a RuntimeError with contextual message before raising to preserve the stack trace and context.
🧹 Nitpick comments (8)
apps/api-admin/src/main/java/com/ott/api_admin/config/WebClientConfig.java (1)
14-26: 타임아웃이 두 곳에서 설정되고 있습니다.
WebClientConfig의responseTimeout과AiClient의Mono.timeout()모두 타임아웃을 설정합니다. 두 설정 모두 동일한ai.timeout-ms값을 사용하므로 기능적으로 문제는 없지만, 중복 설정입니다.
HttpClient.responseTimeout: 연결 수준 응답 타임아웃Mono.timeout(): 리액티브 스트림 레벨 타임아웃하나로 통일하는 것을 권장합니다. 일반적으로
HttpClient레벨 설정만으로 충분합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/api-admin/src/main/java/com/ott/api_admin/config/WebClientConfig.java` around lines 14 - 26, The code sets timeouts in two places—HttpClient.responseTimeout in WebClientConfig.aiWebClient and Mono.timeout in AiClient—causing redundant timeout behavior; remove the reactive-level timeout by deleting or disabling the Mono.timeout call in AiClient so only the HttpClient-level responseTimeout (configured in WebClientConfig.aiWebClient using ai.timeout-ms) controls request timing, or if you prefer the reactive timeout, remove the responseTimeout configuration and rely on Mono.timeout; update AiClient (the method invoking Mono.timeout) or WebClientConfig.aiWebClient accordingly so only one timeout mechanism remains..github/workflows/deploy-ai.yml (2)
149-156: Docker 컨테이너에 헬스체크가 설정되지 않았습니다.
--restart unless-stopped옵션이 있지만, 컨테이너 헬스체크가 없어 애플리케이션이 비정상 상태에서도 재시작되지 않을 수 있습니다.💡 헬스체크 추가 제안
- "sudo docker run -d --name ${CONTAINER_NAME} --restart unless-stopped -p ${PORT}:${PORT} --env-file ${ENV_FILE} ${IMAGE_URI}" + "sudo docker run -d --name ${CONTAINER_NAME} --restart unless-stopped --health-cmd='curl -f http://localhost:${PORT}/health || exit 1' --health-interval=30s --health-timeout=10s --health-retries=3 -p ${PORT}:${PORT} --env-file ${ENV_FILE} ${IMAGE_URI}"또는 Dockerfile에서
HEALTHCHECK지시어를 추가하는 방법도 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/deploy-ai.yml around lines 149 - 156, Add a container healthcheck so the deployed service can be detected and restarted when unhealthy: modify the docker run command that uses ${IMAGE_URI} and ${CONTAINER_NAME} (the line starting with "sudo docker run -d --name ${CONTAINER_NAME} --restart unless-stopped -p ${PORT}:${PORT} --env-file ${ENV_FILE} ${IMAGE_URI}") to include appropriate --health-cmd, --health-interval and --health-retries flags (or alternatively implement a HEALTHCHECK in the Dockerfile used to build ${IMAGE_URI}); ensure the health command probes the app endpoint or port used by ${PORT} and choose sensible interval/retry values so docker can mark and restart unhealthy containers.
148-151: 오래된 Docker 이미지가 누적될 수 있습니다.배포 시 이전 이미지를 정리하지 않아 디스크 공간이 점차 부족해질 수 있습니다.
🧹 이미지 정리 명령어 추가 제안
컨테이너 실행 후 사용하지 않는 이미지를 정리하는 명령어를 추가하세요:
"sudo docker run -d --name ${CONTAINER_NAME} --restart unless-stopped -p ${PORT}:${PORT} --env-file ${ENV_FILE} ${IMAGE_URI}" + "sudo docker image prune -af --filter 'until=24h'"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/deploy-ai.yml around lines 148 - 151, The workflow currently pulls and runs ${IMAGE_URI} but never cleans up old images, which can fill disk over time; after the "sudo docker run -d --name ${CONTAINER_NAME} ..." step add a cleanup command such as running a non-interactive image prune (e.g., docker image prune -a -f or docker system prune -af) to remove unused images/containers/volumes, ensuring the command references the same environment (use sudo if other docker commands use sudo) and is placed immediately after the run to reclaim space.apps/api-admin/src/main/java/com/ott/api_admin/ai/client/AiClient.java (1)
23-24:@RequiredArgsConstructor와 함께 필드 주입을 사용하고 있습니다.
aiWebClient은 생성자 주입을 사용하지만timeoutMs는 필드 주입을 사용합니다. 일관성과 테스트 용이성을 위해 생성자 주입으로 통일하는 것이 좋습니다.♻️ 생성자 주입으로 통일 제안
`@Slf4j` `@Component` -@RequiredArgsConstructor public class AiClient { private final WebClient aiWebClient; + private final long timeoutMs; - `@Value`("${ai.timeout-ms}") - private Long timeoutMs; + public AiClient( + WebClient aiWebClient, + `@Value`("${ai.timeout-ms}") long timeoutMs + ) { + this.aiWebClient = aiWebClient; + this.timeoutMs = timeoutMs; + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/api-admin/src/main/java/com/ott/api_admin/ai/client/AiClient.java` around lines 23 - 24, The AiClient class mixes field injection for timeoutMs with constructor injection for aiWebClient; change timeoutMs to constructor injection by making it a final field and include it in the generated constructor (or explicitly add a constructor) so both aiWebClient and timeoutMs are injected via constructor (update the field declaration for timeoutMs to be final and remove the `@Value` field injection), ensuring `@RequiredArgsConstructor` covers it or adding a constructor that accepts Long timeoutMs and the existing aiWebClient dependency.apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.java (2)
97-101: 불일치 검증용missingTags계산 결과가 사용되지 않습니다.주석 의도대로라면 로그에 남기거나, 사용하지 않을 거면 계산 자체를 제거하는 편이 좋습니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.java` around lines 97 - 101, The computed missingTags list in AITaggingAsyncService (derived from aiTags.stream().filter(...).distinct().toList()) is not used; either remove this unused computation or log it for DB<->AI mismatch verification—update the code in AITaggingAsyncService to (1) if keeping, call the logger (e.g., processLogger or class logger) to emit a clear message including missingTags and context (aiTags and moodTagByName keys), or (2) if not needed, delete the missingTags variable and its stream pipeline to avoid dead code.
47-101: 컬렉션 변수명 규칙(Listsuffix) 일관성을 맞춰주세요.
aiTags,foundMoodTags,newMediaMoodTags,missingTags는 프로젝트 규칙상List접미사 형태로 맞추는 것이 좋습니다.As per coding guidelines, "Collection variable names with
Listsuffix".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.java` around lines 47 - 101, Rename the collection variables to follow the project's "List" suffix convention: update aiTags -> aiTagsList, foundMoodTags -> foundMoodTagList, newMediaMoodTags -> newMediaMoodTagList, and missingTags -> missingTagList across AITaggingAsyncService (ensure you update all references: the for-loop index/reads, moodTagByName construction/filtering logic, new MediaMoodTag builder usage, and all log messages that reference these variables) so names remain consistent and compile after refactor.apps/machine/app/config.py (1)
13-14: 주석 처리된 레거시 설정 필드는 제거해도 됩니다.사용하지 않는 설정 흔적이 남아 있으면 실제 유효 설정 범위를 읽기 어렵습니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/machine/app/config.py` around lines 13 - 14, Remove the commented legacy config fields by deleting the two commented lines referring to recommend_model_path and model_path in apps/machine/app/config.py; search for any remaining references to recommend_model_path or model_path elsewhere in the codebase (e.g., usages in code or docs) and remove or update them if unused, then run linters/tests to ensure no unresolved references remain.docker-compose.yml (1)
80-81:AI_TIMEOUT_MS를 환경변수 기반으로 열어두는 편이 운영에 안전합니다.현재 고정값이라 스테이징/운영별 튜닝이 어렵습니다. 기본값을 두되 env override 가능하게 바꾸는 걸 권장합니다.
💡 제안 수정안
- AI_TIMEOUT_MS: 30000 + AI_TIMEOUT_MS: ${AI_TIMEOUT_MS:-30000}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docker-compose.yml` around lines 80 - 81, The AI_TIMEOUT_MS value is hard-coded which prevents per-environment tuning; update the docker-compose service environment so AI_TIMEOUT_MS uses Docker Compose variable substitution with a default (so it remains 30000 if unset) and can be overridden by an environment variable at deploy time (modify the AI_TIMEOUT_MS entry in docker-compose.yml where AI_BASE_URL is defined to use the substitution/default pattern).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/api-admin/src/main/java/com/ott/api_admin/ApiAdminApplication.java`:
- Around line 9-16: The async handler AITaggingAsyncService.handleAiTagging
performs multiple repository operations that must be atomic; add the
`@Transactional` annotation to the handleAiTagging method (which already has
`@TransactionalEventListener` and `@Async`) so findById, findByNameInAndStatus,
deleteByMedia_Id and saveAll execute within a single transaction to prevent
partial commits on failure.
In
`@apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.java`:
- Around line 36-38: The delete-then-insert sequence in
AITaggingAsyncService.handleAiTagging (calls to deleteByMedia_Id and saveAll) is
not executed atomically, risking tag loss if saveAll fails; extract that
delete+save logic into a dedicated transactional method (e.g.,
replaceTagsTransactional(mediaId, newTags)) annotated with `@Transactional` (or
`@Transactional`(propagation = Propagation.REQUIRES_NEW)) inside the service and
invoke that method from handleAiTagging so the delete and save run in one DB
transaction; ensure the handler only calls this transactional helper and does
not itself rely on the event listener transaction semantics.
In `@apps/api-admin/src/main/resources/application.yml`:
- Around line 61-63: The ai.timeout-ms value is hardcoded so environment
overrides (AI_TIMEOUT_MS) in docker-compose don't apply; change the YAML entry
to use a Spring env placeholder like ai.timeout-ms: ${AI_TIMEOUT_MS:30000} so
the timeout can be overridden at runtime, and verify any code that reads this
property (e.g., the component or `@ConfigurationProperties` class that reads
ai.base-url / ai.timeout-ms) expects an integer and will parse the injected
value correctly.
---
Outside diff comments:
In `@apps/machine/app/services/tagging.py`:
- Around line 28-30: The current except block around model loading that only
calls logger.error("모델 로딩 실패... {e}") swallows startup failures; change it to
fail-fast by either re-raising the caught exception or terminating the process
after logging so the service does not start in a broken state. Locate the except
Exception as e block (the model loading code in
apps/machine/app/services/tagging.py that calls logger.error) and replace the
silent swallow with either: 1) logger.error(...); raise or 2) logger.error(...);
import sys; sys.exit(1). Optionally wrap the original exception in a
RuntimeError with contextual message before raising to preserve the stack trace
and context.
---
Nitpick comments:
In @.github/workflows/deploy-ai.yml:
- Around line 149-156: Add a container healthcheck so the deployed service can
be detected and restarted when unhealthy: modify the docker run command that
uses ${IMAGE_URI} and ${CONTAINER_NAME} (the line starting with "sudo docker run
-d --name ${CONTAINER_NAME} --restart unless-stopped -p ${PORT}:${PORT}
--env-file ${ENV_FILE} ${IMAGE_URI}") to include appropriate --health-cmd,
--health-interval and --health-retries flags (or alternatively implement a
HEALTHCHECK in the Dockerfile used to build ${IMAGE_URI}); ensure the health
command probes the app endpoint or port used by ${PORT} and choose sensible
interval/retry values so docker can mark and restart unhealthy containers.
- Around line 148-151: The workflow currently pulls and runs ${IMAGE_URI} but
never cleans up old images, which can fill disk over time; after the "sudo
docker run -d --name ${CONTAINER_NAME} ..." step add a cleanup command such as
running a non-interactive image prune (e.g., docker image prune -a -f or docker
system prune -af) to remove unused images/containers/volumes, ensuring the
command references the same environment (use sudo if other docker commands use
sudo) and is placed immediately after the run to reclaim space.
In `@apps/api-admin/src/main/java/com/ott/api_admin/ai/client/AiClient.java`:
- Around line 23-24: The AiClient class mixes field injection for timeoutMs with
constructor injection for aiWebClient; change timeoutMs to constructor injection
by making it a final field and include it in the generated constructor (or
explicitly add a constructor) so both aiWebClient and timeoutMs are injected via
constructor (update the field declaration for timeoutMs to be final and remove
the `@Value` field injection), ensuring `@RequiredArgsConstructor` covers it or
adding a constructor that accepts Long timeoutMs and the existing aiWebClient
dependency.
In `@apps/api-admin/src/main/java/com/ott/api_admin/config/WebClientConfig.java`:
- Around line 14-26: The code sets timeouts in two
places—HttpClient.responseTimeout in WebClientConfig.aiWebClient and
Mono.timeout in AiClient—causing redundant timeout behavior; remove the
reactive-level timeout by deleting or disabling the Mono.timeout call in
AiClient so only the HttpClient-level responseTimeout (configured in
WebClientConfig.aiWebClient using ai.timeout-ms) controls request timing, or if
you prefer the reactive timeout, remove the responseTimeout configuration and
rely on Mono.timeout; update AiClient (the method invoking Mono.timeout) or
WebClientConfig.aiWebClient accordingly so only one timeout mechanism remains.
In
`@apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.java`:
- Around line 97-101: The computed missingTags list in AITaggingAsyncService
(derived from aiTags.stream().filter(...).distinct().toList()) is not used;
either remove this unused computation or log it for DB<->AI mismatch
verification—update the code in AITaggingAsyncService to (1) if keeping, call
the logger (e.g., processLogger or class logger) to emit a clear message
including missingTags and context (aiTags and moodTagByName keys), or (2) if not
needed, delete the missingTags variable and its stream pipeline to avoid dead
code.
- Around line 47-101: Rename the collection variables to follow the project's
"List" suffix convention: update aiTags -> aiTagsList, foundMoodTags ->
foundMoodTagList, newMediaMoodTags -> newMediaMoodTagList, and missingTags ->
missingTagList across AITaggingAsyncService (ensure you update all references:
the for-loop index/reads, moodTagByName construction/filtering logic, new
MediaMoodTag builder usage, and all log messages that reference these variables)
so names remain consistent and compile after refactor.
In `@apps/machine/app/config.py`:
- Around line 13-14: Remove the commented legacy config fields by deleting the
two commented lines referring to recommend_model_path and model_path in
apps/machine/app/config.py; search for any remaining references to
recommend_model_path or model_path elsewhere in the codebase (e.g., usages in
code or docs) and remove or update them if unused, then run linters/tests to
ensure no unresolved references remain.
In `@docker-compose.yml`:
- Around line 80-81: The AI_TIMEOUT_MS value is hard-coded which prevents
per-environment tuning; update the docker-compose service environment so
AI_TIMEOUT_MS uses Docker Compose variable substitution with a default (so it
remains 30000 if unset) and can be overridden by an environment variable at
deploy time (modify the AI_TIMEOUT_MS entry in docker-compose.yml where
AI_BASE_URL is defined to use the substitution/default pattern).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: d537002b-aa24-4eec-aac4-138d9336769b
📒 Files selected for processing (15)
.github/workflows/deploy-ai.yml.gitignoreapps/api-admin/src/main/java/com/ott/api_admin/ApiAdminApplication.javaapps/api-admin/src/main/java/com/ott/api_admin/ai/client/AiClient.javaapps/api-admin/src/main/java/com/ott/api_admin/ai/dto/TaggingRequest.javaapps/api-admin/src/main/java/com/ott/api_admin/config/WebClientConfig.javaapps/api-admin/src/main/java/com/ott/api_admin/content/service/BackOfficeContentsService.javaapps/api-admin/src/main/java/com/ott/api_admin/tagging/event/AiTaggingRequestedEvent.javaapps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.javaapps/api-admin/src/main/resources/application.ymlapps/machine/Dockerfileapps/machine/app/config.pyapps/machine/app/services/tagging.pyapps/machine/models/recommend/.gitkeepdocker-compose.yml
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.java (2)
49-103: 컬렉션 변수명에List접미사 누락코딩 가이드라인에 따르면 컬렉션 변수명에
List접미사를 사용해야 합니다. 현재aiTags,foundMoodTags,newMediaMoodTags,missingTags등이 해당됩니다.예:
aiTagList,foundMoodTagList,newMediaMoodTagList,missingTagListAs per coding guidelines: "Collection variable names with
Listsuffix".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.java` around lines 49 - 103, Rename the collection variables to include the List suffix: change aiTags -> aiTagList, foundMoodTags -> foundMoodTagList, newMediaMoodTags -> newMediaMoodTagList, and missingTags -> missingTagList; update every usage (loop index accesses, streams, collectors, log messages, method arguments like aiClient.getEmotionTags result assignment, moodTagByName lookups, and the final .toList() assignment) to the new names and ensure any related local variables or warnings (e.g., seen set usage) remain consistent so the code compiles and behavior is unchanged.
110-114: 비동기 컨텍스트에서BusinessException재throw가 의미가 없습니다.
@Async메서드에서 예외를 throw해도 호출자에게 전파되지 않습니다.AsyncUncaughtExceptionHandler가 설정되어 있지 않다면,BusinessException도 다른 예외와 동일하게 로그만 남기는 것이 일관성 있습니다.♻️ 일관된 예외 처리 제안
- } catch (BusinessException e) { - throw e; - } catch (Exception e) { + } catch (Exception e) { log.error("[AI Tagging] 미디어 ID: {} - 태깅 저장 중 예외 발생", mediaId, e); }또는
AsyncUncaughtExceptionHandler를 구성하여BusinessException을 별도로 처리하세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.java` around lines 110 - 114, In the `@Async` method inside AITaggingAsyncService, remove the ineffective catch(BusinessException) that simply rethrows the exception and instead handle BusinessException the same way as other exceptions (log the error with mediaId and the exception) or configure an AsyncUncaughtExceptionHandler to handle BusinessException centrally; specifically, update the method that currently catches BusinessException to either (a) delete the catch and let the generic Exception catch log the error, or (b) consolidate both catches so BusinessException is logged via log.error("[AI Tagging] 미디어 ID: {} - 태깅 저장 중 예외 발생", mediaId, e), or alternatively implement/configure AsyncUncaughtExceptionHandler to process BusinessException from async methods.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.java`:
- Around line 99-103: The computed missingTags list (from aiTags filtered
against moodTagByName) is never used; add a log statement in
AITaggingAsyncService after the missingTags computation that logs the
missingTags (and context like the input id or media identifier if available)
using the existing logger (e.g., logger.warn or logger.info) so the "DB <-> AI
태깅 불일치" can be observed; ensure you reference aiTags, moodTagByName and
missingTags in the message and only log when missingTags is non-empty to avoid
noisy logs.
---
Nitpick comments:
In
`@apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.java`:
- Around line 49-103: Rename the collection variables to include the List
suffix: change aiTags -> aiTagList, foundMoodTags -> foundMoodTagList,
newMediaMoodTags -> newMediaMoodTagList, and missingTags -> missingTagList;
update every usage (loop index accesses, streams, collectors, log messages,
method arguments like aiClient.getEmotionTags result assignment, moodTagByName
lookups, and the final .toList() assignment) to the new names and ensure any
related local variables or warnings (e.g., seen set usage) remain consistent so
the code compiles and behavior is unchanged.
- Around line 110-114: In the `@Async` method inside AITaggingAsyncService, remove
the ineffective catch(BusinessException) that simply rethrows the exception and
instead handle BusinessException the same way as other exceptions (log the error
with mediaId and the exception) or configure an AsyncUncaughtExceptionHandler to
handle BusinessException centrally; specifically, update the method that
currently catches BusinessException to either (a) delete the catch and let the
generic Exception catch log the error, or (b) consolidate both catches so
BusinessException is logged via log.error("[AI Tagging] 미디어 ID: {} - 태깅 저장 중 예외
발생", mediaId, e), or alternatively implement/configure
AsyncUncaughtExceptionHandler to process BusinessException from async methods.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 127e837f-60a3-46b6-847c-9029ffe0c042
📒 Files selected for processing (1)
apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.java
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.java (1)
118-122:⚠️ Potential issue | 🟡 Minor
missingTags는 아직도 계산만 하고 버려집니다.Line 119-122에서 만든 리스트를 사용하지 않아, 주석에 적은 DB/AI 불일치 정보가 실제로 남지 않습니다. non-empty일 때 warn 로그나 metric으로 소비해 주세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.java` around lines 118 - 122, The computed List<String> missingTags in AITaggingAsyncService is never used; after creating missingTags from aiTags and moodTagByName, add a non-empty check and consume it: if missingTags is not empty, emit a warn log (e.g., logger.warn("DB <-> AI tag mismatch, missing tags: {}", missingTags)) and/or increment a metric or counter (e.g., metricRegistry.counter("ai.tagging.missing").increment(missingTags.size())) so the DB/AI inconsistency is recorded and observable.
🧹 Nitpick comments (1)
apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.java (1)
90-91: 리스트 타입 로컬 변수명은Listsuffix로 맞춰주세요.변경분 기준으로
newMediaMoodTags,missingTags는 둘 다List<?>타입이라 규칙과 어긋납니다.newMediaMoodTagList,missingTagList처럼 맞춰 두면 일관성이 유지됩니다. As per coding guidelines, 'Collection variable names with List suffix'.Also applies to: 118-122
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.java` around lines 90 - 91, The local List-typed variables do not follow the naming guideline requiring a 'List' suffix; rename newMediaMoodTags to newMediaMoodTagList and missingTags to missingTagList (and any other List-typed locals in the same method/class that follow the old pattern) and update all references/usages accordingly in AITaggingAsyncService (e.g., variable declarations, iterations, additions, and returns) so names consistently end with 'List'.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.java`:
- Around line 56-57: The event handler in AITaggingAsyncService (the method
annotated with `@TransactionalEventListener`(phase =
TransactionPhase.AFTER_COMMIT) and `@Async`) performs a blind delete+saveAll and
can be overwritten by a later/earlier concurrent event for the same mediaId;
change it to enforce per-mediaId latest-write semantics by either (A) checking
entity version/modifiedDate before applying changes: fetch current tagging
metadata (version or modifiedDate) and only perform delete+saveAll if the
event's timestamp/version is newer, or (B) serialize processing per mediaId
using a lock/queue (in-memory ConcurrentHashMap lock or a distributed lock like
Redisson or ShedLock) so only one event for a given mediaId runs at a time;
apply the same guard to the other handler occurrences mentioned (lines around
61-62 and 124-126) so stale events cannot overwrite newer results.
- Around line 56-59: The current handleAiTagging method mixes external AI calls
and DB delete/insert inside one transactional block causing potential atomicity
violations; move the aiClient.getEmotionTags call out of the `@Transactional`
handleAiTagging method so it runs before any DB transaction, then perform the
deleteByMedia_Id and saveAll DB operations inside a dedicated transactional
boundary (either use TransactionTemplate or delegate to a new `@Transactional`
method such as persistMediaTagsTransaction(mediaId, tags)) so failures roll back
the delete, or simply do not swallow non-business exceptions (rethrow them) in
the catch block so the transaction is marked for rollback; also rename
collection variables missingTags -> missingTagsList and newMediaMoodTags ->
newMediaMoodTagsList and consider adding ordering/locking or single-threaded
processing for events with the same mediaId to avoid race conditions.
---
Duplicate comments:
In
`@apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.java`:
- Around line 118-122: The computed List<String> missingTags in
AITaggingAsyncService is never used; after creating missingTags from aiTags and
moodTagByName, add a non-empty check and consume it: if missingTags is not
empty, emit a warn log (e.g., logger.warn("DB <-> AI tag mismatch, missing tags:
{}", missingTags)) and/or increment a metric or counter (e.g.,
metricRegistry.counter("ai.tagging.missing").increment(missingTags.size())) so
the DB/AI inconsistency is recorded and observable.
---
Nitpick comments:
In
`@apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.java`:
- Around line 90-91: The local List-typed variables do not follow the naming
guideline requiring a 'List' suffix; rename newMediaMoodTags to
newMediaMoodTagList and missingTags to missingTagList (and any other List-typed
locals in the same method/class that follow the old pattern) and update all
references/usages accordingly in AITaggingAsyncService (e.g., variable
declarations, iterations, additions, and returns) so names consistently end with
'List'.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: b1e33207-6e63-4691-a246-c750aea871be
📒 Files selected for processing (2)
apps/api-admin/src/main/java/com/ott/api_admin/ai/client/AiClient.javaapps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.java
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/api-admin/src/main/java/com/ott/api_admin/ai/client/AiClient.java
phonil
left a comment
There was a problem hiding this comment.
로컬에서 모델 직접 넣기? 이거 어떻게 하는지 간단하게 알려주세요
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.java (1)
59-59: 컬렉션 변수명에List접미사 누락코딩 가이드라인에 따르면 컬렉션 변수명에는
List접미사를 사용해야 합니다.
aiTags→aiTagListfoundMoodTags→foundMoodTagListnewMediaMoodTags→newMediaMoodTagListmissingTags→missingTagListAs per coding guidelines: "Collection variable names with
Listsuffix"Also applies to: 79-79, 91-91, 119-119
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.java` at line 59, Rename the collection variables in AITaggingAsyncService to follow the "List" suffix guideline: change aiTags to aiTagList, foundMoodTags to foundMoodTagList, newMediaMoodTags to newMediaMoodTagList, and missingTags to missingTagList; update all declarations and every usage/reference within methods (e.g., where aiTags is built/iterated or returned) so identifiers match, and adjust any method parameters, local variables, and related log/messages to use the new names (ensure imports/types remain List<String> and run tests/compile to catch any missed references).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.java`:
- Around line 70-73: The if-block in AITaggingAsyncService (the conditional
checking aiTags == null || aiTags.isEmpty()) has over-indented statements;
adjust the indentation of the inner statements (the log.info("[AI Tagging] 미디어
ID: {} - AI가 반환한 태그가 없습니다.", event.mediaId()) and the return;) to match the
standard block indentation level for the method containing that if, ensuring
they align directly under the if and not indented further.
In
`@apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/MediaMoodTagAppend.java`:
- Around line 17-21: The replaceMediaMoodTags method risks UNIQUE constraint
violations because mediaMoodTagRepository.deleteByMedia_Id(mediaId) may be
delayed by JPA; after the delete call either (A) make the repository delete
method explicitly `@Modifying` `@Query`(...) so it executes immediately or (B)
invoke an EntityManager.flush() (e.g., entityManager.flush()) right after delete
and before mediaMoodTagRepository.saveAll(...); keep the `@Transactional` on
replaceMediaMoodTags and then call saveAll(newMediaMoodTagsList); also rename
the parameter newMediaMoodTags to newMediaMoodTagsList (and update all
references) to follow the List suffix naming convention.
---
Nitpick comments:
In
`@apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.java`:
- Line 59: Rename the collection variables in AITaggingAsyncService to follow
the "List" suffix guideline: change aiTags to aiTagList, foundMoodTags to
foundMoodTagList, newMediaMoodTags to newMediaMoodTagList, and missingTags to
missingTagList; update all declarations and every usage/reference within methods
(e.g., where aiTags is built/iterated or returned) so identifiers match, and
adjust any method parameters, local variables, and related log/messages to use
the new names (ensure imports/types remain List<String> and run tests/compile to
catch any missed references).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 9ed5ec65-e2a9-4b5a-9097-e788b657e6b6
📒 Files selected for processing (2)
apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.javaapps/api-admin/src/main/java/com/ott/api_admin/tagging/service/MediaMoodTagAppend.java
| @Transactional | ||
| public void replaceMediaMoodTags(Long mediaId, List<MediaMoodTag> newMediaMoodTags) { | ||
| mediaMoodTagRepository.deleteByMedia_Id(mediaId); | ||
| mediaMoodTagRepository.saveAll(newMediaMoodTags); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# media_mood_tag 테이블의 UNIQUE 제약 조건 확인
fd -e sql | xargs rg -i "media_mood_tag" -A 10 | rg -i "unique|constraint|index"Repository: OpenTheTaste/backend
Length of output: 700
🏁 Script executed:
fd -e java | xargs rg "class MediaMoodTagRepository" -A 20Repository: OpenTheTaste/backend
Length of output: 48
🏁 Script executed:
fd -e java | xargs rg "class MediaMoodTag" -A 30 | head -100Repository: OpenTheTaste/backend
Length of output: 3156
🏁 Script executed:
cat -n apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/MediaMoodTagAppend.javaRepository: OpenTheTaste/backend
Length of output: 935
🏁 Script executed:
fd -e java | xargs rg "MediaMoodTagRepository" -lRepository: OpenTheTaste/backend
Length of output: 248
🏁 Script executed:
fd -e java | xargs rg "deleteByMedia_Id|class MediaMoodTagRepository" -B 5 -A 10Repository: OpenTheTaste/backend
Length of output: 2084
JPA 플러시 순서 문제로 인한 제약 조건 위반 위험
deleteByMedia_Id()는 @Modifying 어노테이션이 없는 파생 쿼리로, JPA가 SELECT 후 DELETE를 실행합니다. 명시적 flush() 호출 없이 즉시 saveAll()을 호출하면 DELETE가 지연되어 INSERT가 먼저 실행될 수 있습니다. DB 스키마의 (media_id, mood_tag_id) UNIQUE 제약 조건으로 인해 제약 조건 위반이 발생할 수 있습니다.
🔧 해결 방법
`@Transactional`
-public void replaceMediaMoodTags(Long mediaId, List<MediaMoodTag> newMediaMoodTags) {
+public void replaceMediaMoodTags(Long mediaId, List<MediaMoodTag> newMediaMoodTagsList) {
mediaMoodTagRepository.deleteByMedia_Id(mediaId);
+ mediaMoodTagRepository.flush();
- mediaMoodTagRepository.saveAll(newMediaMoodTags);
+ mediaMoodTagRepository.saveAll(newMediaMoodTagsList);
}또한 컬렉션 변수명은 List 접미사를 사용하세요: newMediaMoodTags → newMediaMoodTagsList
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @Transactional | |
| public void replaceMediaMoodTags(Long mediaId, List<MediaMoodTag> newMediaMoodTags) { | |
| mediaMoodTagRepository.deleteByMedia_Id(mediaId); | |
| mediaMoodTagRepository.saveAll(newMediaMoodTags); | |
| } | |
| `@Transactional` | |
| public void replaceMediaMoodTags(Long mediaId, List<MediaMoodTag> newMediaMoodTagsList) { | |
| mediaMoodTagRepository.deleteByMedia_Id(mediaId); | |
| mediaMoodTagRepository.flush(); | |
| mediaMoodTagRepository.saveAll(newMediaMoodTagsList); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/MediaMoodTagAppend.java`
around lines 17 - 21, The replaceMediaMoodTags method risks UNIQUE constraint
violations because mediaMoodTagRepository.deleteByMedia_Id(mediaId) may be
delayed by JPA; after the delete call either (A) make the repository delete
method explicitly `@Modifying` `@Query`(...) so it executes immediately or (B)
invoke an EntityManager.flush() (e.g., entityManager.flush()) right after delete
and before mediaMoodTagRepository.saveAll(...); keep the `@Transactional` on
replaceMediaMoodTags and then call saveAll(newMediaMoodTagsList); also rename
the parameter newMediaMoodTags to newMediaMoodTagsList (and update all
references) to follow the List suffix naming convention.
📝 작업 내용
트랜잭션이 걸려있는 상태에서 AI서버로 비동기 호출을 보내고 있기 때문에 정합성을 맞추기위해
Spring에서 제공하는 EventListner를 사용했습니다.
📷 스크린샷
☑️ 체크 리스트
#️⃣ 연관된 이슈
💬 리뷰 요구사항
env 변경점 있습니다. 해당 pr 머지 후 수정하겠습니다.
배포환경에서는 CI 진행 중 S3에서 모델을 다운로드 받아서 도커 내부 이미지로 저장되며
로컬환경에서는 모델을 직접 넣으셔야됩니다.
오쁠티 구글 드라이브 공유함에 모델 넣었으니 사용하실 때 참고 바랍니다 :)
추가로 AI서버 수동 배포 금지입니다.
아직 EC2 및 AWS 변수 설정 안해서 터질겁니다.
Summary by CodeRabbit
새로운 기능
개선사항
구성 변경
기타