# ORM 동기식 전환 보고서 ## 개요 현재 프로젝트는 **SQLAlchemy 2.0+ 비동기 방식**으로 구현되어 있습니다. 이 보고서는 동기식으로 전환할 경우 필요한 코드 수정 사항을 정리합니다. --- ## 1. 현재 비동기 구현 현황 ### 1.1 사용 중인 라이브러리 - `sqlalchemy[asyncio]>=2.0.45` - `asyncmy>=0.2.10` (MySQL 비동기 드라이버) - `aiomysql>=0.3.2` ### 1.2 주요 비동기 컴포넌트 | 컴포넌트 | 현재 (비동기) | 변경 후 (동기) | |---------|-------------|--------------| | 엔진 | `create_async_engine` | `create_engine` | | 세션 팩토리 | `async_sessionmaker` | `sessionmaker` | | 세션 클래스 | `AsyncSession` | `Session` | | DB 드라이버 | `mysql+asyncmy` | `mysql+pymysql` | --- ## 2. 파일별 수정 사항 ### 2.1 pyproject.toml - 의존성 변경 **파일**: `pyproject.toml` ```diff dependencies = [ "fastapi[standard]>=0.115.8", - "sqlalchemy[asyncio]>=2.0.45", + "sqlalchemy>=2.0.45", - "asyncmy>=0.2.10", - "aiomysql>=0.3.2", + "pymysql>=1.1.0", ... ] ``` --- ### 2.2 config.py - 데이터베이스 URL 변경 **파일**: `config.py` (라인 74-96) ```diff class DatabaseSettings(BaseSettings): @property def MYSQL_URL(self) -> str: - return f"mysql+asyncmy://{self.MYSQL_USER}:{self.MYSQL_PASSWORD}@{self.MYSQL_HOST}:{self.MYSQL_PORT}/{self.MYSQL_DB}" + return f"mysql+pymysql://{self.MYSQL_USER}:{self.MYSQL_PASSWORD}@{self.MYSQL_HOST}:{self.MYSQL_PORT}/{self.MYSQL_DB}" ``` --- ### 2.3 app/database/session.py - 세션 설정 전면 수정 **파일**: `app/database/session.py` #### 현재 코드 (비동기) ```python from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from typing import AsyncGenerator engine = create_async_engine( url=db_settings.MYSQL_URL, echo=False, pool_size=10, max_overflow=10, pool_timeout=5, pool_recycle=3600, pool_pre_ping=True, pool_reset_on_return="rollback", ) AsyncSessionLocal = async_sessionmaker( bind=engine, class_=AsyncSession, expire_on_commit=False, autoflush=False, ) async def get_session() -> AsyncGenerator[AsyncSession, None]: async with AsyncSessionLocal() as session: try: yield session except Exception as e: await session.rollback() raise e ``` #### 변경 후 코드 (동기) ```python from sqlalchemy import create_engine from sqlalchemy.orm import Session, sessionmaker from typing import Generator engine = create_engine( url=db_settings.MYSQL_URL, echo=False, pool_size=10, max_overflow=10, pool_timeout=5, pool_recycle=3600, pool_pre_ping=True, pool_reset_on_return="rollback", ) SessionLocal = sessionmaker( bind=engine, class_=Session, expire_on_commit=False, autoflush=False, ) def get_session() -> Generator[Session, None, None]: with SessionLocal() as session: try: yield session except Exception as e: session.rollback() raise e ``` #### get_worker_session 함수 변경 ```diff - from contextlib import asynccontextmanager + from contextlib import contextmanager - @asynccontextmanager - async def get_worker_session() -> AsyncGenerator[AsyncSession, None]: - worker_engine = create_async_engine( + @contextmanager + def get_worker_session() -> Generator[Session, None, None]: + worker_engine = create_engine( url=db_settings.MYSQL_URL, poolclass=NullPool, ) - session_factory = async_sessionmaker( - bind=worker_engine, - class_=AsyncSession, + session_factory = sessionmaker( + bind=worker_engine, + class_=Session, expire_on_commit=False, autoflush=False, ) - async with session_factory() as session: + with session_factory() as session: try: yield session finally: - await session.close() - await worker_engine.dispose() + session.close() + worker_engine.dispose() ``` --- ### 2.4 app/*/dependencies.py - 타입 힌트 변경 **파일**: `app/song/dependencies.py`, `app/lyric/dependencies.py`, `app/video/dependencies.py` ```diff - from sqlalchemy.ext.asyncio import AsyncSession + from sqlalchemy.orm import Session - SessionDep = Annotated[AsyncSession, Depends(get_session)] + SessionDep = Annotated[Session, Depends(get_session)] ``` --- ### 2.5 라우터 파일들 - async/await 제거 **영향받는 파일**: - `app/home/api/routers/v1/home.py` - `app/lyric/api/routers/v1/lyric.py` - `app/song/api/routers/v1/song.py` - `app/video/api/routers/v1/video.py` #### 예시: lyric.py (라인 70-90) ```diff - async def get_lyric_by_task_id( + def get_lyric_by_task_id( task_id: str, - session: AsyncSession = Depends(get_session), + session: Session = Depends(get_session), ): - result = await session.execute(select(Lyric).where(Lyric.task_id == task_id)) + result = session.execute(select(Lyric).where(Lyric.task_id == task_id)) lyric = result.scalar_one_or_none() ... ``` #### 예시: CRUD 작업 (라인 218-260) ```diff - async def create_project( + def create_project( request_body: ProjectCreateRequest, - session: AsyncSession = Depends(get_session), + session: Session = Depends(get_session), ): project = Project( store_name=request_body.customer_name, region=request_body.region, task_id=task_id, ) session.add(project) - await session.commit() - await session.refresh(project) + session.commit() + session.refresh(project) return project ``` #### 예시: 플러시 작업 (home.py 라인 340-350) ```diff session.add(image) - await session.flush() + session.flush() result = image.id ``` --- ### 2.6 서비스 파일들 - Raw SQL 쿼리 변경 **영향받는 파일**: - `app/lyric/services/lyrics.py` - `app/song/services/song.py` - `app/video/services/video.py` #### 예시: lyrics.py (라인 20-30) ```diff - async def get_store_default_info(conn: AsyncConnection): + def get_store_default_info(conn: Connection): query = """SELECT * FROM store_default_info;""" - result = await conn.execute(text(query)) + result = conn.execute(text(query)) return result.fetchall() ``` #### 예시: INSERT 쿼리 (라인 360-400) ```diff - async def insert_song_result(conn: AsyncConnection, params: dict): + def insert_song_result(conn: Connection, params: dict): insert_query = """INSERT INTO song_results_all (...) VALUES (...)""" - await conn.execute(text(insert_query), params) - await conn.commit() + conn.execute(text(insert_query), params) + conn.commit() ``` --- ### 2.7 app/home/services/base.py - BaseService 클래스 **파일**: `app/home/services/base.py` ```diff - from sqlalchemy.ext.asyncio import AsyncSession + from sqlalchemy.orm import Session class BaseService: - def __init__(self, model, session: AsyncSession): + def __init__(self, model, session: Session): self.model = model self.session = session - async def _get(self, id: UUID): - return await self.session.get(self.model, id) + def _get(self, id: UUID): + return self.session.get(self.model, id) - async def _add(self, entity): + def _add(self, entity): self.session.add(entity) - await self.session.commit() - await self.session.refresh(entity) + self.session.commit() + self.session.refresh(entity) return entity - async def _update(self, entity): - return await self._add(entity) + def _update(self, entity): + return self._add(entity) - async def _delete(self, entity): - await self.session.delete(entity) + def _delete(self, entity): + self.session.delete(entity) ``` --- ## 3. 모델 파일 - 변경 불필요 다음 모델 파일들은 **변경이 필요 없습니다**: - `app/home/models.py` - `app/lyric/models.py` - `app/song/models.py` - `app/video/models.py` 모델 정의 자체는 비동기/동기와 무관하게 동일합니다. `Mapped`, `mapped_column`, `relationship` 등은 그대로 사용 가능합니다. 단, **관계 로딩 전략**에서 `lazy="selectin"` 설정은 동기 환경에서도 작동하지만, 필요에 따라 `lazy="joined"` 또는 `lazy="subquery"`로 변경할 수 있습니다. --- ## 4. 수정 패턴 요약 ### 4.1 Import 변경 패턴 ```diff # 엔진/세션 관련 - from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine + from sqlalchemy import create_engine + from sqlalchemy.orm import Session, sessionmaker # 타입 힌트 - from typing import AsyncGenerator + from typing import Generator # 컨텍스트 매니저 - from contextlib import asynccontextmanager + from contextlib import contextmanager ``` ### 4.2 함수 정의 변경 패턴 ```diff - async def function_name(...): + def function_name(...): ``` ### 4.3 await 제거 패턴 ```diff - result = await session.execute(query) + result = session.execute(query) - await session.commit() + session.commit() - await session.refresh(obj) + session.refresh(obj) - await session.flush() + session.flush() - await session.rollback() + session.rollback() - await session.close() + session.close() - await engine.dispose() + engine.dispose() ``` ### 4.4 컨텍스트 매니저 변경 패턴 ```diff - async with SessionLocal() as session: + with SessionLocal() as session: ``` --- ## 5. 영향받는 파일 목록 ### 5.1 반드시 수정해야 하는 파일 | 파일 | 수정 범위 | |-----|---------| | `pyproject.toml` | 의존성 변경 | | `config.py` | DB URL 변경 | | `app/database/session.py` | 전면 수정 | | `app/database/session-prod.py` | 전면 수정 | | `app/home/api/routers/v1/home.py` | async/await 제거 | | `app/lyric/api/routers/v1/lyric.py` | async/await 제거 | | `app/song/api/routers/v1/song.py` | async/await 제거 | | `app/video/api/routers/v1/video.py` | async/await 제거 | | `app/lyric/services/lyrics.py` | async/await 제거 | | `app/song/services/song.py` | async/await 제거 | | `app/video/services/video.py` | async/await 제거 | | `app/home/services/base.py` | async/await 제거 | | `app/song/dependencies.py` | 타입 힌트 변경 | | `app/lyric/dependencies.py` | 타입 힌트 변경 | | `app/video/dependencies.py` | 타입 힌트 변경 | | `app/dependencies/database.py` | 타입 힌트 변경 | ### 5.2 수정 불필요한 파일 | 파일 | 이유 | |-----|-----| | `app/home/models.py` | 모델 정의는 동기/비동기 무관 | | `app/lyric/models.py` | 모델 정의는 동기/비동기 무관 | | `app/song/models.py` | 모델 정의는 동기/비동기 무관 | | `app/video/models.py` | 모델 정의는 동기/비동기 무관 | --- ## 6. 주의사항 ### 6.1 FastAPI와의 호환성 FastAPI는 동기 엔드포인트도 지원합니다. 동기 함수는 스레드 풀에서 실행됩니다: ```python # 동기 엔드포인트 - FastAPI가 자동으로 스레드풀에서 실행 @router.get("/items/{item_id}") def get_item(item_id: int, session: Session = Depends(get_session)): return session.get(Item, item_id) ``` ### 6.2 성능 고려사항 동기식으로 전환 시 고려할 점: - **동시성 감소**: 비동기 I/O의 이점 상실 - **스레드 풀 의존**: 동시 요청이 많을 경우 스레드 풀 크기 조정 필요 - **블로킹 I/O**: DB 쿼리 중 다른 요청 처리 불가 ### 6.3 백그라운드 작업 현재 `get_worker_session()`으로 별도 이벤트 루프에서 실행되는 백그라운드 작업이 있습니다. 동기식 전환 시 스레드 기반 백그라운드 작업으로 변경해야 합니다: ```python from concurrent.futures import ThreadPoolExecutor executor = ThreadPoolExecutor(max_workers=4) def background_task(): with get_worker_session() as session: # 작업 수행 pass # 실행 executor.submit(background_task) ``` --- ## 7. 마이그레이션 단계 ### Step 1: 의존성 변경 1. `pyproject.toml` 수정 2. `pip install pymysql` 또는 `uv sync` 실행 ### Step 2: 설정 파일 수정 1. `config.py`의 DB URL 변경 2. `app/database/session.py` 전면 수정 ### Step 3: 라우터 수정 1. 각 라우터 파일의 `async def` → `def` 변경 2. 모든 `await` 키워드 제거 3. `AsyncSession` → `Session` 타입 힌트 변경 ### Step 4: 서비스 수정 1. 서비스 파일들의 async/await 제거 2. Raw SQL 쿼리 함수들 수정 ### Step 5: 의존성 수정 1. `dependencies.py` 파일들의 타입 힌트 변경 ### Step 6: 테스트 1. 모든 엔드포인트 기능 테스트 2. 성능 테스트 (동시 요청 처리 확인) --- ## 8. 결론 비동기에서 동기로 전환은 기술적으로 가능하지만, 다음을 고려해야 합니다: **장점**: - 코드 복잡도 감소 (async/await 제거) - 디버깅 용이 - 레거시 라이브러리와의 호환성 향상 **단점**: - 동시성 처리 능력 감소 - I/O 바운드 작업에서 성능 저하 가능 - FastAPI의 비동기 장점 미활용 현재 프로젝트가 FastAPI 기반이고 I/O 작업(DB, 외부 API 호출)이 많다면, **비동기 유지를 권장**합니다. 동기 전환은 특별한 요구사항(레거시 통합, 팀 역량 등)이 있을 때만 고려하시기 바랍니다.