비동기 적용
parent
95d90dcb50
commit
153b9f0ca4
|
|
@ -25,9 +25,7 @@ Lyric API Router
|
||||||
from app.utils.pagination import PaginatedResponse, get_paginated
|
from app.utils.pagination import PaginatedResponse, get_paginated
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Optional
|
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query, status
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
|
@ -41,6 +39,7 @@ from app.lyric.schemas.lyric import (
|
||||||
LyricListItem,
|
LyricListItem,
|
||||||
LyricStatusResponse,
|
LyricStatusResponse,
|
||||||
)
|
)
|
||||||
|
from app.lyric.worker.lyric_task import generate_lyric_background
|
||||||
from app.utils.chatgpt_prompt import ChatgptService
|
from app.utils.chatgpt_prompt import ChatgptService
|
||||||
from app.utils.pagination import PaginatedResponse, get_paginated
|
from app.utils.pagination import PaginatedResponse, get_paginated
|
||||||
|
|
||||||
|
|
@ -76,7 +75,12 @@ async def get_lyric_status_by_task_id(
|
||||||
# 완료 처리
|
# 완료 처리
|
||||||
"""
|
"""
|
||||||
print(f"[get_lyric_status_by_task_id] START - task_id: {task_id}")
|
print(f"[get_lyric_status_by_task_id] START - task_id: {task_id}")
|
||||||
result = await session.execute(select(Lyric).where(Lyric.task_id == task_id))
|
result = await session.execute(
|
||||||
|
select(Lyric)
|
||||||
|
.where(Lyric.task_id == task_id)
|
||||||
|
.order_by(Lyric.created_at.desc())
|
||||||
|
.limit(1)
|
||||||
|
)
|
||||||
lyric = result.scalar_one_or_none()
|
lyric = result.scalar_one_or_none()
|
||||||
|
|
||||||
if not lyric:
|
if not lyric:
|
||||||
|
|
@ -124,7 +128,12 @@ async def get_lyric_by_task_id(
|
||||||
lyric = await get_lyric_by_task_id(session, task_id)
|
lyric = await get_lyric_by_task_id(session, task_id)
|
||||||
"""
|
"""
|
||||||
print(f"[get_lyric_by_task_id] START - task_id: {task_id}")
|
print(f"[get_lyric_by_task_id] START - task_id: {task_id}")
|
||||||
result = await session.execute(select(Lyric).where(Lyric.task_id == task_id))
|
result = await session.execute(
|
||||||
|
select(Lyric)
|
||||||
|
.where(Lyric.task_id == task_id)
|
||||||
|
.order_by(Lyric.created_at.desc())
|
||||||
|
.limit(1)
|
||||||
|
)
|
||||||
lyric = result.scalar_one_or_none()
|
lyric = result.scalar_one_or_none()
|
||||||
|
|
||||||
if not lyric:
|
if not lyric:
|
||||||
|
|
@ -156,6 +165,7 @@ async def get_lyric_by_task_id(
|
||||||
summary="가사 생성",
|
summary="가사 생성",
|
||||||
description="""
|
description="""
|
||||||
고객 정보를 기반으로 ChatGPT를 이용하여 가사를 생성합니다.
|
고객 정보를 기반으로 ChatGPT를 이용하여 가사를 생성합니다.
|
||||||
|
백그라운드에서 비동기로 처리되며, 즉시 task_id를 반환합니다.
|
||||||
|
|
||||||
## 요청 필드
|
## 요청 필드
|
||||||
- **task_id**: 작업 고유 식별자 (이미지 업로드 시 생성된 task_id, 필수)
|
- **task_id**: 작업 고유 식별자 (이미지 업로드 시 생성된 task_id, 필수)
|
||||||
|
|
@ -165,16 +175,15 @@ async def get_lyric_by_task_id(
|
||||||
- **language**: 가사 출력 언어 (Korean, English, Chinese, Japanese, Thai, Vietnamese)
|
- **language**: 가사 출력 언어 (Korean, English, Chinese, Japanese, Thai, Vietnamese)
|
||||||
|
|
||||||
## 반환 정보
|
## 반환 정보
|
||||||
- **success**: 생성 성공 여부
|
- **success**: 요청 접수 성공 여부
|
||||||
- **task_id**: 작업 고유 식별자
|
- **task_id**: 작업 고유 식별자
|
||||||
- **lyric**: 생성된 가사 (성공 시)
|
- **lyric**: null (백그라운드 처리 중)
|
||||||
- **language**: 가사 언어
|
- **language**: 가사 언어
|
||||||
- **error_message**: 에러 메시지 (실패 시)
|
- **error_message**: 에러 메시지 (요청 접수 실패 시)
|
||||||
|
|
||||||
## 실패 조건
|
## 상태 확인
|
||||||
- ChatGPT API 오류
|
- GET /lyric/status/{task_id} 로 처리 상태 확인
|
||||||
- ChatGPT 거부 응답 (I'm sorry, I cannot 등)
|
- GET /lyric/{task_id} 로 생성된 가사 조회
|
||||||
- 응답에 ERROR: 포함
|
|
||||||
|
|
||||||
## 사용 예시
|
## 사용 예시
|
||||||
```
|
```
|
||||||
|
|
@ -188,43 +197,34 @@ POST /lyric/generate
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 응답 예시 (성공)
|
## 응답 예시
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"success": true,
|
"success": true,
|
||||||
"task_id": "0694b716-dbff-7219-8000-d08cb5fce431",
|
"task_id": "0694b716-dbff-7219-8000-d08cb5fce431",
|
||||||
"lyric": "인스타 감성의 스테이 머뭄...",
|
|
||||||
"language": "Korean",
|
|
||||||
"error_message": null
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 응답 예시 (실패)
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"task_id": "0694b716-dbff-7219-8000-d08cb5fce431",
|
|
||||||
"lyric": null,
|
"lyric": null,
|
||||||
"language": "Korean",
|
"language": "Korean",
|
||||||
"error_message": "I'm sorry, I can't comply with that request."
|
"error_message": null
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
""",
|
""",
|
||||||
response_model=GenerateLyricResponse,
|
response_model=GenerateLyricResponse,
|
||||||
responses={
|
responses={
|
||||||
200: {"description": "가사 생성 성공 또는 실패 (success 필드로 구분)"},
|
200: {"description": "가사 생성 요청 접수 성공"},
|
||||||
500: {"description": "서버 내부 오류"},
|
500: {"description": "서버 내부 오류"},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
async def generate_lyric(
|
async def generate_lyric(
|
||||||
request_body: GenerateLyricRequest,
|
request_body: GenerateLyricRequest,
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
session: AsyncSession = Depends(get_session),
|
session: AsyncSession = Depends(get_session),
|
||||||
) -> GenerateLyricResponse:
|
) -> GenerateLyricResponse:
|
||||||
"""고객 정보를 기반으로 가사를 생성합니다."""
|
"""고객 정보를 기반으로 가사를 생성합니다. (백그라운드 처리)"""
|
||||||
task_id = request_body.task_id
|
task_id = request_body.task_id
|
||||||
print(
|
print(
|
||||||
f"[generate_lyric] START - task_id: {task_id}, "
|
f"[generate_lyric] START - task_id: {task_id}, "
|
||||||
f"customer_name: {request_body.customer_name}, region: {request_body.region}"
|
f"customer_name: {request_body.customer_name}, "
|
||||||
|
f"region: {request_body.region}"
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -247,9 +247,10 @@ async def generate_lyric(
|
||||||
)
|
)
|
||||||
session.add(project)
|
session.add(project)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
await session.refresh(project) # commit 후 project.id 동기화
|
await session.refresh(project)
|
||||||
print(
|
print(
|
||||||
f"[generate_lyric] Project saved - project_id: {project.id}, task_id: {task_id}"
|
f"[generate_lyric] Project saved - "
|
||||||
|
f"project_id: {project.id}, task_id: {task_id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 3. Lyric 테이블에 데이터 저장 (status: processing)
|
# 3. Lyric 테이블에 데이터 저장 (status: processing)
|
||||||
|
|
@ -262,62 +263,31 @@ async def generate_lyric(
|
||||||
language=request_body.language,
|
language=request_body.language,
|
||||||
)
|
)
|
||||||
session.add(lyric)
|
session.add(lyric)
|
||||||
await (
|
|
||||||
session.commit()
|
|
||||||
) # processing 상태를 확실히 저장 (다른 트랜잭션에서 조회 가능)
|
|
||||||
await session.refresh(lyric) # commit 후 객체 상태 동기화
|
|
||||||
print(
|
|
||||||
f"[generate_lyric] Lyric saved (processing) - lyric_id: {lyric.id}, task_id: {task_id}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 4. ChatGPT를 통해 가사 생성
|
|
||||||
print(f"[generate_lyric] ChatGPT generation started - task_id: {task_id}")
|
|
||||||
result = await service.generate(prompt=prompt)
|
|
||||||
print(f"[generate_lyric] ChatGPT generation completed - task_id: {task_id}")
|
|
||||||
|
|
||||||
# 5. 실패 응답 검사 (ERROR 또는 ChatGPT 거부 응답)
|
|
||||||
failure_patterns = [
|
|
||||||
"ERROR:",
|
|
||||||
"I'm sorry",
|
|
||||||
"I cannot",
|
|
||||||
"I can't",
|
|
||||||
"I apologize",
|
|
||||||
"I'm unable",
|
|
||||||
"I am unable",
|
|
||||||
"I'm not able",
|
|
||||||
"I am not able",
|
|
||||||
]
|
|
||||||
is_failure = any(
|
|
||||||
pattern.lower() in result.lower() for pattern in failure_patterns
|
|
||||||
)
|
|
||||||
|
|
||||||
if is_failure:
|
|
||||||
print(f"[generate_lyric] FAILED - task_id: {task_id}, error: {result}")
|
|
||||||
lyric.status = "failed"
|
|
||||||
lyric.lyric_result = result
|
|
||||||
await session.commit()
|
|
||||||
|
|
||||||
return GenerateLyricResponse(
|
|
||||||
success=False,
|
|
||||||
task_id=task_id,
|
|
||||||
lyric=None,
|
|
||||||
language=request_body.language,
|
|
||||||
error_message=result,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 6. 성공 시 Lyric 테이블 업데이트 (status: completed)
|
|
||||||
lyric.status = "completed"
|
|
||||||
lyric.lyric_result = result
|
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
await session.refresh(lyric)
|
||||||
|
print(
|
||||||
|
f"[generate_lyric] Lyric saved (processing) - "
|
||||||
|
f"lyric_id: {lyric.id}, task_id: {task_id}"
|
||||||
|
)
|
||||||
|
|
||||||
print(f"[generate_lyric] SUCCESS - task_id: {task_id}")
|
# 4. 백그라운드 태스크로 ChatGPT 가사 생성 실행
|
||||||
|
background_tasks.add_task(
|
||||||
|
generate_lyric_background,
|
||||||
|
task_id=task_id,
|
||||||
|
prompt=prompt,
|
||||||
|
language=request_body.language,
|
||||||
|
)
|
||||||
|
print(f"[generate_lyric] Background task scheduled - task_id: {task_id}")
|
||||||
|
|
||||||
|
# 5. 즉시 응답 반환
|
||||||
return GenerateLyricResponse(
|
return GenerateLyricResponse(
|
||||||
success=True,
|
success=True,
|
||||||
task_id=task_id,
|
task_id=task_id,
|
||||||
lyric=result,
|
lyric=None,
|
||||||
language=request_body.language,
|
language=request_body.language,
|
||||||
error_message=None,
|
error_message=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[generate_lyric] EXCEPTION - task_id: {task_id}, error: {e}")
|
print(f"[generate_lyric] EXCEPTION - task_id: {task_id}, error: {e}")
|
||||||
await session.rollback()
|
await session.rollback()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
"""
|
||||||
|
Lyric Background Tasks
|
||||||
|
|
||||||
|
가사 생성 관련 백그라운드 태스크를 정의합니다.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
from app.database.session import AsyncSessionLocal
|
||||||
|
from app.lyric.models import Lyric
|
||||||
|
from app.utils.chatgpt_prompt import ChatgptService
|
||||||
|
|
||||||
|
|
||||||
|
async def generate_lyric_background(
|
||||||
|
task_id: str,
|
||||||
|
prompt: str,
|
||||||
|
language: str,
|
||||||
|
) -> None:
|
||||||
|
"""백그라운드에서 ChatGPT를 통해 가사를 생성하고 Lyric 테이블을 업데이트합니다.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
task_id: 프로젝트 task_id
|
||||||
|
prompt: ChatGPT에 전달할 프롬프트
|
||||||
|
language: 가사 언어
|
||||||
|
"""
|
||||||
|
print(f"[generate_lyric_background] START - task_id: {task_id}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# ChatGPT 서비스 초기화 (프롬프트는 이미 생성되어 있음)
|
||||||
|
service = ChatgptService(
|
||||||
|
customer_name="", # 프롬프트가 이미 생성되었으므로 빈 값
|
||||||
|
region="",
|
||||||
|
detail_region_info="",
|
||||||
|
language=language,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ChatGPT를 통해 가사 생성
|
||||||
|
print(f"[generate_lyric_background] ChatGPT generation started - task_id: {task_id}")
|
||||||
|
result = await service.generate(prompt=prompt)
|
||||||
|
print(f"[generate_lyric_background] ChatGPT generation completed - task_id: {task_id}")
|
||||||
|
|
||||||
|
# 실패 응답 검사 (ERROR 또는 ChatGPT 거부 응답)
|
||||||
|
failure_patterns = [
|
||||||
|
"ERROR:",
|
||||||
|
"I'm sorry",
|
||||||
|
"I cannot",
|
||||||
|
"I can't",
|
||||||
|
"I apologize",
|
||||||
|
"I'm unable",
|
||||||
|
"I am unable",
|
||||||
|
"I'm not able",
|
||||||
|
"I am not able",
|
||||||
|
]
|
||||||
|
is_failure = any(
|
||||||
|
pattern.lower() in result.lower() for pattern in failure_patterns
|
||||||
|
)
|
||||||
|
|
||||||
|
# Lyric 테이블 업데이트 (새 세션 사용)
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
query_result = await session.execute(
|
||||||
|
select(Lyric)
|
||||||
|
.where(Lyric.task_id == task_id)
|
||||||
|
.order_by(Lyric.created_at.desc())
|
||||||
|
.limit(1)
|
||||||
|
)
|
||||||
|
lyric = query_result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if lyric:
|
||||||
|
if is_failure:
|
||||||
|
print(f"[generate_lyric_background] FAILED - task_id: {task_id}, error: {result}")
|
||||||
|
lyric.status = "failed"
|
||||||
|
lyric.lyric_result = result
|
||||||
|
else:
|
||||||
|
print(f"[generate_lyric_background] SUCCESS - task_id: {task_id}")
|
||||||
|
lyric.status = "completed"
|
||||||
|
lyric.lyric_result = result
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
else:
|
||||||
|
print(f"[generate_lyric_background] Lyric NOT FOUND in DB - task_id: {task_id}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[generate_lyric_background] EXCEPTION - task_id: {task_id}, error: {e}")
|
||||||
|
# 실패 시 Lyric 테이블 업데이트
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
query_result = await session.execute(
|
||||||
|
select(Lyric)
|
||||||
|
.where(Lyric.task_id == task_id)
|
||||||
|
.order_by(Lyric.created_at.desc())
|
||||||
|
.limit(1)
|
||||||
|
)
|
||||||
|
lyric = query_result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if lyric:
|
||||||
|
lyric.status = "failed"
|
||||||
|
lyric.lyric_result = f"Error: {str(e)}"
|
||||||
|
await session.commit()
|
||||||
|
print(f"[generate_lyric_background] FAILED - task_id: {task_id}, status updated to failed")
|
||||||
|
|
@ -95,9 +95,12 @@ async def generate_song(
|
||||||
"""
|
"""
|
||||||
print(f"[generate_song] START - task_id: {task_id}, genre: {request_body.genre}, language: {request_body.language}")
|
print(f"[generate_song] START - task_id: {task_id}, genre: {request_body.genre}, language: {request_body.language}")
|
||||||
try:
|
try:
|
||||||
# 1. task_id로 Project 조회
|
# 1. task_id로 Project 조회 (중복 시 최신 것 선택)
|
||||||
project_result = await session.execute(
|
project_result = await session.execute(
|
||||||
select(Project).where(Project.task_id == task_id)
|
select(Project)
|
||||||
|
.where(Project.task_id == task_id)
|
||||||
|
.order_by(Project.created_at.desc())
|
||||||
|
.limit(1)
|
||||||
)
|
)
|
||||||
project = project_result.scalar_one_or_none()
|
project = project_result.scalar_one_or_none()
|
||||||
|
|
||||||
|
|
@ -109,9 +112,12 @@ async def generate_song(
|
||||||
)
|
)
|
||||||
print(f"[generate_song] Project found - project_id: {project.id}, task_id: {task_id}")
|
print(f"[generate_song] Project found - project_id: {project.id}, task_id: {task_id}")
|
||||||
|
|
||||||
# 2. task_id로 Lyric 조회
|
# 2. task_id로 Lyric 조회 (중복 시 최신 것 선택)
|
||||||
lyric_result = await session.execute(
|
lyric_result = await session.execute(
|
||||||
select(Lyric).where(Lyric.task_id == task_id)
|
select(Lyric)
|
||||||
|
.where(Lyric.task_id == task_id)
|
||||||
|
.order_by(Lyric.created_at.desc())
|
||||||
|
.limit(1)
|
||||||
)
|
)
|
||||||
lyric = lyric_result.scalar_one_or_none()
|
lyric = lyric_result.scalar_one_or_none()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,14 +13,14 @@ creatomate = CreatomateService()
|
||||||
# 또는 명시적으로 API 키 전달
|
# 또는 명시적으로 API 키 전달
|
||||||
creatomate = CreatomateService(api_key="your_api_key")
|
creatomate = CreatomateService(api_key="your_api_key")
|
||||||
|
|
||||||
# 템플릿 목록 조회
|
# 템플릿 목록 조회 (비동기)
|
||||||
templates = creatomate.get_all_templates_data()
|
templates = await creatomate.get_all_templates_data()
|
||||||
|
|
||||||
# 특정 템플릿 조회
|
# 특정 템플릿 조회 (비동기)
|
||||||
template = creatomate.get_one_template_data(template_id)
|
template = await creatomate.get_one_template_data(template_id)
|
||||||
|
|
||||||
# 영상 렌더링 요청
|
# 영상 렌더링 요청 (비동기)
|
||||||
response = creatomate.make_creatomate_call(template_id, modifications)
|
response = await creatomate.make_creatomate_call(template_id, modifications)
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -37,7 +37,10 @@ OrientationType = Literal["horizontal", "vertical"]
|
||||||
|
|
||||||
|
|
||||||
class CreatomateService:
|
class CreatomateService:
|
||||||
"""Creatomate API를 통한 영상 생성 서비스"""
|
"""Creatomate API를 통한 영상 생성 서비스
|
||||||
|
|
||||||
|
모든 HTTP 호출 메서드는 비동기(async)로 구현되어 있습니다.
|
||||||
|
"""
|
||||||
|
|
||||||
BASE_URL = "https://api.creatomate.com"
|
BASE_URL = "https://api.creatomate.com"
|
||||||
|
|
||||||
|
|
@ -71,37 +74,43 @@ class CreatomateService:
|
||||||
self.orientation = orientation
|
self.orientation = orientation
|
||||||
|
|
||||||
# orientation에 따른 템플릿 설정 가져오기
|
# orientation에 따른 템플릿 설정 가져오기
|
||||||
config = self.TEMPLATE_CONFIG.get(orientation, self.TEMPLATE_CONFIG["vertical"])
|
config = self.TEMPLATE_CONFIG.get(
|
||||||
|
orientation, self.TEMPLATE_CONFIG["vertical"]
|
||||||
|
)
|
||||||
self.template_id = config["template_id"]
|
self.template_id = config["template_id"]
|
||||||
self.target_duration = target_duration if target_duration is not None else config["duration"]
|
self.target_duration = (
|
||||||
|
target_duration if target_duration is not None else config["duration"]
|
||||||
|
)
|
||||||
|
|
||||||
self.headers = {
|
self.headers = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Authorization": f"Bearer {self.api_key}",
|
"Authorization": f"Bearer {self.api_key}",
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_all_templates_data(self) -> dict:
|
async def get_all_templates_data(self) -> dict:
|
||||||
"""모든 템플릿 정보를 조회합니다."""
|
"""모든 템플릿 정보를 조회합니다."""
|
||||||
url = f"{self.BASE_URL}/v1/templates"
|
url = f"{self.BASE_URL}/v1/templates"
|
||||||
response = httpx.get(url, headers=self.headers, timeout=30.0)
|
async with httpx.AsyncClient() as client:
|
||||||
response.raise_for_status()
|
response = await client.get(url, headers=self.headers, timeout=30.0)
|
||||||
return response.json()
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
|
||||||
def get_one_template_data(self, template_id: str) -> dict:
|
async def get_one_template_data(self, template_id: str) -> dict:
|
||||||
"""특정 템플릿 ID로 템플릿 정보를 조회합니다."""
|
"""특정 템플릿 ID로 템플릿 정보를 조회합니다."""
|
||||||
url = f"{self.BASE_URL}/v1/templates/{template_id}"
|
url = f"{self.BASE_URL}/v1/templates/{template_id}"
|
||||||
response = httpx.get(url, headers=self.headers, timeout=30.0)
|
|
||||||
response.raise_for_status()
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
async def get_one_template_data_async(self, template_id: str) -> dict:
|
|
||||||
"""특정 템플릿 ID로 템플릿 정보를 비동기로 조회합니다."""
|
|
||||||
url = f"{self.BASE_URL}/v1/templates/{template_id}"
|
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
response = await client.get(url, headers=self.headers, timeout=30.0)
|
response = await client.get(url, headers=self.headers, timeout=30.0)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
|
# 하위 호환성을 위한 별칭 (deprecated)
|
||||||
|
async def get_one_template_data_async(self, template_id: str) -> dict:
|
||||||
|
"""특정 템플릿 ID로 템플릿 정보를 조회합니다.
|
||||||
|
|
||||||
|
Deprecated: get_one_template_data()를 사용하세요.
|
||||||
|
"""
|
||||||
|
return await self.get_one_template_data(template_id)
|
||||||
|
|
||||||
def parse_template_component_name(self, template_source: list) -> dict:
|
def parse_template_component_name(self, template_source: list) -> dict:
|
||||||
"""템플릿 정보를 파싱하여 리소스 이름을 추출합니다."""
|
"""템플릿 정보를 파싱하여 리소스 이름을 추출합니다."""
|
||||||
|
|
||||||
|
|
@ -129,7 +138,7 @@ class CreatomateService:
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def template_connect_resource_blackbox(
|
async def template_connect_resource_blackbox(
|
||||||
self,
|
self,
|
||||||
template_id: str,
|
template_id: str,
|
||||||
image_url_list: list[str],
|
image_url_list: list[str],
|
||||||
|
|
@ -143,7 +152,7 @@ class CreatomateService:
|
||||||
- 가사는 개행마다 한 텍스트 삽입
|
- 가사는 개행마다 한 텍스트 삽입
|
||||||
- Template에 audio-music 항목이 있어야 함
|
- Template에 audio-music 항목이 있어야 함
|
||||||
"""
|
"""
|
||||||
template_data = self.get_one_template_data(template_id)
|
template_data = await self.get_one_template_data(template_id)
|
||||||
template_component_data = self.parse_template_component_name(
|
template_component_data = self.parse_template_component_name(
|
||||||
template_data["source"]["elements"]
|
template_data["source"]["elements"]
|
||||||
)
|
)
|
||||||
|
|
@ -223,7 +232,9 @@ class CreatomateService:
|
||||||
|
|
||||||
return elements
|
return elements
|
||||||
|
|
||||||
def make_creatomate_call(self, template_id: str, modifications: dict):
|
async def make_creatomate_call(
|
||||||
|
self, template_id: str, modifications: dict
|
||||||
|
) -> dict:
|
||||||
"""Creatomate에 렌더링 요청을 보냅니다.
|
"""Creatomate에 렌더링 요청을 보냅니다.
|
||||||
|
|
||||||
Note:
|
Note:
|
||||||
|
|
@ -234,58 +245,37 @@ class CreatomateService:
|
||||||
"template_id": template_id,
|
"template_id": template_id,
|
||||||
"modifications": modifications,
|
"modifications": modifications,
|
||||||
}
|
}
|
||||||
response = httpx.post(url, json=data, headers=self.headers, timeout=60.0)
|
async with httpx.AsyncClient() as client:
|
||||||
response.raise_for_status()
|
response = await client.post(
|
||||||
return response.json()
|
url, json=data, headers=self.headers, timeout=60.0
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
|
||||||
def make_creatomate_custom_call(self, source: dict):
|
async def make_creatomate_custom_call(self, source: dict) -> dict:
|
||||||
"""템플릿 없이 Creatomate에 커스텀 렌더링 요청을 보냅니다.
|
"""템플릿 없이 Creatomate에 커스텀 렌더링 요청을 보냅니다.
|
||||||
|
|
||||||
Note:
|
|
||||||
response에 요청 정보가 있으니 폴링 필요
|
|
||||||
"""
|
|
||||||
url = f"{self.BASE_URL}/v2/renders"
|
|
||||||
response = httpx.post(url, json=source, headers=self.headers, timeout=60.0)
|
|
||||||
response.raise_for_status()
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
async def make_creatomate_custom_call_async(self, source: dict):
|
|
||||||
"""템플릿 없이 Creatomate에 비동기로 커스텀 렌더링 요청을 보냅니다.
|
|
||||||
|
|
||||||
Note:
|
Note:
|
||||||
response에 요청 정보가 있으니 폴링 필요
|
response에 요청 정보가 있으니 폴링 필요
|
||||||
"""
|
"""
|
||||||
url = f"{self.BASE_URL}/v2/renders"
|
url = f"{self.BASE_URL}/v2/renders"
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
response = await client.post(url, json=source, headers=self.headers, timeout=60.0)
|
response = await client.post(
|
||||||
|
url, json=source, headers=self.headers, timeout=60.0
|
||||||
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
def get_render_status(self, render_id: str) -> dict:
|
# 하위 호환성을 위한 별칭 (deprecated)
|
||||||
"""렌더링 작업의 상태를 조회합니다.
|
async def make_creatomate_custom_call_async(self, source: dict) -> dict:
|
||||||
|
"""템플릿 없이 Creatomate에 커스텀 렌더링 요청을 보냅니다.
|
||||||
|
|
||||||
Args:
|
Deprecated: make_creatomate_custom_call()을 사용하세요.
|
||||||
render_id: Creatomate 렌더 ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
렌더링 상태 정보
|
|
||||||
|
|
||||||
Note:
|
|
||||||
상태 값:
|
|
||||||
- planned: 예약됨
|
|
||||||
- waiting: 대기 중
|
|
||||||
- transcribing: 트랜스크립션 중
|
|
||||||
- rendering: 렌더링 중
|
|
||||||
- succeeded: 성공
|
|
||||||
- failed: 실패
|
|
||||||
"""
|
"""
|
||||||
url = f"{self.BASE_URL}/v1/renders/{render_id}"
|
return await self.make_creatomate_custom_call(source)
|
||||||
response = httpx.get(url, headers=self.headers, timeout=30.0)
|
|
||||||
response.raise_for_status()
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
async def get_render_status_async(self, render_id: str) -> dict:
|
async def get_render_status(self, render_id: str) -> dict:
|
||||||
"""렌더링 작업의 상태를 비동기로 조회합니다.
|
"""렌더링 작업의 상태를 조회합니다.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
render_id: Creatomate 렌더 ID
|
render_id: Creatomate 렌더 ID
|
||||||
|
|
@ -308,6 +298,14 @@ class CreatomateService:
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
|
# 하위 호환성을 위한 별칭 (deprecated)
|
||||||
|
async def get_render_status_async(self, render_id: str) -> dict:
|
||||||
|
"""렌더링 작업의 상태를 조회합니다.
|
||||||
|
|
||||||
|
Deprecated: get_render_status()를 사용하세요.
|
||||||
|
"""
|
||||||
|
return await self.get_render_status(render_id)
|
||||||
|
|
||||||
def calc_scene_duration(self, template: dict) -> float:
|
def calc_scene_duration(self, template: dict) -> float:
|
||||||
"""템플릿의 전체 장면 duration을 계산합니다."""
|
"""템플릿의 전체 장면 duration을 계산합니다."""
|
||||||
total_template_duration = 0.0
|
total_template_duration = 0.0
|
||||||
|
|
|
||||||
|
|
@ -107,9 +107,12 @@ async def generate_video(
|
||||||
"""
|
"""
|
||||||
print(f"[generate_video] START - task_id: {task_id}, orientation: {orientation}")
|
print(f"[generate_video] START - task_id: {task_id}, orientation: {orientation}")
|
||||||
try:
|
try:
|
||||||
# 1. task_id로 Project 조회
|
# 1. task_id로 Project 조회 (중복 시 최신 것 선택)
|
||||||
project_result = await session.execute(
|
project_result = await session.execute(
|
||||||
select(Project).where(Project.task_id == task_id)
|
select(Project)
|
||||||
|
.where(Project.task_id == task_id)
|
||||||
|
.order_by(Project.created_at.desc())
|
||||||
|
.limit(1)
|
||||||
)
|
)
|
||||||
project = project_result.scalar_one_or_none()
|
project = project_result.scalar_one_or_none()
|
||||||
|
|
||||||
|
|
@ -121,9 +124,12 @@ async def generate_video(
|
||||||
)
|
)
|
||||||
print(f"[generate_video] Project found - project_id: {project.id}, task_id: {task_id}")
|
print(f"[generate_video] Project found - project_id: {project.id}, task_id: {task_id}")
|
||||||
|
|
||||||
# 2. task_id로 Lyric 조회
|
# 2. task_id로 Lyric 조회 (중복 시 최신 것 선택)
|
||||||
lyric_result = await session.execute(
|
lyric_result = await session.execute(
|
||||||
select(Lyric).where(Lyric.task_id == task_id)
|
select(Lyric)
|
||||||
|
.where(Lyric.task_id == task_id)
|
||||||
|
.order_by(Lyric.created_at.desc())
|
||||||
|
.limit(1)
|
||||||
)
|
)
|
||||||
lyric = lyric_result.scalar_one_or_none()
|
lyric = lyric_result.scalar_one_or_none()
|
||||||
|
|
||||||
|
|
@ -593,14 +599,19 @@ async def get_videos(
|
||||||
result = await session.execute(query)
|
result = await session.execute(query)
|
||||||
videos = result.scalars().all()
|
videos = result.scalars().all()
|
||||||
|
|
||||||
# Project 정보와 함께 VideoListItem으로 변환
|
# Project 정보 일괄 조회 (N+1 문제 해결)
|
||||||
|
project_ids = [v.project_id for v in videos if v.project_id]
|
||||||
|
projects_map: dict = {}
|
||||||
|
if project_ids:
|
||||||
|
projects_result = await session.execute(
|
||||||
|
select(Project).where(Project.id.in_(project_ids))
|
||||||
|
)
|
||||||
|
projects_map = {p.id: p for p in projects_result.scalars().all()}
|
||||||
|
|
||||||
|
# VideoListItem으로 변환
|
||||||
items = []
|
items = []
|
||||||
for video in videos:
|
for video in videos:
|
||||||
# Project 조회 (video.project_id 직접 사용)
|
project = projects_map.get(video.project_id)
|
||||||
project_result = await session.execute(
|
|
||||||
select(Project).where(Project.id == video.project_id)
|
|
||||||
)
|
|
||||||
project = project_result.scalar_one_or_none()
|
|
||||||
|
|
||||||
item = VideoListItem(
|
item = VideoListItem(
|
||||||
store_name=project.store_name if project else None,
|
store_name=project.store_name if project else None,
|
||||||
|
|
@ -611,13 +622,6 @@ async def get_videos(
|
||||||
)
|
)
|
||||||
items.append(item)
|
items.append(item)
|
||||||
|
|
||||||
# 개별 아이템 로그
|
|
||||||
print(
|
|
||||||
f"[get_videos] Item - store_name: {item.store_name}, region: {item.region}, "
|
|
||||||
f"task_id: {item.task_id}, result_movie_url: {item.result_movie_url}, "
|
|
||||||
f"created_at: {item.created_at}"
|
|
||||||
)
|
|
||||||
|
|
||||||
response = PaginatedResponse.create(
|
response = PaginatedResponse.create(
|
||||||
items=items,
|
items=items,
|
||||||
total=total,
|
total=total,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,297 @@
|
||||||
|
# 비동기 처리 문제 분석 보고서
|
||||||
|
|
||||||
|
## 요약
|
||||||
|
|
||||||
|
전반적으로 이 프로젝트는 현대적인 비동기 아키텍처를 잘 구현하고 있습니다. 그러나 몇 가지 잠재적인 문제점과 개선 가능한 부분이 발견되었습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 심각도 높음 - 즉시 개선 권장
|
||||||
|
|
||||||
|
### 1.1 N+1 쿼리 문제 (video.py:596-612)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# get_videos() 엔드포인트에서
|
||||||
|
for video in videos:
|
||||||
|
# 매 video마다 별도의 DB 쿼리 실행 - N+1 문제!
|
||||||
|
project_result = await session.execute(
|
||||||
|
select(Project).where(Project.id == video.project_id)
|
||||||
|
)
|
||||||
|
project = project_result.scalar_one_or_none()
|
||||||
|
```
|
||||||
|
|
||||||
|
**문제점**: 비디오 목록 조회 시 각 비디오마다 별도의 Project 쿼리가 발생합니다. 10개 비디오 조회 시 11번의 DB 쿼리가 실행됩니다.
|
||||||
|
|
||||||
|
**개선 방안**:
|
||||||
|
```python
|
||||||
|
# selectinload를 사용한 eager loading
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
|
query = (
|
||||||
|
select(Video)
|
||||||
|
.options(selectinload(Video.project)) # relationship 필요
|
||||||
|
.where(Video.id.in_(select(subquery.c.max_id)))
|
||||||
|
.order_by(Video.created_at.desc())
|
||||||
|
.offset(offset)
|
||||||
|
.limit(pagination.page_size)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 또는 한 번에 project_ids 수집 후 일괄 조회
|
||||||
|
project_ids = [v.project_id for v in videos]
|
||||||
|
projects_result = await session.execute(
|
||||||
|
select(Project).where(Project.id.in_(project_ids))
|
||||||
|
)
|
||||||
|
projects_map = {p.id: p for p in projects_result.scalars().all()}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.2 가사 생성 API의 블로킹 문제 (lyric.py:274-276)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ChatGPT API 호출이 완료될 때까지 HTTP 응답이 블로킹됨
|
||||||
|
print(f"[generate_lyric] ChatGPT generation started - task_id: {task_id}")
|
||||||
|
result = await service.generate(prompt=prompt) # 수 초~수십 초 소요
|
||||||
|
print(f"[generate_lyric] ChatGPT generation completed - task_id: {task_id}")
|
||||||
|
```
|
||||||
|
|
||||||
|
**문제점**:
|
||||||
|
- ChatGPT API 응답이 5-30초 이상 걸릴 수 있음
|
||||||
|
- 이 시간 동안 클라이언트 연결이 유지되어야 함
|
||||||
|
- 다수 동시 요청 시 worker 스레드 고갈 가능성
|
||||||
|
|
||||||
|
**개선 방안 (BackgroundTask 패턴)**:
|
||||||
|
```python
|
||||||
|
@router.post("/generate")
|
||||||
|
async def generate_lyric(
|
||||||
|
request_body: GenerateLyricRequest,
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
|
session: AsyncSession = Depends(get_session),
|
||||||
|
) -> GenerateLyricResponse:
|
||||||
|
# DB에 processing 상태로 저장
|
||||||
|
lyric = Lyric(status="processing", ...)
|
||||||
|
session.add(lyric)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
# 백그라운드에서 ChatGPT 호출
|
||||||
|
background_tasks.add_task(
|
||||||
|
generate_lyric_background,
|
||||||
|
task_id=task_id,
|
||||||
|
prompt=prompt,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 즉시 응답 반환
|
||||||
|
return GenerateLyricResponse(
|
||||||
|
success=True,
|
||||||
|
task_id=task_id,
|
||||||
|
message="가사 생성이 시작되었습니다. /status/{task_id}로 상태를 확인하세요.",
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.3 Creatomate 서비스의 동기/비동기 메서드 혼재 (creatomate.py)
|
||||||
|
|
||||||
|
**문제점**: 동기 메서드가 여전히 존재하여 실수로 async 컨텍스트에서 호출될 수 있습니다.
|
||||||
|
|
||||||
|
| 동기 메서드 | 비동기 메서드 |
|
||||||
|
|------------|--------------|
|
||||||
|
| `get_all_templates_data()` | 없음 |
|
||||||
|
| `get_one_template_data()` | `get_one_template_data_async()` |
|
||||||
|
| `make_creatomate_call()` | 없음 |
|
||||||
|
| `make_creatomate_custom_call()` | `make_creatomate_custom_call_async()` |
|
||||||
|
| `get_render_status()` | `get_render_status_async()` |
|
||||||
|
|
||||||
|
**개선 방안**:
|
||||||
|
```python
|
||||||
|
# 모든 HTTP 호출 메서드를 async로 통일
|
||||||
|
async def get_all_templates_data(self) -> dict:
|
||||||
|
url = f"{self.BASE_URL}/v1/templates"
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.get(url, headers=self.headers, timeout=30.0)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
# 동기 버전 제거 또는 deprecated 표시
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 심각도 중간 - 개선 권장
|
||||||
|
|
||||||
|
### 2.1 백그라운드 태스크에서 매번 엔진 생성 (session.py:82-127)
|
||||||
|
|
||||||
|
```python
|
||||||
|
@asynccontextmanager
|
||||||
|
async def get_worker_session() -> AsyncGenerator[AsyncSession, None]:
|
||||||
|
# 매 호출마다 새 엔진 생성 - 오버헤드 발생
|
||||||
|
worker_engine = create_async_engine(
|
||||||
|
url=db_settings.MYSQL_URL,
|
||||||
|
poolclass=NullPool,
|
||||||
|
...
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**문제점**: 백그라운드 태스크가 빈번하게 호출되면 엔진 생성/소멸 오버헤드가 증가합니다.
|
||||||
|
|
||||||
|
**개선 방안**:
|
||||||
|
```python
|
||||||
|
# 모듈 레벨에서 워커 전용 엔진 생성
|
||||||
|
_worker_engine = create_async_engine(
|
||||||
|
url=db_settings.MYSQL_URL,
|
||||||
|
poolclass=NullPool,
|
||||||
|
)
|
||||||
|
_WorkerSessionLocal = async_sessionmaker(bind=_worker_engine, ...)
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def get_worker_session() -> AsyncGenerator[AsyncSession, None]:
|
||||||
|
async with _WorkerSessionLocal() as session:
|
||||||
|
try:
|
||||||
|
yield session
|
||||||
|
except Exception as e:
|
||||||
|
await session.rollback()
|
||||||
|
raise e
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.2 대용량 파일 다운로드 시 메모리 사용 (video_task.py:49-54)
|
||||||
|
|
||||||
|
```python
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.get(video_url, timeout=180.0)
|
||||||
|
response.raise_for_status()
|
||||||
|
# 전체 파일을 메모리에 로드 - 대용량 영상 시 문제
|
||||||
|
async with aiofiles.open(str(temp_file_path), "wb") as f:
|
||||||
|
await f.write(response.content)
|
||||||
|
```
|
||||||
|
|
||||||
|
**문제점**: 수백 MB 크기의 영상 파일을 한 번에 메모리에 로드합니다.
|
||||||
|
|
||||||
|
**개선 방안 - 스트리밍 다운로드**:
|
||||||
|
```python
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
async with client.stream("GET", video_url, timeout=180.0) as response:
|
||||||
|
response.raise_for_status()
|
||||||
|
async with aiofiles.open(str(temp_file_path), "wb") as f:
|
||||||
|
async for chunk in response.aiter_bytes(chunk_size=8192):
|
||||||
|
await f.write(chunk)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.3 httpx.AsyncClient 반복 생성
|
||||||
|
|
||||||
|
여러 곳에서 `async with httpx.AsyncClient() as client:`를 사용하여 매번 새 클라이언트를 생성합니다.
|
||||||
|
|
||||||
|
**개선 방안 - 재사용 가능한 클라이언트**:
|
||||||
|
```python
|
||||||
|
# app/utils/http_client.py
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
_client: httpx.AsyncClient | None = None
|
||||||
|
|
||||||
|
async def get_http_client() -> httpx.AsyncClient:
|
||||||
|
global _client
|
||||||
|
if _client is None:
|
||||||
|
_client = httpx.AsyncClient(timeout=30.0)
|
||||||
|
return _client
|
||||||
|
|
||||||
|
async def close_http_client():
|
||||||
|
global _client
|
||||||
|
if _client:
|
||||||
|
await _client.aclose()
|
||||||
|
_client = None
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 심각도 낮음 - 선택적 개선
|
||||||
|
|
||||||
|
### 3.1 generate_video 엔드포인트의 다중 DB 조회 (video.py:109-191)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 4개의 개별 쿼리가 순차적으로 실행됨
|
||||||
|
project_result = await session.execute(select(Project).where(...))
|
||||||
|
lyric_result = await session.execute(select(Lyric).where(...))
|
||||||
|
song_result = await session.execute(select(Song).where(...))
|
||||||
|
image_result = await session.execute(select(Image).where(...))
|
||||||
|
```
|
||||||
|
|
||||||
|
**개선 방안 - 병렬 쿼리 실행**:
|
||||||
|
```python
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
project_task = session.execute(select(Project).where(Project.task_id == task_id))
|
||||||
|
lyric_task = session.execute(select(Lyric).where(Lyric.task_id == task_id))
|
||||||
|
song_task = session.execute(
|
||||||
|
select(Song).where(Song.task_id == task_id).order_by(Song.created_at.desc()).limit(1)
|
||||||
|
)
|
||||||
|
image_task = session.execute(
|
||||||
|
select(Image).where(Image.task_id == task_id).order_by(Image.img_order.asc())
|
||||||
|
)
|
||||||
|
|
||||||
|
project_result, lyric_result, song_result, image_result = await asyncio.gather(
|
||||||
|
project_task, lyric_task, song_task, image_task
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.2 템플릿 조회 캐싱 미적용
|
||||||
|
|
||||||
|
`get_one_template_data_async()`가 매번 Creatomate API를 호출합니다.
|
||||||
|
|
||||||
|
**개선 방안 - 간단한 메모리 캐싱**:
|
||||||
|
```python
|
||||||
|
from functools import lru_cache
|
||||||
|
from cachetools import TTLCache
|
||||||
|
|
||||||
|
_template_cache = TTLCache(maxsize=100, ttl=3600) # 1시간 캐시
|
||||||
|
|
||||||
|
async def get_one_template_data_async(self, template_id: str) -> dict:
|
||||||
|
if template_id in _template_cache:
|
||||||
|
return _template_cache[template_id]
|
||||||
|
|
||||||
|
url = f"{self.BASE_URL}/v1/templates/{template_id}"
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.get(url, headers=self.headers, timeout=30.0)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
_template_cache[template_id] = data
|
||||||
|
return data
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 긍정적인 부분 (잘 구현된 패턴)
|
||||||
|
|
||||||
|
| 항목 | 상태 | 설명 |
|
||||||
|
|------|------|------|
|
||||||
|
| SQLAlchemy AsyncSession | O | `asyncmy` 드라이버와 `AsyncSessionLocal` 사용 |
|
||||||
|
| 파일 I/O | O | `aiofiles` 사용으로 비동기 파일 처리 |
|
||||||
|
| HTTP 클라이언트 | O | `httpx.AsyncClient` 사용 |
|
||||||
|
| OpenAI API | O | `AsyncOpenAI` 클라이언트 사용 |
|
||||||
|
| 백그라운드 태스크 | O | FastAPI `BackgroundTasks` 적절히 사용 |
|
||||||
|
| 세션 관리 | O | 메인/워커 세션 분리로 이벤트 루프 충돌 방지 |
|
||||||
|
| 연결 풀 설정 | O | `pool_size`, `pool_recycle`, `pool_pre_ping` 적절히 설정 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 우선순위별 개선 권장 사항
|
||||||
|
|
||||||
|
| 우선순위 | 항목 | 예상 효과 |
|
||||||
|
|----------|------|----------|
|
||||||
|
| **1** | N+1 쿼리 문제 해결 | DB 부하 감소, 응답 속도 개선 |
|
||||||
|
| **2** | 가사 생성 백그라운드 처리 | 동시 요청 처리 능력 향상 |
|
||||||
|
| **3** | Creatomate 동기 메서드 제거 | 실수로 인한 블로킹 방지 |
|
||||||
|
| **4** | 대용량 파일 스트리밍 다운로드 | 메모리 사용량 감소 |
|
||||||
|
| **5** | 워커 세션 엔진 재사용 | 오버헤드 감소 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 분석 일자
|
||||||
|
|
||||||
|
2024-12-29
|
||||||
Loading…
Reference in New Issue