298 lines
9.2 KiB
Markdown
298 lines
9.2 KiB
Markdown
# 비동기 처리 문제 분석 보고서
|
|
|
|
## 요약
|
|
|
|
전반적으로 이 프로젝트는 현대적인 비동기 아키텍처를 잘 구현하고 있습니다. 그러나 몇 가지 잠재적인 문제점과 개선 가능한 부분이 발견되었습니다.
|
|
|
|
---
|
|
|
|
## 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
|