fastapi-best-practices
npx skills add https://github.com/datamktkorea/agent-skills --skill fastapi-best-practices
Agent 安装分布
Skill 文档
Use this skill when you are developing a FastAPI application and want to ensure that you are following best practices for project structure, code organization, and common patterns. This skill can help you create maintainable and scalable FastAPI projects by providing guidelines and recommendations.
Skill: FastAPI DDD & Hexagonal Architecture Standard Guide
ì´ ê°ì´ëë FastAPI íë ììí¬ ê¸°ë° íë¡ì í¸ìì ëë©ì¸ 주ë ì¤ê³(DDD) ë° í¥ì¬ê³ ë ìí¤í ì²(Hexagonal Architecture)를 ì¼ê´ì± ìê² ì ì©í기 ìí íì¤ ê°ì´ëë¼ì¸ì ì ê³µí©ëë¤.
0. íë¡ì í¸ êµ¬ì¡° (Standard Project Structure)
app/
âââ main.py
âââ settings.py # pydantic-settings ê¸°ë° íê²½ ì¤ì
âââ libs/ # [SHARED] ê³µíµ ìì¡´ì± (DI Container, Global Exceptions)
â âââ containers.py # Dependency Injection (dependency-injector) ì¤ì
â âââ exceptions.py # ì ì ìì¸ ë° ìë¬ í¸ë¤ë¬
âââ routes/ # [DOMAINS] ëë©ì¸ ê¸°ë° API ë¼ì°í¸
âââ {domain_name}/ # ê°ë³ ëë©ì¸ ë¨ì (ì: users, orders, products)
âââ interface/ # [IN] Entry Points (Controller, Request/Response Schema)
â âââ controller.py
â âââ schema.py
â âââ dependencies.py # FastAPI Depends í©í 리 í¨ì (Native DI ë°©ì ì¬ì© ì)
âââ application/ # [CORE] Use-Cases & Orchestration
â âââ service.py
â âââ core/ # ìì ë¹ì¦ëì¤ ë¡ì§ ë¶ë¦¬ (ê³ì°, ë¶ì ë±)
âââ domain/ # [RULES] Pure Business Rules & Interfaces
â âââ entity.py # ìí°í° ë° ê° ê°ì²´ ì ì
â âââ exceptions.py # ëë©ì¸ ìì¸ í´ëì¤
â âââ repository.py # Repository ì¸í°íì´ì¤ (ABC)
âââ infra/ # [OUT] Technical Implementation
âââ models.py # ORM ëª¨ë¸ (SQLAlchemy ë±)
âââ repository.py # Repository 구íì²´
packages/ # [EXTERNAL] ì¬ì¬ì© ê°ë¥í ê³µíµ í¨í¤ì§ (DB, Auth, ì¸ë¶ ìë¹ì¤ ëí¼)
âââ database/ # DB ì°ê²° ë° ê¸°ì´ ì°ë
âââ auth/ # ì¸ì¦/ì¸ê° ê³µíµ ëª¨ë
âââ {service_name}/ # ì¸ë¶ ìë¹ì¤ ëí¼ (ì: aws/, email/, sms/)
tests/
âââ {domain_name}/
âââ unit/ # Application ê³ì¸µ ë¨ì í
ì¤í¸
âââ integration/ # Infrastructure ê³ì¸µ íµí© í
ì¤í¸
1. ìí¤í ì² ìì¹ (Architecture Principles)
ìì¡´ì± ê·ì¹ (Dependency Rule)
- 모ë ìì¡´ì±ì ì¸ë¶ìì ë´ë¶(ê³ ìì¤ ì ì± )ë¡ íë¬ì¼ í©ëë¤.
- Infrastructureë Domainì ì¸í°íì´ì¤ë¥¼ 구ííë©°, ì§ì ì ì¸ ìì ë ì´ì´ 참조를 ê¸ì§í©ëë¤.
- Applicationì ë¹ì¦ëì¤ ê·ì¹ì ì¡°í©íì¬ ì ì¤ì¼ì´ì¤ë¥¼ ìì±íë©°, íë ììí¬ ì¢ ìì±ì ìµìíí©ëë¤.
í¬í¸ì ì´ëí° (Ports & Adapters)
í¥ì¬ê³ ë ìí¤í ì²ì íµì¬ì ì í리ì¼ì´ì ì½ì´ë¥¼ ì¸ë¶ 기ì ë¡ë¶í° 격리íë ê²ì ëë¤.
| êµ¬ë¶ | ìí | í´ë¹ ê³ì¸µ |
|---|---|---|
| Driving Adapter (Primary) | ì¸ë¶ìì ì±ì í¸ì¶ | interface/controller.py |
| Port (Interface) | ì´ëí°ê° ë°ë¼ì¼ í ê·ê²© | domain/repository.py (ABC) |
| Driven Adapter (Secondary) | ì±ì´ ì¸ë¶ ìì¤í ì í¸ì¶ | infra/repository.py |
[HTTP Client] â [Controller] â [Service] â [Repository ABC] â [Repository Impl] â [DB]
(Driving) (Adapter) (Core) (Port) (Driven Adapter)
ëë©ì¸ ììì± ìì¹ (Domain Purity)
domain/ê³ì¸µì ì´ë¤ íë ììí¬ë importí´ìë ì ë©ëë¤. FastAPI, SQLAlchemy, Pydantic 모ë ê¸ì§.- ìì Python íì
(
dataclass,str,int,Optionalë±)ë§ ì¬ì©í©ëë¤. application/ê³ì¸µ ììHTTPExceptionë± HTTP ê°ë ì í¬í¨í´ìë ì ë©ëë¤. ëì ëë©ì¸ ìì¸ë¥¼ raiseíê³ , ë³íìinterface/ëë ì ì í¸ë¤ë¬ê° ë´ë¹í©ëë¤.
ìì¡´ì± ì£¼ì ë°©ì ì í (DI Strategy)
íë¡ì í¸ ê·ëª¨ì í ì í¸ì ë°ë¼ ë ê°ì§ ë°©ì ì¤ íë를 ì íí©ëë¤. ë°©ìì íë¡ì í¸ ìì ì íëë¡ íµì¼íë©°, í¼ì©íì§ ììµëë¤.
| dependency-injector | FastAPI Native DI | |
|---|---|---|
| ì í©í ê·ëª¨ | ì¤ëí (ëë©ì¸ 5ê° ì´ì) | ìí (ëë©ì¸ 3ê° ì´í) |
| ì¥ì | ì¤ì ì§ì¤ì ë°°ì , 컨í ì´ë ë¨ì Mock êµì²´ | ì¸ë¶ ë¼ì´ë¸ë¬ë¦¬ ìì, Python íì ìì¤í ê³¼ ìì°ì¤ë¬ì´ íµí© |
| ë¨ì | @inject + Provide[...] ë³´ì¼ë¬íë ì´í¸ |
ìì¡´ì± ê·¸ëíê° ë³µì¡í´ì§ë©´ í©í 리 í¨ì ë¶ì° |
dependency-injector ë°©ì (ì¤ëí íë¡ì í¸)
# libs/containers.py â ìì¡´ì± ê´ê³ë¥¼ í íì¼ìì ì¤ì ê´ë¦¬
from dependency_injector import containers, providers
class Container(containers.DeclarativeContainer):
wiring_config = containers.WiringConfiguration(packages=["app.routes"])
resource_repo = providers.Factory(ResourceRepositoryImpl)
resource_service = providers.Factory(ResourceService, repo=resource_repo)
# interface/controller.py
from dependency_injector.wiring import Provide, inject
@router.get("/{resource_id}")
@inject
async def get_resource(
resource_id: str,
service: ResourceService = Depends(Provide[Container.resource_service]),
):
return await service.get_resource(resource_id)
FastAPI Native DI ë°©ì (ìí íë¡ì í¸)
í©í 리 í¨ìë ë°ëì interface/dependencies.pyì 모ìëë¤. application/ì´ë domain/ ê³ì¸µì´ FastAPIì Dependsì ì¤ì¼ëë ê²ì ë°©ì§í©ëë¤.
# interface/dependencies.py â FastAPI Depends í©í ë¦¬ë§ ëª¨ìëë íì¼
from fastapi import Depends
from typing import Annotated
def get_resource_repo() -> ResourceRepository:
return ResourceRepositoryImpl()
def get_resource_service(
repo: Annotated[ResourceRepository, Depends(get_resource_repo)],
) -> ResourceService:
return ResourceService(repo=repo)
# interface/controller.py â í©í 리 í¨ìë dependencies.pyìì import
from .dependencies import get_resource_service
@router.get("/{resource_id}")
async def get_resource(
resource_id: str,
service: Annotated[ResourceService, Depends(get_resource_service)],
):
return await service.get_resource(resource_id)
2. ê³ì¸µë³ ìí ë° ì½ë 컨벤ì (Code Convention)
ð Interface Layer (controller.py, schema.py)
- Controller: FastAPI
APIRouter를 ì ìíë©°,@inject를 íµí´ íìí ìë¹ì¤ë¥¼ 주ì ë°ìµëë¤. ìì¸ ë³íì ì ì í¸ë¤ë¬ì ììíê³ , 컨í¸ë¡¤ë¬ë ì±ê³µ ì¼ì´ì¤ìë§ ì§ì¤í©ëë¤. - Schema: Pydantic v2를 ì¬ì©íì¬ API ê·ê²©ì ì ìí©ëë¤.
Field(description=...)ì íµí´ API 문ì를 ìëíí©ëë¤. Requestì Response ì¤í¤ë§ë ë°ëì ë¶ë¦¬í©ëë¤.
# schema.py â Request / Response ë¶ë¦¬
class CreateResourceRequest(BaseModel):
name: str = Field(..., description="리ìì¤ ì´ë¦")
class ResourceResponse(BaseModel):
id: str
name: str
# controller.py â ì ì í¸ë¤ë¬ íì¤ ì¬ì© ì, ì±ê³µ ì¼ì´ì¤ìë§ ì§ì¤
@router.get("/{resource_id}", status_code=200, response_model=ResourceResponse)
@inject
async def get_resource(
resource_id: str,
service: ResourceService = Depends(Provide[Container.resource_service]),
):
return await service.get_resource(resource_id)
# ResourceNotFoundException â libs/exceptions.py ì ì í¸ë¤ë¬ê° ìëì¼ë¡ 404 ë°í
íì¤ ê¶ì¥:
libs/exceptions.pyì ì ì í¸ë¤ë¬ë¥¼ ë±ë¡íì¬ ìì¸ ë³í ë¡ì§ì í ê³³ìì ê´ë¦¬í©ëë¤. ëì¼ ìì¸ì ëí´ ìëí¬ì¸í¸ë§ë¤ ë¤ë¥¸ ìëµì´ íìí ìì¸ì ì¸ ê²½ì°ì íí´ ì»¨í¸ë¡¤ë¬ ìì¤ì try/except를 íì©í©ëë¤. (Section 3 ì°¸ê³ )
ð Application Layer (service.py)
- Service: ë¹ì¦ëì¤ ì ì¤ì¼ì´ì¤ë¥¼ 구íí©ëë¤. Repository ì¸í°íì´ì¤ë¥¼ ìì±ìë¡ ì£¼ì
ë°ì¼ë©°, HTTP ê°ë
(
HTTPExceptionë±)ì í¬í¨íì§ ììµëë¤. - ë¹ì¦ëì¤ ê·ì¹ ìë° ì
domain/exceptions.pyì ì ìë ëë©ì¸ ìì¸ë¥¼ raiseí©ëë¤. - Separation: ë³µì¡í ê³ì°ì´ë ë¶ì ë¡ì§ì
core/ëë í 리ì ìì í´ëì¤ë¡ ë¶ë¦¬íì¬ í ì¤í¸ ì©ì´ì±ì íë³´í©ëë¤.
# application/service.py
class ResourceService:
def __init__(self, repo: ResourceRepository): # ì¸í°íì´ì¤ íì
ì¼ë¡ 주ì
self._repo = repo
async def get_resource(self, resource_id: str) -> ResourceEntity:
entity = await self._repo.find_by_id(resource_id)
if entity is None:
raise ResourceNotFoundException(resource_id) # ëë©ì¸ ìì¸, HTTPException ìë
return entity
async def create_resource(self, name: str) -> ResourceEntity:
entity = ResourceEntity(id=str(uuid4()), name=name)
return await self._repo.save(entity)
ð Domain Layer (entity.py, exceptions.py, repository.py)
- Entity: ê³ ì ìë³ì(
id)를 ê°ì§ë©° ê°ë³ì ì¸ ê°ì²´ì ëë¤. - Value Object: ìë³ì ìì´ ìì± ê° ìì²´ê° ëì¼ì±ì ì ìíë ë¶ë³ ê°ì²´ì
ëë¤.
@dataclass(frozen=True)ë¡ ë¶ë³ì±ì ê°ì í©ëë¤. - Repository Interface:
ABC를 ì¬ì©íì¬ ì ì¥ì ê·ê²©ì ì ìí©ëë¤. DB ì¢ ë¥(SQL, NoSQL, S3 ë±)ì 구ì ë°ì§ ììì¼ í©ëë¤. - Domain Exceptions: HTTP ìí ì½ëì 무ê´í ìì ë¹ì¦ëì¤ ìì¸ë¥¼ ì ìí©ëë¤.
# domain/entity.py
from dataclasses import dataclass
@dataclass
class ResourceEntity:
id: str
name: str
email: str
@dataclass(frozen=True) # Value Object â ë¶ë³, ëë±ì±ì ê°ì¼ë¡ íë¨
class Email:
value: str
def __post_init__(self):
if "@" not in self.value:
raise ValueError(f"Invalid email: {self.value}")
# domain/exceptions.py â HTTP ê°ë
ìì´ ìì ë¹ì¦ëì¤ ìì¸ë§ ì ì
class ResourceNotFoundException(Exception):
def __init__(self, resource_id: str):
self.resource_id = resource_id
super().__init__(f"Resource '{resource_id}' not found")
class DuplicateResourceException(Exception):
def __init__(self, name: str):
self.name = name
super().__init__(f"Resource '{name}' already exists")
# domain/repository.py
from abc import ABC, abstractmethod
class ResourceRepository(ABC):
@abstractmethod
async def find_by_id(self, id: str) -> Optional[ResourceEntity]:
pass
@abstractmethod
async def save(self, entity: ResourceEntity) -> ResourceEntity:
pass
ð Infrastructure Layer (models.py, repository.py – Implementation)
- ORM Model:
models.pyì SQLAlchemy ë± ORM 모ë¸ì ì ìí©ëë¤. Domain Entityì ë¶ë¦¬ë ë³ë í´ëì¤ì ëë¤. - Mapping: Repository 구íì²´ê° ORM Model â Domain Entity ë³íì ì ë´í©ëë¤. ì´ ë§¤í ë¡ì§ì´ Infrastructure ê³ì¸µì íµì¬ ì± ìì ëë¤.
# infra/models.py â ORM ëª¨ë¸ (íë ììí¬ ì¢
ì)
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class ResourceModel(Base):
__tablename__ = "resources"
id: Mapped[str] = mapped_column(primary_key=True)
name: Mapped[str]
email: Mapped[str]
# infra/repository.py â ëë©ì¸ ì¸í°íì´ì¤ 구í + ORM â Entity 매í
from sqlalchemy.ext.asyncio import AsyncSession
class ResourceRepositoryImpl(ResourceRepository):
def __init__(self, session: AsyncSession):
self._session = session
async def find_by_id(self, id: str) -> Optional[ResourceEntity]:
row = await self._session.get(ResourceModel, id)
if row is None:
return None
return self._to_entity(row) # ORM â Domain ë³í
async def save(self, entity: ResourceEntity) -> ResourceEntity:
model = self._to_model(entity) # Domain â ORM ë³í
self._session.add(model)
await self._session.flush()
return entity
def _to_entity(self, row: ResourceModel) -> ResourceEntity:
return ResourceEntity(id=row.id, name=row.name, email=row.email)
def _to_model(self, entity: ResourceEntity) -> ResourceModel:
return ResourceModel(id=entity.id, name=entity.name, email=entity.email)
Pragmatic DDD Trade-off: Entity â ORM Model â Pydantic Schema ì¬ì´ì 3ì¤ ë³íì íëê° ë§ìì§ìë¡ ì ì§ë³´ì ë¶ë´(매í ì¸ê¸)ì´ ì»¤ì§ëë¤. ë¹ì¦ëì¤ ë¡ì§ì´ ë¨ìíê±°ë í ê·ëª¨ê° ìì ê²½ì°, Domain Entityë¡ Pydantic
BaseModelì ì¬ì©íì¬ ë³í ê³ì¸µì ì¤ì´ë ì¤ì©ì ì ê·¼ë²ë ì íì§ì ëë¤. ë¨, ì´ ê²½ì° Domain ê³ì¸µì´ Pydanticì ì¢ ìëë¯ë¡ íì´ ëª ìì ì¼ë¡ í©ìíê³ ì¼ê´ì± ìê² ì ì©í´ì¼ í©ëë¤.ì´ ì ê·¼ë²ì ì ííë¤ë©´
model_config = ConfigDict(from_attributes=True)를 ì¤ì íì¸ì. ORM ê°ì²´ì ìì±ì ì§ì ì½ì´_to_entityë³í ë©ìëê° í ì¤ë¡ ì¶ì½ë©ëë¤.# domain/entity.py â Pragmatic DDD: Pydantic BaseModel ì¬ì© from pydantic import BaseModel, ConfigDict class ResourceEntity(BaseModel): model_config = ConfigDict(from_attributes=True) # ORM ê°ì²´ ìì± ì§ì ì½ê¸° id: str name: str email: str # infra/repository.py â _to_entity ë©ìëê° í ì¤ë¡ ì¶ì½ async def find_by_id(self, id: str) -> Optional[ResourceEntity]: row = await self._session.get(ResourceModel, id) if row is None: return None return ResourceEntity.model_validate(row) # â ìë 매í ë¶íì
3. íµì¬ ì¤ë¬´ ê°ì´ë (Best Practices)
ð ìë¬ ì²ë¦¬ ì ëµ (Error Handling)
ê³ì¸µë³ ìì¸ ì± ìì ëª íí ë¶ë¦¬í©ëë¤.
- Infrastructure: DB/ì¸ë¶ API ìì¸ë¥¼ ëë©ì¸ ìì¸ë¡ ëííê±°ë
Noneì ë°íí©ëë¤. - Domain: ë¹ì¦ëì¤ ê·ì¹ ìë° ì
domain/exceptions.pyì ìì ìì¸ë¥¼ raiseí©ëë¤. - Application: ëë©ì¸ ìì¸ë¥¼ ê·¸ëë¡ ì íí©ëë¤.
HTTPExceptionì ì§ì raiseíì§ ììµëë¤. - Interface: ëë©ì¸ ìì¸ë¥¼ catchíì¬
HTTPExceptionì¼ë¡ ë³íí©ëë¤.
ì ì í¸ë¤ë¬ë¥¼ íì©íë©´ 컨í¸ë¡¤ë¬ ì½ë를 ê°ê²°íê² ì ì§í ì ììµëë¤.
# libs/exceptions.py â ì ì ìì¸ í¸ë¤ë¬ ë±ë¡
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
def register_exception_handlers(app: FastAPI) -> None:
@app.exception_handler(ResourceNotFoundException)
async def not_found_handler(request: Request, exc: ResourceNotFoundException):
return JSONResponse(status_code=404, content={"detail": str(exc)})
@app.exception_handler(DuplicateResourceException)
async def conflict_handler(request: Request, exc: DuplicateResourceException):
return JSONResponse(status_code=409, content={"detail": str(exc)})
𧪠í ì¤í¸ ì ëµ (Testing Pyramid)
- Unit Test:
Applicationê³ì¸µì ìë¹ì¤ ë¡ì§ì í ì¤í¸í©ëë¤. RepositoryëAsyncMockì¼ë¡ ê³ ë¦½ìíµëë¤. - Integration Test:
Infrastructureê³ì¸µê³¼ ì¤ì DB를 ì°ê²°íì¬ Repository 구í체를 ê²ì¦í©ëë¤.testcontainers를 ì¬ì©íë©´ ì¤ì PostgreSQL 컨í ì´ë를 í ì¤í¸ ì¤ ìëì¼ë¡ ëì¸ ì ìì´, Mockì ìì±íë ë¹ì©ë³´ë¤ ì ë ´íê² ëì ì 뢰ë를 íë³´í ì ììµëë¤. - End-to-End (E2E):
httpx.AsyncClient를 ì¬ì©íì¬ ì¤ì API í¸ì¶ë¶í° ìëµê¹ì§ì íë¦ì ê²ì¦í©ëë¤. (ë¹ë기 ìëí¬ì¸í¸ìëTestClientëìAsyncClientì¬ì©)
# tests/unit/test_resource_service.py
import pytest
from unittest.mock import AsyncMock
@pytest.mark.asyncio
async def test_get_resource_not_found():
mock_repo = AsyncMock(spec=ResourceRepository)
mock_repo.find_by_id.return_value = None # AsyncMockì await ê°ë¥
service = ResourceService(repo=mock_repo)
with pytest.raises(ResourceNotFoundException):
await service.get_resource("non-existent-id")
# tests/integration/test_resource_repository.py â Testcontainers íì©
import pytest
from testcontainers.postgres import PostgresContainer
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
@pytest.fixture(scope="session")
def postgres():
with PostgresContainer("postgres:16") as pg:
yield pg
@pytest.mark.asyncio
async def test_find_by_id_returns_none_when_not_found(postgres):
engine = create_async_engine(postgres.get_connection_url())
async with AsyncSession(engine) as session:
repo = ResourceRepositoryImpl(session)
result = await repo.find_by_id("non-existent")
assert result is None
â¡ ì±ë¥ ë° ìºì± (Caching Pattern)
- Cache-Aside í¨í´: ë°ì´í° ì¡°í ì ìºì ì ì¥ì(Redis ë±)를 먼ì íì¸íê³ , ìì ê²½ì°ìë§ DB를 ì¡°íí ë¤ ìºì를 ê°±ì í©ëë¤.
- Side-Effect: ë°ì´í° ë³ê²½(
CUD)ì´ ë°ìí ê²½ì° ë°ëì ê´ë ¨ ìºì를 무í¨í(Invalidate)í´ì¼ í©ëë¤.
ð ë¡ê¹ ê·ì¹ (Observability)
- Contextual Logging: ë¡ê·¸ ë©ìì§ì
request_id,user_idë±ì 컨í ì¤í¸ë¥¼ í¬í¨íì¬ ì¶ì ì±ì ëì ëë¤. - Log Levels: 주ì íë¦ì
INFO, ë¹ì ìì ì´ë 복구 ê°ë¥í ìí©ìWARNING, ì¥ì ì¶ì ìERROR, ìì¸ ë°ì´í° ëë²ê¹ ìDEBUGë 벨ì ì격í ì¤ìí©ëë¤. - ë¯¼ê° ë°ì´í° ë§ì¤í¹: ë¹ë°ë²í¸, í í°, ê°ì¸ì ë³´(ì´ë©ì¼, ì íë²í¸ ë±)ë ì ë ë¡ê·¸ì í¬í¨íì§ ììµëë¤.
4. ì¤ì ê´ë¦¬ (Configuration Management)
pydantic-settings를 ì¬ì©íì¬ íê²½ ë³ì를 íì
ìì íê² ê´ë¦¬í©ëë¤. .env íì¼ê³¼ íê²½ ë³ì를 ëìì ì§ìí©ëë¤.
# settings.py
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
database_url: str
redis_url: str
secret_key: str
debug: bool = False
settings = Settings()
settingsì¸ì¤í´ì¤ë 모ë ìì¤ìì ì±ê¸í´ì¼ë¡ ìì±í©ëë¤.- DI Container(
containers.py)ììsettings를 참조íì¬ ì¸íë¼ ì»´í¬ëí¸ë¥¼ ì´ê¸°íí©ëë¤. - í
ì¤í¸ íê²½ììë
.env.testíì¼ ëë íê²½ ë³ì ì¤ë²ë¼ì´ëë¡ ì¤ì ì ë¶ë¦¬í©ëë¤.
5. ë°ì´í°ë² ì´ì¤ ì¸ì ê´ë¦¬ (DB Session Management)
í¸ëìì ê²½ê³ë¥¼ ì´ëì ê´ë¦¬í ì§ì ëí ë ê°ì§ ì ê·¼ë²ì´ ììµëë¤.
ë°©ë² A â ì¸ì ì§ì ì ë¬ (Session-per-Request)
FastAPIì Dependsë¡ ì¸ì
ì ìì±íê³ , Controller â Service â Repositoryë¡ ì¸ì를 íµí´ ì ë¬í©ëë¤.
êµ¬ì¡°ê° ë¨ìíê³ ì§ê´ì ì´ë©°, FastAPI ìíê³ìì ê°ì¥ ì¼ë°ì ì¸ í¨í´ì
ëë¤.
â ï¸ ìí¤í ì² í¸ë ì´ëì¤í:
AsyncSessionì Serviceê¹ì§ ì§ì ì ë¬íë©´ Application ê³ì¸µì´ SQLAlchemyì ì¢ ìë©ëë¤. í¥ì¬ê³ ë ìì¹ì ì격í ì¤ìí´ì¼ íë íë¡ì í¸ë¼ë©´ ë°©ë² B(UoW í¨í´)를 ì ííì¸ì.
# packages/database/session.py
async def get_session() -> AsyncGenerator[AsyncSession, None]:
async with async_session_factory() as session:
async with session.begin():
yield session
# interface/controller.py
@router.post("/", status_code=201)
@inject
async def create_resource(
body: CreateResourceRequest,
service: ResourceService = Depends(Provide[Container.resource_service]),
db: AsyncSession = Depends(get_session), # ìì²ë¹ ì¸ì
ìì±
):
return await service.create_resource(db=db, name=body.name)
ë°©ë² B â Unit of Work í¨í´
í¸ëìì
ê²½ê³ë¥¼ ëª
ìì ì¸ UnitOfWork ê°ì²´ë¡ 캡ìíí©ëë¤.
ì¬ë¬ Repositoryì ê±¸ì¹ ë³µì¡í í¸ëìì
ì ì¼ê´ëê² ê´ë¦¬í ë ì 리í©ëë¤.
# domain/unit_of_work.py â ì¶ì ì¸í°íì´ì¤ (Domain ê³ì¸µ, SQLAlchemy 미í¬í¨)
class AbstractUnitOfWork(ABC):
resources: ResourceRepository
@abstractmethod
async def commit(self): pass
@abstractmethod
async def rollback(self): pass
async def __aenter__(self): return self
async def __aexit__(self, *args): await self.rollback()
# infra/unit_of_work.py â 구íì²´ (Infrastructure ê³ì¸µ)
class SqlAlchemyUnitOfWork(AbstractUnitOfWork):
def __init__(self, session_factory):
self._session_factory = session_factory
async def __aenter__(self):
self._session = self._session_factory()
self.resources = ResourceRepositoryImpl(self._session)
return self
async def commit(self):
await self._session.commit()
async def rollback(self):
await self._session.rollback()
async def __aexit__(self, *args):
await super().__aexit__(*args)
await self._session.close()
# application/service.py â Applicationì AbstractUnitOfWorkë§ ìì¡´, SQLAlchemy 미í¬í¨
class ResourceService:
def __init__(self, uow: AbstractUnitOfWork):
self._uow = uow
async def create_resource(self, name: str) -> ResourceEntity:
async with self._uow:
entity = ResourceEntity(id=str(uuid4()), name=name)
await self._uow.resources.save(entity)
await self._uow.commit()
return entity
ì í 기ì¤: ë¨ì¼ ëë©ì¸ ë´ ë¨ì CRUDë ë°©ë² A, ì¬ë¬ Aggregateì ê±¸ì¹ í¸ëìì ì´ ë¹ë²íë©´ ë°©ë² B를 ì íí©ëë¤.