[OTT-277][FEAT]: flyway 신규 버전 및 엔티티 추가#158
Conversation
…o OT-277-feature/tagging-ml
Walkthrough이 PR은 머신러닝 모델 기반의 감정 태깅 시스템을 구현합니다. 비동기 처리, 오류 처리 프레임워크, 새로운 데이터베이스 스키마, API 엔드포인트 재설계를 포함하며 PyTorch 모델을 백엔드와 통합합니다. Changes
Sequence DiagramsequenceDiagram
participant Client
participant API as API Server
participant AsyncService as AITaggingAsyncService
participant MLService as MoodTagger
participant Repository as Repository Layer
participant Database as Database
Client->>API: POST /tagging (media_id, description)
API->>API: Validate Request
API->>AsyncService: processAiTagging(mediaId, description)
activate AsyncService
AsyncService->>MLService: predict(description, top_k=3)
activate MLService
MLService->>MLService: Load Model & Tokenize
MLService->>MLService: Run Inference (Sigmoid)
MLService->>MLService: Select Top-K Labels
MLService-->>AsyncService: mood_tags[list]
deactivate MLService
AsyncService->>Repository: findByNameInAndStatus(mood_tags)
Repository->>Database: Query MoodTag
Database-->>Repository: MoodTag Entities
Repository-->>AsyncService: MoodTag List
AsyncService->>Repository: deleteByMedia_Id(mediaId)
Repository->>Database: Delete Existing Associations
AsyncService->>Repository: save(MediaMoodTag List)
Repository->>Database: Insert New Associations
Database-->>Repository: Success
Repository-->>AsyncService: Persisted Entities
AsyncService-->>API: Completion
deactivate AsyncService
API-->>Client: 202 Accepted
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 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)
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: 5
🧹 Nitpick comments (10)
modules/infra-db/src/main/resources/db/migration/V7__add_mood_tag_tables.sql (1)
41-49:mood_tag_id단일 컬럼 인덱스 추가 고려현재 유니크 인덱스
uq_media_mood_tag_media_id_mood_tag_id는media_id가 선행 컬럼이므로,mood_tag_id로만 필터링하는 쿼리(예: 특정 태그가 적용된 모든 미디어 조회)에서는 인덱스 활용이 제한적입니다. 역방향 조회가 빈번하다면mood_tag_id단일 인덱스 추가를 검토해 주세요.📊 선택적 인덱스 추가 제안
CREATE UNIQUE INDEX uq_media_mood_tag_media_id_mood_tag_id ON media_mood_tag (media_id, mood_tag_id); + +CREATE INDEX idx_media_mood_tag_mood_tag_id + ON media_mood_tag (mood_tag_id);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@modules/infra-db/src/main/resources/db/migration/V7__add_mood_tag_tables.sql` around lines 41 - 49, Add a single-column index on media_mood_tag.mood_tag_id to support queries that filter by mood_tag_id alone; locate the media_mood_tag table (see existing unique index uq_media_mood_tag_media_id_mood_tag_id and FKs fk_media_mood_tag_to_media / fk_media_mood_tag_to_mood_tag) and create a non-unique index like idx_media_mood_tag_mood_tag_id on mood_tag_id to improve reverse-lookups of all media for a given mood tag.modules/domain/src/main/java/com/ott/domain/mood_tag/repository/MoodTagRepository.java (1)
11-11: 컬렉션 파라미터 네이밍 컨벤션코딩 가이드라인에 따르면 컬렉션 변수명에는
List접미사를 사용해야 합니다.aiTags→aiTagNameList또는nameList로 변경을 권장합니다.♻️ 네이밍 수정 제안
- List<MoodTag> findByNameInAndStatus(List<String> aiTags, Status status); + List<MoodTag> findByNameInAndStatus(List<String> nameList, Status status);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 `@modules/domain/src/main/java/com/ott/domain/mood_tag/repository/MoodTagRepository.java` at line 11, Rename the collection parameter in the repository method to follow the "List" suffix convention: update the signature of MoodTagRepository's findByNameInAndStatus to use a clearer name such as aiTagNameList or nameList (i.e., change List<String> aiTags to List<String> aiTagNameList) and update all call sites and usages accordingly so parameter naming remains consistent with the coding guideline.modules/domain/src/main/java/com/ott/domain/mood_category/repository/MoodCategoryRepository.java (1)
3-8: 사용되지 않는 import 제거
Status,List,Optional이 import되었으나 인터페이스 내에서 사용되지 않습니다.♻️ 불필요한 import 제거
package com.ott.domain.mood_category.repository; -import com.ott.domain.common.Status; import com.ott.domain.mood_category.domain.MoodCategory; import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; -import java.util.Optional; - public interface MoodCategoryRepository extends JpaRepository<MoodCategory, Long> { }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@modules/domain/src/main/java/com/ott/domain/mood_category/repository/MoodCategoryRepository.java` around lines 3 - 8, Remove the unused imports in the MoodCategoryRepository interface: delete the imported symbols Status, List, and Optional from the import block so only actual dependencies (e.g., MoodCategory and JpaRepository) remain; update the import section in the MoodCategoryRepository file to reflect only the types referenced by the interface.apps/machine/requirements.txt (1)
6-8: ML 의존성 버전 고정 필요
torch,transformers,numpy에 버전이 명시되지 않아 빌드 재현성 문제가 발생할 수 있습니다. PyTorch와 Transformers는 메이저 버전 간 호환성 변경이 잦아 버전 고정이 중요합니다.♻️ 버전 고정 예시
-torch -transformers -numpy +torch>=2.1.0,<3.0.0 +transformers>=4.35.0,<6.0.0 +numpy>=1.24.0,<2.5.0🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/machine/requirements.txt` around lines 6 - 8, requirements.txt에 torch, transformers, numpy가 버전 미지정으로 적혀 있어 빌드 재현성이 깨집니다; requirements.txt에서 각각의 패키지(torch, transformers, numpy)를 명시적 버전(예: == 또는 ~= 등 재현성을 보장하는 연산자)으로 고정하고, PyTorch-Transformers 상호 호환성을 확인해 호환 가능한 메이저/마이너 조합을 선택하며(또는 주석으로 호환성 근거를 남김) 필요하면 별도의 constraints 파일로 관리하도록 변경하세요.apps/machine/app/main.py (1)
63-69: Health 체크 엔드포인트 개선 고려현재 health check는 단순히
{"status": "ok"}를 반환합니다. ML 모델 로딩 상태를 포함하면 운영 모니터링에 더 유용할 수 있습니다.♻️ 개선 예시
from app.services.tagging import mood_tagger `@app.get`("/health") def health() -> dict[str, str]: model_loaded = mood_tagger.model is not None return { "status": "ok" if model_loaded else "degraded", "model_loaded": model_loaded, }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/machine/app/main.py` around lines 63 - 69, The /health endpoint currently returns a static {"status":"ok"}; update the health() function to check the ML model load state (use the mood_tagger object from app.services.tagging, e.g., mood_tagger.model) and return a JSON that includes model_loaded (boolean) and set status to "ok" when model_loaded is true and "degraded" when false; ensure you import mood_tagger at top and keep the endpoint signature health() -> dict[str, Any] (or dict[str, str|bool]) to reflect the added field.apps/machine/app/services/tagging.py (1)
27-28: 예외 로깅 개선:logger.exception()사용 권장정적 분석 도구(Ruff TRY400)에서 지적한 대로, 예외 발생 시 스택 트레이스를 자동으로 포함하려면
logger.exception()을 사용하는 것이 좋습니다.♻️ 제안된 수정사항
except Exception as e: - logger.error(f"모델 로딩 실패. 경로에 모델 파일이 있는지 확인하세요: {e}") + logger.exception("모델 로딩 실패. 경로에 모델 파일이 있는지 확인하세요.")🤖 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 27 - 28, Replace the current exception log call that uses logger.error in the except Exception as e block with logger.exception so the stack trace is recorded; locate the except Exception as e handler around the model-loading logic (the block that now logs "모델 로딩 실패. 경로에 모델 파일이 있는지 확인하세요: {e}") and change the call to logger.exception with the same descriptive message to capture the traceback.apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.java (1)
53-66: 코딩 가이드라인: 컬렉션 변수명에List접미사 사용 권장코딩 가이드라인에 따라 컬렉션 변수명에
List접미사를 사용해야 합니다.♻️ 제안된 수정사항
- List<MoodTag> foundMoodTags = moodTagRepository.findByNameInAndStatus(aiTags, Status.ACTIVE); - Map<String, MoodTag> moodTagByName = foundMoodTags.stream() + List<MoodTag> foundMoodTagList = moodTagRepository.findByNameInAndStatus(aiTags, Status.ACTIVE); + Map<String, MoodTag> moodTagByName = foundMoodTagList.stream() .collect(Collectors.toMap(MoodTag::getName, Function.identity(), (left, right) -> left, LinkedHashMap::new)); - List<MediaMoodTag> newMediaMoodTags = IntStream.range(0, aiTags.size()) + List<MediaMoodTag> newMediaMoodTagList = IntStream.range(0, aiTags.size())마찬가지로
aiTags→aiTagList,missingTags→missingTagList로 변경을 고려하세요.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 53 - 66, Rename collection variables to follow the `List` suffix guideline: replace `aiTags` with `aiTagList` (and `missingTags` with `missingTagList` where used) throughout the AITaggingAsyncService class/method so all references (calls to moodTagRepository.findByNameInAndStatus(aiTagList,...), the IntStream.range mapping, filters using aiTagList.indexOf(...), and creation of newMediaMoodTags via MediaMoodTag.builder()) are updated consistently; ensure local variables foundMoodTags, moodTagByName and newMediaMoodTags remain unchanged but consume the renamed aiTagList variable and update any parameter names/signatures that pass the collection to other methods.apps/machine/app/api/tagging.py (2)
11-24:ErrorItem및ErrorResponse모델 중복 가능성
errors.py에서 이미 유사한 에러 응답 구조를 정의하고 있습니다. OpenAPI 문서화를 위해 별도로 정의한 것일 수 있으나, 공통 모델로 통합하여 일관성을 유지하는 것을 고려하세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/machine/app/api/tagging.py` around lines 11 - 24, ErrorItem and ErrorResponse are duplicated here; replace these local definitions with the shared models from the common errors module: remove the ErrorItem and ErrorResponse classes in this file and import the canonical ErrorItem and ErrorResponse from the existing errors.py (preserving Field descriptions/examples there or augmenting them if needed), then update any references in functions/route decorators in this module to use the imported ErrorItem/ErrorResponse symbols so OpenAPI docs remain identical.
143-147: 예외 체인 보존 및 로깅 개선정적 분석 도구(Ruff B904)에서 지적한 대로, 예외를 다시 발생시킬 때 원본 예외를 연결하면 디버깅이 용이합니다. 또한 프로덕션 환경에서
str(e)를 응답에 포함하면 민감한 정보가 노출될 수 있습니다.♻️ 제안된 수정사항
except AppException: raise except Exception as e: + # 프로덕션에서는 상세 에러 메시지 노출 주의 raise AppException( ErrorCode.INTERNAL_ERROR, - detail=f"태깅 처리 중 예외가 발생했습니다: {str(e)}", - ) + detail="태깅 처리 중 예외가 발생했습니다", + ) from e🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/machine/app/api/tagging.py` around lines 143 - 147, Replace the current except block that raises AppException with a pattern that preserves the original exception chain and avoids exposing exception text in the API response: log the original exception (including stack trace) using the module logger inside the except handler, then raise AppException(ErrorCode.INTERNAL_ERROR, detail="태깅 처리 중 예외가 발생했습니다") from e so the original exception is chained but the response detail omits sensitive str(e) content; reference the existing AppException and ErrorCode.INTERNAL_ERROR symbols and the except handler around the tagging logic.apps/machine/app/errors.py (1)
93-93: 타임스탬프에 타임존 정보 포함 권장
datetime.now().isoformat()은 타임존 정보가 없는 로컬 시간을 반환합니다. 분산 시스템에서 로그 분석 시 혼란을 방지하려면 UTC 또는 타임존이 포함된 시간을 사용하는 것이 좋습니다.♻️ 제안된 수정사항
-from datetime import datetime +from datetime import datetime, timezone # _error_body 내부 - "timestamp": datetime.now().isoformat(), + "timestamp": datetime.now(timezone.utc).isoformat(),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/machine/app/errors.py` at line 93, The "timestamp" entry currently uses datetime.now().isoformat() which lacks timezone info; update the timestamp generation in errors.py to produce an ISO8601 timestamp with timezone (e.g., use datetime.now(timezone.utc).isoformat() or equivalent) and ensure the datetime.timezone (or timezone import) is present so logs carry UTC/tz info; adjust the code that builds the "timestamp" value accordingly (reference the "timestamp" key and the use of datetime.now().isoformat()).
🤖 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 82-83: In AITaggingAsyncService where you call
mediaMoodTagRepository.deleteByMedia_Id(mediaId) followed by
mediaMoodTagRepository.saveAll(newMediaMoodTags), wrap the operation in a
`@Transactional` method and enforce a DB-level lock or optimistic versioning to
prevent concurrent-modify races; e.g., obtain the Media row with
MediaRepository.findById(mediaId) using `@Lock`(LockModeType.PESSIMISTIC_WRITE)
(or add an `@Version` field to the Media/MediaMoodTag entity and handle
OptimisticLockingFailureException) and perform the delete+save inside that
transaction so concurrent requests for the same mediaId are serialized or will
retry on version conflict.
- Around line 86-90: In AITaggingAsyncService update the `@Async` void method that
currently catches BusinessException and rethrows it so that it no longer
rethrows; instead handle BusinessException the same way as the general Exception
branch by logging it with the same log.error call (include mediaId and the
exception), removing the throw e; statement; ensure the catch blocks are
consolidated or both call the same logger to provide consistent logging for any
exception thrown in the async method.
In `@apps/machine/app/errors.py`:
- Around line 158-161: The unhandled_exception_handler currently returns the
exception text via content=_error_body(..., detail=str(exc)) which may expose
internal details; change it to return a generic error detail (e.g., a fixed
message like "Internal server error") while logging the real exception
server-side using the application's logger (call logger.exception or
logger.error with the exception) before returning; update the call in
unhandled_exception_handler to pass the generic detail to _error_body and ensure
the real exc is logged for diagnostics.
In `@apps/machine/app/main.py`:
- Around line 43-50: The CORS middleware is currently configured with
allow_origins=["*"] which is unsafe for production; replace the hardcoded
wildcard in the app.add_middleware(CORSMiddleware, ...) call by loading allowed
origins from configuration or environment (e.g. settings.cors_origins) and pass
that value to allow_origins, keeping
allow_credentials/allow_methods/allow_headers as configured; ensure the settings
value is validated (list of origins) and falls back to a safe default for dev.
In
`@modules/infra-db/src/main/resources/db/migration/V8__seed_mood_tags_and_add_priority.sql`:
- Around line 4-17: The INSERT statement using deprecated VALUES() should be
changed to use a row alias: update the INSERT INTO mood_category (...) VALUES
(...) block to add "AS new" immediately after the VALUES list and replace usages
of VALUES(name)/VALUES(status) in the ON DUPLICATE KEY UPDATE clause with
new.name and new.status, keeping modified_date = NOW() as-is; locate the INSERT
INTO mood_category statement and the ON DUPLICATE KEY UPDATE clause to apply
this change.
---
Nitpick comments:
In
`@apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.java`:
- Around line 53-66: Rename collection variables to follow the `List` suffix
guideline: replace `aiTags` with `aiTagList` (and `missingTags` with
`missingTagList` where used) throughout the AITaggingAsyncService class/method
so all references (calls to
moodTagRepository.findByNameInAndStatus(aiTagList,...), the IntStream.range
mapping, filters using aiTagList.indexOf(...), and creation of newMediaMoodTags
via MediaMoodTag.builder()) are updated consistently; ensure local variables
foundMoodTags, moodTagByName and newMediaMoodTags remain unchanged but consume
the renamed aiTagList variable and update any parameter names/signatures that
pass the collection to other methods.
In `@apps/machine/app/api/tagging.py`:
- Around line 11-24: ErrorItem and ErrorResponse are duplicated here; replace
these local definitions with the shared models from the common errors module:
remove the ErrorItem and ErrorResponse classes in this file and import the
canonical ErrorItem and ErrorResponse from the existing errors.py (preserving
Field descriptions/examples there or augmenting them if needed), then update any
references in functions/route decorators in this module to use the imported
ErrorItem/ErrorResponse symbols so OpenAPI docs remain identical.
- Around line 143-147: Replace the current except block that raises AppException
with a pattern that preserves the original exception chain and avoids exposing
exception text in the API response: log the original exception (including stack
trace) using the module logger inside the except handler, then raise
AppException(ErrorCode.INTERNAL_ERROR, detail="태깅 처리 중 예외가 발생했습니다") from e so
the original exception is chained but the response detail omits sensitive str(e)
content; reference the existing AppException and ErrorCode.INTERNAL_ERROR
symbols and the except handler around the tagging logic.
In `@apps/machine/app/errors.py`:
- Line 93: The "timestamp" entry currently uses datetime.now().isoformat() which
lacks timezone info; update the timestamp generation in errors.py to produce an
ISO8601 timestamp with timezone (e.g., use
datetime.now(timezone.utc).isoformat() or equivalent) and ensure the
datetime.timezone (or timezone import) is present so logs carry UTC/tz info;
adjust the code that builds the "timestamp" value accordingly (reference the
"timestamp" key and the use of datetime.now().isoformat()).
In `@apps/machine/app/main.py`:
- Around line 63-69: The /health endpoint currently returns a static
{"status":"ok"}; update the health() function to check the ML model load state
(use the mood_tagger object from app.services.tagging, e.g., mood_tagger.model)
and return a JSON that includes model_loaded (boolean) and set status to "ok"
when model_loaded is true and "degraded" when false; ensure you import
mood_tagger at top and keep the endpoint signature health() -> dict[str, Any]
(or dict[str, str|bool]) to reflect the added field.
In `@apps/machine/app/services/tagging.py`:
- Around line 27-28: Replace the current exception log call that uses
logger.error in the except Exception as e block with logger.exception so the
stack trace is recorded; locate the except Exception as e handler around the
model-loading logic (the block that now logs "모델 로딩 실패. 경로에 모델 파일이 있는지 확인하세요:
{e}") and change the call to logger.exception with the same descriptive message
to capture the traceback.
In `@apps/machine/requirements.txt`:
- Around line 6-8: requirements.txt에 torch, transformers, numpy가 버전 미지정으로 적혀 있어
빌드 재현성이 깨집니다; requirements.txt에서 각각의 패키지(torch, transformers, numpy)를 명시적 버전(예:
== 또는 ~= 등 재현성을 보장하는 연산자)으로 고정하고, PyTorch-Transformers 상호 호환성을 확인해 호환 가능한
메이저/마이너 조합을 선택하며(또는 주석으로 호환성 근거를 남김) 필요하면 별도의 constraints 파일로 관리하도록 변경하세요.
In
`@modules/domain/src/main/java/com/ott/domain/mood_category/repository/MoodCategoryRepository.java`:
- Around line 3-8: Remove the unused imports in the MoodCategoryRepository
interface: delete the imported symbols Status, List, and Optional from the
import block so only actual dependencies (e.g., MoodCategory and JpaRepository)
remain; update the import section in the MoodCategoryRepository file to reflect
only the types referenced by the interface.
In
`@modules/domain/src/main/java/com/ott/domain/mood_tag/repository/MoodTagRepository.java`:
- Line 11: Rename the collection parameter in the repository method to follow
the "List" suffix convention: update the signature of MoodTagRepository's
findByNameInAndStatus to use a clearer name such as aiTagNameList or nameList
(i.e., change List<String> aiTags to List<String> aiTagNameList) and update all
call sites and usages accordingly so parameter naming remains consistent with
the coding guideline.
In
`@modules/infra-db/src/main/resources/db/migration/V7__add_mood_tag_tables.sql`:
- Around line 41-49: Add a single-column index on media_mood_tag.mood_tag_id to
support queries that filter by mood_tag_id alone; locate the media_mood_tag
table (see existing unique index uq_media_mood_tag_media_id_mood_tag_id and FKs
fk_media_mood_tag_to_media / fk_media_mood_tag_to_mood_tag) and create a
non-unique index like idx_media_mood_tag_mood_tag_id on mood_tag_id to improve
reverse-lookups of all media for a given mood tag.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: ca50f983-3bb8-4f49-b4d2-5f52016eb516
📒 Files selected for processing (18)
.gitignoreapps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.javaapps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AiTaggingAsyncService.javaapps/machine/app/api/tagging.pyapps/machine/app/config.pyapps/machine/app/constants.pyapps/machine/app/errors.pyapps/machine/app/main.pyapps/machine/app/services/tagging.pyapps/machine/requirements.txtmodules/domain/src/main/java/com/ott/domain/media_mood_tag/domain/MediaMoodTag.javamodules/domain/src/main/java/com/ott/domain/media_mood_tag/repository/MediaMoodTagRepository.javamodules/domain/src/main/java/com/ott/domain/mood_category/domain/MoodCategory.javamodules/domain/src/main/java/com/ott/domain/mood_category/repository/MoodCategoryRepository.javamodules/domain/src/main/java/com/ott/domain/mood_tag/domain/MoodTag.javamodules/domain/src/main/java/com/ott/domain/mood_tag/repository/MoodTagRepository.javamodules/infra-db/src/main/resources/db/migration/V7__add_mood_tag_tables.sqlmodules/infra-db/src/main/resources/db/migration/V8__seed_mood_tags_and_add_priority.sql
💤 Files with no reviewable changes (1)
- apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AiTaggingAsyncService.java
| mediaMoodTagRepository.deleteByMedia_Id(mediaId); | ||
| mediaMoodTagRepository.saveAll(newMediaMoodTags); |
There was a problem hiding this comment.
동시성 문제: 동일 mediaId에 대한 동시 호출 시 데이터 불일치 가능성
deleteByMedia_Id와 saveAll 사이에 동일한 mediaId로 다른 요청이 들어오면 데이터 일관성 문제가 발생할 수 있습니다. 동시 업로드가 드물다면 현재 구현도 괜찮지만, 빈번한 경우 비관적 잠금 또는 @Version을 고려하세요.
🤖 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 82 - 83, In AITaggingAsyncService where you call
mediaMoodTagRepository.deleteByMedia_Id(mediaId) followed by
mediaMoodTagRepository.saveAll(newMediaMoodTags), wrap the operation in a
`@Transactional` method and enforce a DB-level lock or optimistic versioning to
prevent concurrent-modify races; e.g., obtain the Media row with
MediaRepository.findById(mediaId) using `@Lock`(LockModeType.PESSIMISTIC_WRITE)
(or add an `@Version` field to the Media/MediaMoodTag entity and handle
OptimisticLockingFailureException) and perform the delete+save inside that
transaction so concurrent requests for the same mediaId are serialized or will
retry on version conflict.
| } catch (BusinessException e) { | ||
| throw e; | ||
| } catch (Exception e) { | ||
| log.error("[AI Tagging] 미디어 ID: {} - 태깅 저장 중 예외 발생", mediaId, e); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# AsyncUncaughtExceptionHandler 설정 여부 확인
rg -n "AsyncUncaughtExceptionHandler|AsyncConfigurer" --type=javaRepository: OpenTheTaste/backend
Length of output: 46
🏁 Script executed:
fd "AITaggingAsyncService.java" -t f --exec cat -n {} \;Repository: OpenTheTaste/backend
Length of output: 4540
🏁 Script executed:
rg -n "@Async" --type=java -A 5 | head -100Repository: OpenTheTaste/backend
Length of output: 867
🏁 Script executed:
rg -n "class AITaggingAsyncService" --type=java -A 50 | head -80Repository: OpenTheTaste/backend
Length of output: 7174
🏁 Script executed:
# Check if there are any `@Transactional` `@Async` combinations
rg -n "@Transactional" --type=java -B 2 | grep -A 2 "@Async"Repository: OpenTheTaste/backend
Length of output: 46
🏁 Script executed:
# Search for AsyncConfigurer or any Spring config related to async
fd ".*Config.*" --type=f -name "*.java" | head -20 | xargs rg -l "async|Async" 2>/dev/nullRepository: OpenTheTaste/backend
Length of output: 428
🏁 Script executed:
# Look for configuration files that might set up async
fd ".*Config\.java" --type=f | xargs rg -l "async|Async|@EnableAsync" 2>/dev/nullRepository: OpenTheTaste/backend
Length of output: 48
🏁 Script executed:
# Check the specific `@Transactional` `@Async` in AITaggingAsyncService
sed -n '36,42p' apps/api-admin/src/main/java/com/ott/api_admin/tagging/service/AITaggingAsyncService.javaRepository: OpenTheTaste/backend
Length of output: 292
🏁 Script executed:
# Verify BusinessException behavior - check how it's defined
rg -n "class BusinessException" --type=java -A 10Repository: OpenTheTaste/backend
Length of output: 1352
@Async void 메서드에서 예외 rethrow는 효과 없음
@Async 메서드의 반환 타입이 void이므로, 메서드 내에서 throw한 BusinessException은 호출자에게 도달하지 않습니다. 비동기 스레드에서 발생한 예외는 AsyncUncaughtExceptionHandler가 설정되지 않은 경우 일관된 처리가 불가능합니다.
현재 코드는 BusinessException을 분리하여 rethrow하지만, 이는 실제로 에러 처리 관점에서 의미가 없으며, 일반 Exception 처리(로깅)와도 불일치합니다. 모든 예외를 일관되게 로깅으로 처리하세요.
제안된 수정사항
- } catch (BusinessException e) {
- throw e;
} catch (Exception e) {
log.error("[AI Tagging] 미디어 ID: {} - 태깅 저장 중 예외 발생", mediaId, e);
}📝 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.
| } catch (BusinessException e) { | |
| throw e; | |
| } catch (Exception e) { | |
| log.error("[AI Tagging] 미디어 ID: {} - 태깅 저장 중 예외 발생", mediaId, e); | |
| } | |
| } catch (Exception e) { | |
| log.error("[AI Tagging] 미디어 ID: {} - 태깅 저장 중 예외 발생", mediaId, e); | |
| } |
🤖 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 86 - 90, In AITaggingAsyncService update the `@Async` void method
that currently catches BusinessException and rethrows it so that it no longer
rethrows; instead handle BusinessException the same way as the general Exception
branch by logging it with the same log.error call (include mediaId and the
exception), removing the throw e; statement; ensure the catch blocks are
consolidated or both call the same logger to provide consistent logging for any
exception thrown in the async method.
| async def unhandled_exception_handler(_: Request, exc: Exception) -> JSONResponse: | ||
| return JSONResponse( | ||
| status_code=ErrorCode.INTERNAL_ERROR.http_status, | ||
| content=_error_body(ErrorCode.INTERNAL_ERROR, detail=str(exc)), |
There was a problem hiding this comment.
처리되지 않은 예외의 상세 정보 노출 주의
str(exc)를 응답에 포함하면 스택 트레이스나 내부 구현 세부사항이 노출될 수 있습니다. 프로덕션에서는 일반적인 에러 메시지만 반환하고, 상세 내용은 서버 로그에만 기록하는 것이 안전합니다.
🛠️ 제안된 수정사항
+import logging
+
+logger = logging.getLogger(__name__)
+
async def unhandled_exception_handler(_: Request, exc: Exception) -> JSONResponse:
+ logger.exception("Unhandled exception occurred")
return JSONResponse(
status_code=ErrorCode.INTERNAL_ERROR.http_status,
- content=_error_body(ErrorCode.INTERNAL_ERROR, detail=str(exc)),
+ content=_error_body(ErrorCode.INTERNAL_ERROR),
)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/machine/app/errors.py` around lines 158 - 161, The
unhandled_exception_handler currently returns the exception text via
content=_error_body(..., detail=str(exc)) which may expose internal details;
change it to return a generic error detail (e.g., a fixed message like "Internal
server error") while logging the real exception server-side using the
application's logger (call logger.exception or logger.error with the exception)
before returning; update the call in unhandled_exception_handler to pass the
generic detail to _error_body and ensure the real exc is logged for diagnostics.
| # CORS | ||
| app.add_middleware( | ||
| CORSMiddleware, | ||
| allow_origins=settings.cors_allow_origins, | ||
| allow_origins=["*"], # 운영에서는 프론트 도메인만 허용 권장 | ||
| allow_credentials=True, | ||
| allow_methods=["*"], | ||
| allow_headers=["*"], | ||
| ) |
There was a problem hiding this comment.
프로덕션 CORS 설정 확인 필요
주석에서 언급한 대로 allow_origins=["*"]는 개발 환경에 적합하지만 프로덕션에서는 보안상 특정 도메인만 허용해야 합니다. 배포 전 환경 변수 또는 설정 파일을 통해 허용 도메인을 관리하는 것을 권장합니다.
🛠️ 환경별 설정 예시
from app.config import settings
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins, # 환경별 설정
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/machine/app/main.py` around lines 43 - 50, The CORS middleware is
currently configured with allow_origins=["*"] which is unsafe for production;
replace the hardcoded wildcard in the app.add_middleware(CORSMiddleware, ...)
call by loading allowed origins from configuration or environment (e.g.
settings.cors_origins) and pass that value to allow_origins, keeping
allow_credentials/allow_methods/allow_headers as configured; ensure the settings
value is validated (list of origins) and falls back to a safe default for dev.
| INSERT INTO mood_category (id, name, created_date, modified_date, status) | ||
| VALUES | ||
| (1, '슬픔', NOW(), NOW(), 'ACTIVE'), | ||
| (2, '공포', NOW(), NOW(), 'ACTIVE'), | ||
| (3, '유쾌', NOW(), NOW(), 'ACTIVE'), | ||
| (4, '힐링', NOW(), NOW(), 'ACTIVE'), | ||
| (5, '설렘', NOW(), NOW(), 'ACTIVE'), | ||
| (6, '지식', NOW(), NOW(), 'ACTIVE'), | ||
| (7, '자극', NOW(), NOW(), 'ACTIVE'), | ||
| (8, '도파민', NOW(), NOW(), 'ACTIVE') | ||
| ON DUPLICATE KEY UPDATE | ||
| name = VALUES(name), | ||
| modified_date = NOW(), | ||
| status = VALUES(status); |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
MySQL 8.0 VALUES() deprecated INSERT ON DUPLICATE KEY UPDATE
💡 Result:
In MySQL 8.0.20, using VALUES(col) inside INSERT ... ON DUPLICATE KEY UPDATE was deprecated (it still works, but raises a deprecation warning and may be removed in a future release). (dev.mysql.com)
Replace VALUES() with a row (and optional column) alias
1) INSERT ... VALUES ... ON DUPLICATE KEY UPDATE
Old (deprecated):
INSERT INTO t1 (a,b,c) VALUES (1,2,3),(4,5,6)
ON DUPLICATE KEY UPDATE c = VALUES(a) + VALUES(b);New (recommended):
INSERT INTO t1 (a,b,c) VALUES (1,2,3),(4,5,6) AS new
ON DUPLICATE KEY UPDATE c = new.a + new.b;You can also alias the inserted columns:
INSERT INTO t1 (a,b,c) VALUES (1,2,3),(4,5,6) AS new(m,n,p)
ON DUPLICATE KEY UPDATE c = m + n;
``` ([dev.mysql.com](https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-20.html))
### 2) `INSERT ... SET ... ON DUPLICATE KEY UPDATE`
```sql
INSERT INTO t1 SET a=1,b=2,c=3 AS new
ON DUPLICATE KEY UPDATE c = new.a + new.b;
``` ([dev.mysql.com](https://dev.mysql.com/doc/refman/8.3/en/insert-on-duplicate.html))
### 3) `INSERT ... SELECT ... ON DUPLICATE KEY UPDATE`
If you used `VALUES()` here, MySQL warns; the manual shows eliminating it by wrapping the `SELECT` as a derived table and referencing its output column:
```sql
INSERT INTO t1
SELECT * FROM (SELECT c, c+d AS e FROM t2) AS dt
ON DUPLICATE KEY UPDATE b = e;
``` ([dev.mysql.com](https://dev.mysql.com/doc/refman/8.3/en/insert-on-duplicate.html))
Citations:
- 1: https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-20.html
- 2: https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-20.html
- 3: https://dev.mysql.com/doc/refman/8.3/en/insert-on-duplicate.html
- 4: https://dev.mysql.com/doc/refman/8.3/en/insert-on-duplicate.html
---
</details>
**`VALUES()` 함수를 새로운 alias 구문으로 변경 필요**
`VALUES()` 구문은 MySQL 8.0.20부터 deprecated되었습니다. 다음과 같이 행 alias를 사용하여 변경하세요:
<details>
<summary>수정된 SQL</summary>
```sql
INSERT INTO mood_category (id, name, created_date, modified_date, status)
VALUES
(1, '슬픔', NOW(), NOW(), 'ACTIVE'),
(2, '공포', NOW(), NOW(), 'ACTIVE'),
(3, '유쾌', NOW(), NOW(), 'ACTIVE'),
(4, '힐링', NOW(), NOW(), 'ACTIVE'),
(5, '설렘', NOW(), NOW(), 'ACTIVE'),
(6, '지식', NOW(), NOW(), 'ACTIVE'),
(7, '자극', NOW(), NOW(), 'ACTIVE'),
(8, '도파민', NOW(), NOW(), 'ACTIVE')
AS new
ON DUPLICATE KEY UPDATE
name = new.name,
modified_date = NOW(),
status = new.status;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@modules/infra-db/src/main/resources/db/migration/V8__seed_mood_tags_and_add_priority.sql`
around lines 4 - 17, The INSERT statement using deprecated VALUES() should be
changed to use a row alias: update the INSERT INTO mood_category (...) VALUES
(...) block to add "AS new" immediately after the VALUES list and replace usages
of VALUES(name)/VALUES(status) in the ON DUPLICATE KEY UPDATE clause with
new.name and new.status, keeping modified_date = NOW() as-is; locate the INSERT
INTO mood_category statement and the ON DUPLICATE KEY UPDATE clause to apply
this change.
📝 작업 내용
📷 스크린샷
☑️ 체크 리스트
#️⃣ 연관된 이슈
close #151
💬 리뷰 요구사항
env 수정된거 있습니다. 노션 참고바랍니다.
Summary by CodeRabbit
릴리스 노트
새로운 기능
개선사항