# 비동기 처리 문제 분석 보고서

## 요약

전반적으로 이 프로젝트는 현대적인 비동기 아키텍처를 잘 구현하고 있습니다. 그러나 몇 가지 잠재적인 문제점과 개선 가능한 부분이 발견되었습니다.

---

## 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
