ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • FastAPI Clean Example
    이코에코(Eco²) Knowledge Base/Python 2025. 12. 31. 00:29

    참조: ivan-borovets/fastapi-clean-example

    작성일: 2025-12-30


    이 프로젝트에서 사용하는 아키텍처 패턴들의 원본 출처와 핵심 개념입니다.

    Clean Architecture

    항목 내용
    창시자 Robert C. Martin ("Uncle Bob")
    발표 2012년 8월 블로그 → 2017년 책 출간
    원본 The Clean Architecture
    Clean Architecture: A Craftsman's Guide to Software Structure and Design (2017)

    핵심 원칙:

    • 의존성 규칙 (Dependency Rule): 의존성은 항상 안쪽(고수준)으로만 향한다
    • 독립성: Framework, UI, Database, 외부 에이전시로부터 독립적
    • 동심원 구조: Entities → Use Cases → Interface Adapters → Frameworks & Drivers
    ┌─────────────────────────────────────────────────────────────────┐
    │                    Frameworks & Drivers                         │
    │  ┌─────────────────────────────────────────────────────────┐   │
    │  │                  Interface Adapters                      │   │
    │  │  ┌─────────────────────────────────────────────────┐    │   │
    │  │  │                  Use Cases                       │    │   │
    │  │  │  ┌─────────────────────────────────────────┐    │    │   │
    │  │  │  │              Entities                    │    │    │   │
    │  │  │  │       (Enterprise Business Rules)        │    │    │   │
    │  │  │  └─────────────────────────────────────────┘    │    │   │
    │  │  │        (Application Business Rules)              │    │   │
    │  │  └─────────────────────────────────────────────────┘    │   │
    │  │              (Controllers, Gateways, Presenters)         │   │
    │  └─────────────────────────────────────────────────────────┘   │
    │                    (Web, DB, Devices, UI, External)             │
    └─────────────────────────────────────────────────────────────────┘
                                  ▲
                                  │ 의존성 방향: 항상 안쪽으로

    Hexagonal Architecture (Ports & Adapters)

    항목 내용
    창시자 Alistair Cockburn
    발표 2005년
    원본 Hexagonal Architecture
    별명 Ports and Adapters Pattern

    핵심 개념:

    • Port: 애플리케이션이 외부와 통신하는 인터페이스 (추상)
    • Adapter: Port의 구체적 구현 (기술 의존적)
    • Driving Adapters: 애플리케이션을 호출하는 쪽 (HTTP Controller, CLI)
    • Driven Adapters: 애플리케이션이 호출하는 쪽 (Database, External API)
                        Driving Side                 Driven Side
                        (Primary)                    (Secondary)
                            │                            │
                            ▼                            ▼
    ┌──────────────┐   ┌─────────┐   ┌──────────┐   ┌─────────┐   ┌──────────────┐
    │ HTTP Request │──▶│  Port   │──▶│   App    │──▶│  Port   │──▶│   Database   │
    │  (Adapter)   │   │(Driving)│   │  Core    │   │(Driven) │   │  (Adapter)   │
    └──────────────┘   └─────────┘   └──────────┘   └─────────┘   └──────────────┘

    Domain-Driven Design (DDD)

    항목 내용
    창시자 Eric Evans
    발표 2003년
    Domain-Driven Design: Tackling Complexity in the Heart of Software (Blue Book)
    보조 Implementing Domain-Driven Design - Vaughn Vernon (2013, Red Book)

    전술적 패턴 (Tactical Patterns):

    패턴 설명 예시
    Entity 고유 식별자를 가진 객체, 생명주기 있음 User, Order
    Value Object 식별자 없음, 불변, 속성으로만 동등성 판단 Username, Money
    Aggregate 일관성 경계를 가진 Entity 클러스터 Order + OrderLine
    Repository Aggregate의 영속성 추상화 UserRepository
    Domain Service Entity에 속하지 않는 도메인 로직 TransferService
    Domain Event 도메인에서 발생한 중요한 사건 UserCreated
    Factory 복잡한 객체 생성 캡슐화 OrderFactory

    CQRS (Command Query Responsibility Segregation)

    항목 내용
    창시자 Greg Young
    발표 2010년경
    기원 CQS (Command Query Separation) - Bertrand Meyer
    참고 CQRS - Martin Fowler

    CQS vs CQRS:

    CQS (메서드 수준) CQRS (시스템 수준)
    메서드는 Command 또는 Query 모델을 Command/Query로 분리
    같은 객체 내 분리 별도의 Read/Write 모델
    Bertrand Meyer (1988) Greg Young (2010)
    ┌────────────────────────────────────────────────────────────┐
    │                        Client                              │
    └─────────────────┬──────────────────────┬───────────────────┘
                      │                      │
                      ▼                      ▼
             ┌───────────────┐      ┌───────────────┐
             │   Command     │      │    Query      │
             │    Model      │      │    Model      │
             │  (Write DB)   │      │  (Read DB)    │
             └───────────────┘      └───────────────┘
                      │                      ▲
                      │    Event/Sync        │
                      └──────────────────────┘

    Patterns of Enterprise Application Architecture (PoEAA)

    항목 내용
    저자 Martin Fowler
    발표 2002년
    Patterns of Enterprise Application Architecture
    카탈로그 martinfowler.com/eaaCatalog

    이 프로젝트에서 사용하는 PoEAA 패턴:

    패턴 설명 프로젝트 적용
    Repository 도메인 객체 컬렉션처럼 동작하는 매개체 UserCommandGateway
    Data Mapper 객체와 DB 테이블 간 매핑 분리 SqlaUserDataMapper
    Unit of Work 비즈니스 트랜잭션 동안의 변경 추적 Flusher + TransactionManager
    Gateway 외부 시스템 접근을 캡슐화 모든 *Gateway 포트
    Identity Map 같은 객체 중복 로딩 방지 SQLAlchemy Session

    Repository vs Gateway vs Data Mapper:

    ┌─────────────────────────────────────────────────────────────────┐
    │  Repository (DDD)                                               │
    │  - 도메인 객체의 컬렉션처럼 동작                                │
    │  - 도메인 언어로 표현 (findByUsername, save)                     │
    │  - Aggregate Root 단위                                          │
    ├─────────────────────────────────────────────────────────────────┤
    │  Gateway (PoEAA)                                                │
    │  - 외부 시스템 접근의 통로                                     │
    │  - 기술적 추상화 (데이터 흐름 관점)                              │
    │  - 더 범용적 (DB, API, 메시지 큐 등)                            │
    ├─────────────────────────────────────────────────────────────────┤
    │  Data Mapper (PoEAA)                                            │
    │  - 객체와 DB 레코드 간 변환                                    │
    │  - 영속성 무지(Persistence Ignorance) 달성                      │
    │  - ORM이 이 역할 수행 (SQLAlchemy)                              │
    └─────────────────────────────────────────────────────────────────┘

    SOLID Principles

    항목 내용
    창시자 Robert C. Martin
    발표 2000년대 초반
    출처 Agile Software Development, Principles, Patterns, and Practices (2002)

     

    원칙 설명 적용
    S - Single Responsibility 클래스는 하나의 책임만 Use Case별 Interactor 분리
    O - Open/Closed 확장에 열림, 수정에 닫힘 Port/Adapter 패턴
    L - Liskov Substitution 하위 타입 대체 가능 Protocol 기반 인터페이스
    I - Interface Segregation 클라이언트별 인터페이스 분리 Command/Query Gateway 분리
    D - Dependency Inversion 추상에 의존, 구체에 비의존 핵심 - 모든 Port가 이 원칙

     

    Dependency Inversion Principle (DIP) 상세:

    # ❌ 전통적 의존성 (고수준 → 저수준)
    class UserService:
        def __init__(self):
            self.repo = PostgresUserRepository()  # 구체 클래스 의존
    
    # ✅ 의존성 역전 (고수준 → 추상 ← 저수준)
    class UserService:
        def __init__(self, repo: UserRepository):  # 추상(Protocol) 의존
            self.repo = repo
    
    class PostgresUserRepository(UserRepository):  # 구체가 추상 구현
        ...

    Onion Architecture

    항목 내용
    창시자 Jeffrey Palermo
    발표 2008년
    관계 Clean Architecture의 전신 중 하나
    원본 The Onion Architecture

     

    Clean Architecture와의 비교:

    Onion Architecture Clean Architecture
    Domain Model (중심) Entities (중심)
    Domain Services Use Cases
    Application Services Interface Adapters
    Infrastructure Frameworks & Drivers

    아키텍처 타임라인

    1988  CQS (Bertrand Meyer)
          │
    2002  PoEAA (Martin Fowler) - Repository, Data Mapper, Unit of Work, Gateway
          │
    2003  DDD (Eric Evans) - Entity, Value Object, Aggregate, Repository
          │
    2005  Hexagonal Architecture (Alistair Cockburn) - Ports & Adapters
          │
    2008  Onion Architecture (Jeffrey Palermo)
          │
    2010  CQRS (Greg Young)
          │
    2012  Clean Architecture (Robert C. Martin) - 이전 개념들의 통합
          │
    2017  Clean Architecture 책 출간
          │
    2024  fastapi-clean-example - Python/FastAPI에 실전 적용

    1. 프로젝트 개요

    1.1 소개

    FastAPI 기반 Clean Architecture 백엔드 예제 프로젝트로, 다음 특징을 갖습니다:

    • No Stateful Globals: 의존성 주입(DI) 활용
    • Low Coupling: 의존성 역전 원칙(DIP) 적용
    • Tactical DDD: 도메인 주도 설계의 전술적 패턴
    • CQRS: Command/Query 책임 분리
    • Proper UoW: 올바른 Unit of Work 패턴 사용

    1.2 기술 스택

    • FastAPI
    • SQLAlchemy (async)
    • Dishka (DI Container)
    • bcrypt (Password Hashing)
    • JWT (Session-based Auth)

    2. 폴더 구조

    src/app/
    ├── domain/                                    # 🔵 Domain Layer
    │   ├── entities/
    │   │   └── user.py
    │   ├── enums/
    │   │   └── user_role.py
    │   ├── exceptions/
    │   │   └── user.py
    │   ├── ports/                                # Domain Ports
    │   │   ├── password_hasher.py
    │   │   └── user_id_generator.py
    │   ├── services/
    │   │   └── user.py
    │   └── value_objects/
    │       ├── raw_password.py
    │       ├── user_id.py
    │       ├── user_password_hash.py
    │       └── username.py
    │
    ├── application/                              # 🟢 Application Layer
    │   ├── commands/                             # Command Use Cases
    │   │   ├── activate_user.py
    │   │   ├── create_user.py
    │   │   ├── deactivate_user.py
    │   │   ├── grant_admin.py
    │   │   ├── revoke_admin.py
    │   │   └── set_user_password.py
    │   ├── queries/                              # Query Use Cases
    │   │   └── list_users.py
    │   └── common/
    │       ├── exceptions/
    │       ├── ports/                            # Application Ports
    │       │   ├── access_revoker.py
    │       │   ├── flusher.py
    │       │   ├── identity_provider.py
    │       │   ├── transaction_manager.py
    │       │   ├── user_command_gateway.py
    │       │   └── user_query_gateway.py
    │       ├── query_params/
    │       └── services/
    │
    ├── infrastructure/                           # 🟠 Infrastructure Layer
    │   ├── adapters/                             # Port 구현체들
    │   │   ├── main_flusher_sqla.py
    │   │   ├── main_transaction_manager_sqla.py
    │   │   ├── password_hasher_bcrypt.py
    │   │   ├── user_data_mapper_sqla.py
    │   │   ├── user_id_generator_uuid.py
    │   │   └── user_reader_sqla.py
    │   ├── auth/
    │   │   └── session/
    │   │       ├── ports/                        # Infrastructure 내부 Port
    │   │       │   └── gateway.py
    │   │       └── adapters/
    │   ├── exceptions/
    │   └── persistence_sqla/
    │       └── mappings/
    │
    ├── presentation/                             # 🟣 Presentation Layer
    │   └── http/
    │       ├── auth/
    │       │   └── adapters/
    │       ├── controllers/
    │       └── dependencies/
    │
    └── setup/                                    # ⚙️ Setup
        ├── config/
        └── di/

    3. 레이어별 역할

    3.1 Domain Layer

    위치: domain/

    역할: 핵심 비즈니스 로직, 외부 의존성 없음

    구성요소 설명 예시
    entities/ 도메인 엔티티 User
    value_objects/ 값 객체 Username, UserId, RawPassword
    services/ 도메인 서비스 UserService
    ports/ 도메인 Port PasswordHasher, UserIdGenerator
    enums/ 열거형 UserRole
    exceptions/ 도메인 예외 UsernameAlreadyExistsError

    3.2 Application Layer

    위치: application/

    역할: Use Case 구현, 비즈니스 흐름 조율

    구성요소 설명 예시
    commands/ 상태 변경 Use Case CreateUserInteractor
    queries/ 조회 Use Case ListUsersQueryService
    common/ports/ Application Port UserCommandGateway, Flusher
    common/services/ 공유 서비스 CurrentUserService

    3.3 Infrastructure Layer

    위치: infrastructure/

    역할: Port 구현, 외부 시스템 연동

    구성요소 설명 예시
    adapters/ Port 구현체 SqlaUserDataMapper
    persistence_sqla/ SQLAlchemy 설정/매핑 users_table
    auth/ 인증 인프라 세션, JWT
    exceptions/ 인프라 예외 DataMapperError

    3.4 Presentation Layer

    위치: presentation/

    역할: HTTP 처리, 사용자 인터페이스

    구성요소 설명 예시
    http/controllers/ API 엔드포인트 라우터
    http/dependencies/ FastAPI 의존성 Depends 함수
    http/auth/adapters/ HTTP 전송 어댑터 JwtCookieAuthSessionTransport

    4. Port와 Adapter 매핑

    4.1 Domain Ports

    Port (인터페이스) Adapter (구현체) 기술
    PasswordHasher BcryptPasswordHasher bcrypt
    UserIdGenerator UuidUserIdGenerator UUID

    4.2 Application Ports

    Port (인터페이스) Adapter (구현체) 기술
    UserCommandGateway SqlaUserDataMapper SQLAlchemy
    UserQueryGateway SqlaUserReader SQLAlchemy
    Flusher SqlaMainFlusher SQLAlchemy
    TransactionManager SqlaMainTransactionManager SQLAlchemy
    IdentityProvider - -
    AccessRevoker - -

    4.3 Infrastructure 내부 Ports

    Port (인터페이스) Adapter (구현체) 용도
    AuthSessionGateway SQLAlchemy 구현 세션 저장소 교체 용이

    5. Use Case 패턴 (CQRS)

    5.1 Command Use Case 구조

    # application/commands/create_user.py
    
    @dataclass(frozen=True, slots=True, kw_only=True)
    class CreateUserRequest:           # Input DTO
        username: str
        password: str
        role: UserRole
    
    class CreateUserResponse(TypedDict):  # Output DTO
        id: UUID
    
    class CreateUserInteractor:        # Use Case
        def __init__(
            self,
            current_user_service: CurrentUserService,
            user_service: UserService,
            user_command_gateway: UserCommandGateway,
            flusher: Flusher,
            transaction_manager: TransactionManager,
        ) -> None:
            self._current_user_service = current_user_service
            self._user_service = user_service
            self._user_command_gateway = user_command_gateway
            self._flusher = flusher
            self._transaction_manager = transaction_manager
    
        async def execute(self, request_data: CreateUserRequest) -> CreateUserResponse:
            # 인증/인가
            current_user = await self._current_user_service.get_current_user()
            authorize(CanManageRole(), context=RoleManagementContext(...))
    
            # 도메인 로직
            user = await self._user_service.create_user(...)
            self._user_command_gateway.add(user)
    
            # 영속성
            await self._flusher.flush()
            await self._transaction_manager.commit()
    
            return CreateUserResponse(id=user.id_.value)

    5.2 Query Use Case 구조

    # application/queries/list_users.py
    
    @dataclass(frozen=True, slots=True, kw_only=True)
    class ListUsersRequest:
        limit: int
        offset: int
        sorting_field: str
        sorting_order: SortingOrder
    
    class ListUsersQueryService:
        def __init__(
            self,
            current_user_service: CurrentUserService,
            user_query_gateway: UserQueryGateway,
        ) -> None:
            self._current_user_service = current_user_service
            self._user_query_gateway = user_query_gateway
    
        async def execute(self, request_data: ListUsersRequest) -> ListUsersQM:
            # 인증/인가
            current_user = await self._current_user_service.get_current_user()
            authorize(...)
    
            # 조회
            response = await self._user_query_gateway.read_all(
                pagination=OffsetPaginationParams(...),
                sorting=SortingParams(...),
            )
            return response

    5.3 Command vs Query 비교

    항목 Command Query
    클래스명 XxxInteractor XxxQueryService
    역할 상태 변경 조회만
    Gateway CommandGateway QueryGateway
    트랜잭션 ✅ 필요 ❌ 불필요
    반환값 ID 또는 void DTO/QueryModel

    6. Gateway 패턴

    6.1 Gateway vs Repository

    이 프로젝트는 "Repository" 대신 "Gateway"라는 용어를 사용합니다.

    용어 의미 장점
    Repository 도메인 객체 컬렉션 (DDD) 널리 알려짐
    Gateway 외부 시스템 통로 CQRS 분리 명확

    6.2 Gateway 인터페이스

    UserCommandGateway (쓰기용):

    class UserCommandGateway(Protocol):
        @abstractmethod
        def add(self, user: User) -> None: ...
    
        @abstractmethod
        async def read_by_id(
            self, user_id: UserId, for_update: bool = False
        ) -> User | None: ...
    
        @abstractmethod
        async def read_by_username(
            self, username: Username, for_update: bool = False
        ) -> User | None: ...

    UserQueryGateway (읽기용):

    class UserQueryModel(TypedDict):
        id_: UUID
        username: str
        role: UserRole
        is_active: bool
    
    class ListUsersQM(TypedDict):
        users: list[UserQueryModel]
        total: int
    
    class UserQueryGateway(Protocol):
        @abstractmethod
        async def read_all(
            self, pagination: OffsetPaginationParams, sorting: SortingParams
        ) -> ListUsersQM: ...

    6.3 Gateway 구현체

    # infrastructure/adapters/user_data_mapper_sqla.py
    
    class SqlaUserDataMapper(UserCommandGateway):
        def __init__(self, session: MainAsyncSession) -> None:
            self._session = session
    
        def add(self, user: User) -> None:
            self._session.add(user)
    
        async def read_by_id(self, user_id: UserId, for_update: bool = False) -> User | None:
            stmt = select(User).where(User.id_ == user_id)
            if for_update:
                stmt = stmt.with_for_update()
            return (await self._session.execute(stmt)).scalar_one_or_none()

    7. 명명 규칙

    7.1 Port 명명

    패턴 예시 설명
    Xxx + Gateway UserCommandGateway 데이터 접근 통로
    Xxx + er Flusher, PasswordHasher 동작 수행자
    Xxx + Manager TransactionManager 관리자
    Xxx + Provider IdentityProvider 제공자

    7.2 Adapter 명명

    패턴 예시 설명
    기술 + Port역할 SqlaUserDataMapper SQLAlchemy 구현
    기술 + Port이름 BcryptPasswordHasher bcrypt 구현
    기술 + 역할 SqlaUserReader SQLAlchemy 읽기

    7.3 Use Case 명명

    패턴 예시 설명
    동사 + 명사 + Interactor CreateUserInteractor Command
    동사 + 명사 + QueryService ListUsersQueryService Query

    8. Port의 3단계 구조

    ┌─────────────────────────────────────────────────────────────────┐
    │  domain/ports/                                                  │
    │  └── 도메인 개념 추상화                                          │
    │      - PasswordHasher: 비밀번호 해싱 (도메인 관심사)             │
    │      - UserIdGenerator: ID 생성 (도메인 관심사)                  │
    ├─────────────────────────────────────────────────────────────────┤
    │  application/common/ports/                                      │
    │  └── 레이어 간 경계 (Application → Infrastructure)              │
    │      - UserCommandGateway: 사용자 쓰기                          │
    │      - UserQueryGateway: 사용자 읽기                            │
    │      - Flusher: 영속성 플러시                                   │
    │      - TransactionManager: 트랜잭션 관리                        │
    ├─────────────────────────────────────────────────────────────────┤
    │  infrastructure/.../ports/                                      │
    │  └── 같은 레이어 내 교체 용이                                    │
    │      - AuthSessionGateway: 세션 저장소 (테스트/구현 교체)        │
    └─────────────────────────────────────────────────────────────────┘

    9. 의존성 흐름

    ┌─────────────────────────────────────────────────────────────────┐
    │                      Presentation Layer                         │
    │  HTTP Controllers → Use Case 호출                               │
    └──────────────────────────┬──────────────────────────────────────┘
                               │ depends on (interface)
                               ▼
    ┌─────────────────────────────────────────────────────────────────┐
    │                      Application Layer                          │
    │  Use Cases → Domain Services + Ports 사용                       │
    └──────────────────────────┬──────────────────────────────────────┘
                               │ depends on (interface)
                               ▼
    ┌─────────────────────────────────────────────────────────────────┐
    │                        Domain Layer                             │
    │  Entities + Value Objects + Domain Ports                        │
    └─────────────────────────────────────────────────────────────────┘
                               ▲
                               │ implements (DIP: 의존성 역전)
    ┌─────────────────────────────────────────────────────────────────┐
    │                     Infrastructure Layer                        │
    │  Adapters (SQLAlchemy, bcrypt, UUID 등) → Ports 구현            │
    └─────────────────────────────────────────────────────────────────┘

    10. 추가 개념

    10.1 Python Protocol vs ABC

    이 프로젝트는 Protocol (Structural Subtyping)을 사용합니다.

    방식 특징 사용
    ABC (Abstract Base Class) 명시적 상속 필요 (class A(Base)) 전통적 방식
    Protocol (typing.Protocol) 덕 타이핑, 상속 불필요 이 프로젝트
    # ABC 방식 (Nominal Typing)
    from abc import ABC, abstractmethod
    
    class UserRepository(ABC):
        @abstractmethod
        def get(self, id: int) -> User: ...
    
    class SqlUserRepository(UserRepository):  # 명시적 상속 필수
        def get(self, id: int) -> User: ...
    
    # Protocol 방식 (Structural Typing) - 이 프로젝트 방식
    from typing import Protocol
    
    class UserRepository(Protocol):
        def get(self, id: int) -> User: ...
    
    class SqlUserRepository:  # 상속 없이도 호환!
        def get(self, id: int) -> User: ...
    
    # 타입 체커가 구조적으로 검증
    def use_repo(repo: UserRepository): ...
    use_repo(SqlUserRepository())  # ✅ OK - 메서드 시그니처가 일치

    Protocol 선택 이유:

    • 더 유연한 결합
    • 테스트 Mock 작성이 쉬움
    • 런타임 오버헤드 없음

    10.2 테스트 전략

    Clean Architecture의 핵심 장점은 테스트 용이성입니다.

    레이어별 테스트:

    레이어 테스트 유형 Mock 대상
    Domain Unit Test 없음 (순수 로직)
    Application Unit Test Ports (Gateway, Flusher 등)
    Infrastructure Integration Test 실제 DB/Redis
    Presentation E2E Test 전체 또는 Use Case Mock
    # Use Case 단위 테스트 예시
    class MockUserCommandGateway:
        def __init__(self):
            self.users = []
    
        def add(self, user: User) -> None:
            self.users.append(user)
    
        async def read_by_username(self, username: Username) -> User | None:
            return next((u for u in self.users if u.username == username), None)
    
    class MockFlusher:
        async def flush(self) -> None:
            pass  # 아무것도 안 함
    
    class MockTransactionManager:
        async def commit(self) -> None:
            pass
    
    async def test_create_user():
        # Arrange
        gateway = MockUserCommandGateway()
        interactor = CreateUserInteractor(
            user_service=UserService(...),
            user_command_gateway=gateway,
            flusher=MockFlusher(),
            transaction_manager=MockTransactionManager(),
        )
    
        # Act
        result = await interactor.execute(CreateUserRequest(
            username="testuser",
            password="password123",
            role=UserRole.USER,
        ))
    
        # Assert
        assert result["id"] is not None
        assert len(gateway.users) == 1

    10.3 에러 핸들링 전략

    레이어별 예외 정의:

    ┌─────────────────────────────────────────────────────────────────┐
    │  Domain Exceptions                                              │
    │  - UsernameAlreadyExistsError                                  │
    │  - InvalidPasswordError                                        │
    │  - 순수 비즈니스 규칙 위반                                       │
    ├─────────────────────────────────────────────────────────────────┤
    │  Application Exceptions                                         │
    │  - AuthenticationError                                         │
    │  - AuthorizationError                                          │
    │  - Use Case 실행 중 오류                                        │
    ├─────────────────────────────────────────────────────────────────┤
    │  Infrastructure Exceptions                                      │
    │  - DataMapperError (DB 오류)                                   │
    │  - ReaderError (조회 오류)                                     │
    │  - 기술적 오류                                                  │
    ├─────────────────────────────────────────────────────────────────┤
    │  Presentation Layer                                             │
    │  - 모든 예외를 HTTP 응답으로 변환                               │
    │  - Domain/App 예외 → 4xx                                       │
    │  - Infrastructure 예외 → 5xx                                   │
    └─────────────────────────────────────────────────────────────────┘

    예외 변환 예시:

    # presentation/http/exception_handlers.py
    
    @app.exception_handler(UsernameAlreadyExistsError)
    async def handle_username_exists(request, exc):
        return JSONResponse(
            status_code=409,  # Conflict
            content={"detail": str(exc)},
        )
    
    @app.exception_handler(AuthenticationError)
    async def handle_auth_error(request, exc):
        return JSONResponse(
            status_code=401,  # Unauthorized
            content={"detail": "Authentication required"},
        )
    
    @app.exception_handler(DataMapperError)
    async def handle_db_error(request, exc):
        logger.error(f"Database error: {exc}")
        return JSONResponse(
            status_code=500,  # Internal Server Error
            content={"detail": "Internal server error"},
        )

    10.4 데이터 흐름 예시

    HTTP 요청 → DB 저장 전체 흐름:

    [Client]
        │
        │ POST /users {"username": "john", "password": "secret"}
        ▼
    ┌─────────────────────────────────────────────────────────────────┐
    │  Presentation Layer                                             │
    │  ┌─────────────────────────────────────────────────────────┐   │
    │  │  Controller (router)                                     │   │
    │  │  - Request 파싱 (Pydantic)                              │   │
    │  │  - Use Case 호출                                        │   │
    │  │  - Response 반환                                        │   │
    │  └──────────────────────────┬──────────────────────────────┘   │
    └─────────────────────────────┼───────────────────────────────────┘
                                  │ CreateUserRequest
                                  ▼
    ┌─────────────────────────────────────────────────────────────────┐
    │  Application Layer                                              │
    │  ┌─────────────────────────────────────────────────────────┐   │
    │  │  CreateUserInteractor.execute()                          │   │
    │  │  1. 인증/인가 확인                                       │   │
    │  │  2. UserService.create_user() 호출                      │   │
    │  │  3. UserCommandGateway.add(user)                        │   │
    │  │  4. Flusher.flush()                                     │   │
    │  │  5. TransactionManager.commit()                         │   │
    │  └──────────────────────────┬──────────────────────────────┘   │
    └─────────────────────────────┼───────────────────────────────────┘
                                  │ User entity
                                  ▼
    ┌─────────────────────────────────────────────────────────────────┐
    │  Domain Layer                                                   │
    │  ┌─────────────────────────────────────────────────────────┐   │
    │  │  UserService.create_user()                               │   │
    │  │  1. Username 값 객체 생성 (유효성 검증)                  │   │
    │  │  2. PasswordHasher.hash() 호출 (Port)                   │   │
    │  │  3. UserIdGenerator.generate() 호출 (Port)              │   │
    │  │  4. User 엔티티 생성                                     │   │
    │  └──────────────────────────┬──────────────────────────────┘   │
    └─────────────────────────────┼───────────────────────────────────┘
                                  │ Port 호출
                                  ▼
    ┌─────────────────────────────────────────────────────────────────┐
    │  Infrastructure Layer                                           │
    │  ┌─────────────────────────────────────────────────────────┐   │
    │  │  BcryptPasswordHasher.hash()  → bcrypt 라이브러리       │   │
    │  │  UuidUserIdGenerator.generate() → uuid4()               │   │
    │  │  SqlaUserDataMapper.add() → session.add()               │   │
    │  │  SqlaMainFlusher.flush() → session.flush()              │   │
    │  │  SqlaMainTransactionManager.commit() → session.commit() │   │
    │  └──────────────────────────┬──────────────────────────────┘   │
    └─────────────────────────────┼───────────────────────────────────┘
                                  │ SQL
                                  ▼
                             [PostgreSQL]

    10.5 Trade-offs (장단점)

    장점:

    장점 설명
    테스트 용이성 각 레이어 독립적 테스트 가능
    유지보수성 변경 영향 범위 최소화
    기술 교체 용이 DB, Framework 교체가 비즈니스 로직에 영향 없음
    관심사 분리 각 레이어의 책임이 명확
    도메인 중심 비즈니스 로직이 기술에 오염되지 않음

    단점:

    단점 설명 완화 방안
    복잡성 증가 파일/클래스 수 증가 작은 프로젝트에는 과할 수 있음
    보일러플레이트 Port, Adapter, DTO 중복 코드 생성기 활용
    학습 곡선 팀 전체가 이해해야 함 문서화, 예제 코드
    초기 개발 속도 설정해야 할 것이 많음 템플릿 프로젝트 활용
    과도한 추상화 단순한 CRUD에도 레이어 통과 필요시 단순화

    적용 기준:

    프로젝트 규모/복잡도
           │
           ├── 작은 프로젝트 (CRUD 위주)
           │   → 단순 레이어드 아키텍처로 충분
           │
           ├── 중간 규모 (복잡한 비즈니스 로직)
           │   → Clean Architecture 고려
           │
           └── 대규모 (마이크로서비스, 장기 운영)
               → Clean Architecture 강력 권장

    10.6 DI 컨테이너 설정 (Dishka) - 예제 참고용

    ⚠️ Note: 이 섹션은 예제 프로젝트의 Dishka 사용 방식을 참고용으로 기록한 것입니다. 우리 프로젝트에서는 FastAPI 기본 Depends() 패턴을 사용합니다.

    fastapi-clean-example은 Dishka를 사용합니다:

    # setup/di/providers.py
    
    from dishka import Provider, Scope, provide
    
    class InfrastructureProvider(Provider):
        scope = Scope.REQUEST
    
        @provide
        async def get_session(self) -> AsyncSession:
            async with async_session_maker() as session:
                yield session
    
        @provide
        def get_user_command_gateway(
            self, session: AsyncSession
        ) -> UserCommandGateway:
            return SqlaUserDataMapper(session)
    
        @provide
        def get_flusher(self, session: AsyncSession) -> Flusher:
            return SqlaMainFlusher(session)
    
    class ApplicationProvider(Provider):
        scope = Scope.REQUEST
    
        @provide
        def get_create_user_interactor(
            self,
            user_service: UserService,
            gateway: UserCommandGateway,
            flusher: Flusher,
            tx_manager: TransactionManager,
        ) -> CreateUserInteractor:
            return CreateUserInteractor(
                user_service=user_service,
                user_command_gateway=gateway,
                flusher=flusher,
                transaction_manager=tx_manager,
            )
    
    # main.py
    from dishka.integrations.fastapi import setup_dishka
    
    container = make_async_container(
        InfrastructureProvider(),
        ApplicationProvider(),
    )
    setup_dishka(container, app)

    FastAPI 엔드포인트에서 사용:

    from dishka.integrations.fastapi import FromDishka
    
    @router.post("/users")
    async def create_user(
        request: CreateUserHttpRequest,
        interactor: FromDishka[CreateUserInteractor],
    ):
        result = await interactor.execute(CreateUserRequest(
            username=request.username,
            password=request.password,
            role=request.role,
        ))
        return {"id": result["id"]}

    10.7 Entity Generic (Python 3.12+)

    예제 프로젝트는 Python 3.12+의 새로운 Generic 문법을 사용합니다:

    # src/app/domain/entities/base.py
    from collections.abc import Hashable
    from typing import Any, Self, cast
    
    class Entity[T: Hashable]:  # ← Python 3.12+ Generic 문법
        """
        Base class for domain entities, defined by a unique identity (`id`).
        - `id`: Identity that remains constant throughout the entity's lifecycle.
        - Entities are mutable, but are compared solely by their `id`.
        """
    
        def __new__(cls, *_args: Any, **_kwargs: Any) -> Self:
            if cls is Entity:
                raise TypeError("Base Entity cannot be instantiated directly.")
            return object.__new__(cls)
    
        def __init__(self, *, id_: T) -> None:
            self.id_ = id_
    
        def __setattr__(self, name: str, value: Any) -> None:
            # Prevents modifying the `id` after it's set
            if name == "id_" and getattr(self, "id_", None) is not None:
                raise AttributeError("Changing entity ID is not permitted.")
            object.__setattr__(self, name, value)
    
        def __eq__(self, other: object) -> bool:
            # ID 기반 동등성
            return type(self) is type(other) and cast(Self, other).id_ == self.id_
    
        def __hash__(self) -> int:
            return hash((type(self), self.id_))

    주요 특징:

    • Entity[T: Hashable]: T는 Hashable을 상속해야 함
    • __new__: Base Entity 직접 인스턴스화 방지
    • __setattr__: ID 변경 불가 (불변성)
    • __eq__, __hash__: ID 기반 동등성 및 해시

    10.8 BcryptPasswordHasher 복잡한 DI 패턴

    비밀번호 해싱은 CPU 집약적 작업이므로 비동기 + 스레드풀 + 세마포어 패턴을 사용합니다:

    # src/app/infrastructure/adapters/password_hasher_bcrypt.py
    class BcryptPasswordHasher(PasswordHasher):
        def __init__(
            self,
            pepper: bytes,                           # 보안: 서버 측 비밀 키
            work_factor: int,                        # bcrypt 라운드 수 (12~14 권장)
            executor: HasherThreadPoolExecutor,      # CPU 작업용 스레드풀
            semaphore: HasherSemaphore,              # 동시 요청 제한
            semaphore_wait_timeout_s: float,         # 세마포어 타임아웃
        ) -> None:
            ...
    
        async def hash(self, raw_password: RawPassword) -> UserPasswordHash:
            async with self._permit():  # 세마포어 획득
                loop = asyncio.get_running_loop()
                return await loop.run_in_executor(
                    self._executor,        # 스레드풀에서 실행
                    self.hash_sync,        # 동기 해싱 함수
                    raw_password,
                )
    
        @asynccontextmanager
        async def _permit(self) -> AsyncIterator[None]:
            """세마포어로 동시 해싱 요청 제한"""
            try:
                await asyncio.wait_for(
                    self._semaphore.acquire(),
                    timeout=self._semaphore_wait_timeout_s,
                )
            except TimeoutError as err:
                raise PasswordHasherBusyError from err
            try:
                yield
            finally:
                self._semaphore.release()
    
        def hash_sync(self, raw_password: RawPassword) -> UserPasswordHash:
            # OWASP 권장: Pre-hashing with pepper
            base64_hmac_peppered = self._add_pepper(raw_password, self._pepper)
            salt = bcrypt.gensalt(rounds=self._work_factor)
            return UserPasswordHash(bcrypt.hashpw(base64_hmac_peppered, salt))

    DI 설정 (setup/ioc/domain.py):

    class DomainProvider(Provider):
        @provide
        def provide_password_hasher(
            self,
            security: SecuritySettings,
            executor: HasherThreadPoolExecutor,
            semaphore: HasherSemaphore,
        ) -> PasswordHasher:
            return BcryptPasswordHasher(
                pepper=security.password.pepper.encode(),
                work_factor=security.password.hasher_work_factor,
                executor=executor,
                semaphore=semaphore,
                semaphore_wait_timeout_s=security.password.hasher_semaphore_wait_timeout_s,
            )

    설계 이유:

    • ThreadPoolExecutor: bcrypt는 GIL을 해제하지 않아 스레드풀 필요
    • Semaphore: 과도한 동시 해싱 요청 방지 (DoS 공격 대응)
    • Pepper: DB 유출 시에도 비밀번호 보호 (OWASP 권장)

    10.9 테스트 구조 및 Mock 전략

    테스트 디렉토리 구조

    tests/app/
    ├── unit/
    │   ├── domain/
    │   │   ├── entities/
    │   │   │   └── test_base.py
    │   │   ├── services/
    │   │   │   ├── conftest.py          # Mock fixture 정의
    │   │   │   ├── mock_types.py        # Mock 타입 정의
    │   │   │   └── test_user.py
    │   │   └── value_objects/
    │   │       └── test_username.py
    │   ├── application/
    │   │   └── authz_service/
    │   │       └── test_permissions.py
    │   ├── infrastructure/
    │   │   └── test_password_hasher_bcrypt.py
    │   ├── setup/
    │   │   └── test_cfg_*.py
    │   └── factories/                    # 테스트용 팩토리
    │       ├── user_entity.py
    │       ├── value_objects.py
    │       └── named_entity.py
    ├── integration/
    │   └── setup/
    │       └── test_cfg_loader.py
    └── performance/
        └── profile_password_hasher_bcrypt.py

    conftest.py - Mock Fixture 패턴

    # tests/app/unit/domain/services/conftest.py
    from unittest.mock import create_autospec
    import pytest
    
    @pytest.fixture
    def user_id_generator() -> UserIdGeneratorMock:
        return cast(UserIdGeneratorMock, create_autospec(UserIdGenerator, instance=True))
    
    @pytest.fixture
    def password_hasher() -> PasswordHasherMock:
        return cast(PasswordHasherMock, create_autospec(PasswordHasher, instance=True))

    테스트 팩토리 패턴

    # tests/app/unit/factories/user_entity.py
    def create_user(
        user_id: UserId | None = None,
        username: Username | None = None,
        password_hash: UserPasswordHash | None = None,
        role: UserRole = UserRole.USER,
        is_active: bool = True,
    ) -> User:
        return User(
            id_=user_id or create_user_id(),
            username=username or create_username(),
            password_hash=password_hash or create_password_hash(),
            role=role,
            is_active=is_active,
        )

    실제 테스트 예시

    # tests/app/unit/domain/services/test_user.py
    @pytest.mark.asyncio
    @pytest.mark.parametrize("role", [UserRole.USER, UserRole.ADMIN])
    async def test_creates_active_user_with_hashed_password(
        role: UserRole,
        user_id_generator: UserIdGeneratorMock,
        password_hasher: PasswordHasherMock,
    ) -> None:
        # Arrange
        username = create_username()
        raw_password = create_raw_password()
        expected_id = create_user_id()
        expected_hash = create_password_hash()
    
        user_id_generator.generate.return_value = expected_id
        password_hasher.hash.return_value = expected_hash
    
        sut = UserService(user_id_generator, password_hasher)
    
        # Act
        result = await sut.create_user(username, raw_password, role)
    
        # Assert
        assert isinstance(result, User)
        assert result.id_ == expected_id
        assert result.password_hash == expected_hash

    테스트 전략:

    • create_autospec: Protocol의 스펙을 유지하면서 Mock 생성
    • Factory 패턴: 테스트 데이터 생성 일관성
    • Parametrize: 여러 케이스 효율적 테스트

    10.10 Command 전체 의존성 목록

    문서의 핵심 의존성 외에 각 Command의 전체 의존성입니다:

    Command 전체 의존성
    CreateUserInteractor CurrentUserService, UserService, UserCommandGateway, Flusher, TransactionManager
    ActivateUserInteractor CurrentUserService, UserCommandGateway, UserService, Flusher, TransactionManager
    DeactivateUserInteractor CurrentUserService, UserCommandGateway, UserService, TransactionManager, AccessRevoker
    SetUserPasswordInteractor CurrentUserService, UserCommandGateway, UserService, Flusher, TransactionManager
    GrantAdminInteractor CurrentUserService, UserCommandGateway, UserService, Flusher, TransactionManager
    RevokeAdminInteractor CurrentUserService, UserCommandGateway, UserService, Flusher, TransactionManager
    Query 전체 의존성
    ListUsersQueryService CurrentUserService, UserQueryGateway

    공통 패턴:

    • 모든 Command/Query는 CurrentUserService로 현재 사용자 조회
    • Command는 TransactionManager로 트랜잭션 커밋
    • 쓰기 작업은 Flusher로 변경사항 플러시
    • 사용자 비활성화 시 AccessRevoker로 세션 삭제

    10.11 Value Object 자기 검증 패턴

    Value Object는 생성 시점에 불변성을 검증하여 도메인 무결성을 보장합니다.

    ValueObject 베이스 클래스

    # src/app/domain/value_objects/base.py
    from dataclasses import dataclass, fields
    from typing import Any, Self
    
    @dataclass(frozen=True, slots=True, repr=False)
    class ValueObject:
        """
        Base class for immutable value objects (VO) in domain.
        - Defined by instance attributes only; these must be immutable.
        - For simple type tagging, consider `typing.NewType` instead.
        """
    
        def __new__(cls, *_args: Any, **_kwargs: Any) -> Self:
            if cls is ValueObject:
                raise TypeError("Base ValueObject cannot be instantiated directly.")
            if not fields(cls):
                raise TypeError(f"{cls.__name__} must have at least one field!")
            return object.__new__(cls)
    
        def __post_init__(self) -> None:
            """Hook for additional initialization and ensuring invariants."""
    
        def __repr__(self) -> str:
            """
            - 1 field: outputs value only
            - 2+ fields: outputs `name=value` format
            - All `repr=False`: outputs '<hidden>'
            """
            return f"{type(self).__name__}({self.__repr_value()})"
    
        def __repr_value(self) -> str:
            items = [f for f in fields(self) if f.repr]
            if not items:
                return "<hidden>"
            if len(items) == 1:
                return f"{getattr(self, items[0].name)!r}"
            return ", ".join(f"{f.name}={getattr(self, f.name)!r}" for f in items)

    Username - 복잡한 검증 로직

    # src/app/domain/value_objects/username.py
    import re
    from dataclasses import dataclass
    from typing import ClassVar, Final
    
    @dataclass(frozen=True, slots=True, repr=False)
    class Username(ValueObject):
        """raises DomainTypeError"""
    
        # 검증 규칙 상수
        MIN_LEN: ClassVar[Final[int]] = 5
        MAX_LEN: ClassVar[Final[int]] = 20
    
        # 정규식 패턴들
        PATTERN_START: ClassVar[Final[re.Pattern[str]]] = re.compile(
            r"^[a-zA-Z0-9]",  # 문자/숫자로 시작
        )
        PATTERN_ALLOWED_CHARS: ClassVar[Final[re.Pattern[str]]] = re.compile(
            r"[a-zA-Z0-9._-]*",  # 허용 문자: 문자, 숫자, ., -, _
        )
        PATTERN_NO_CONSECUTIVE_SPECIALS: ClassVar[Final[re.Pattern[str]]] = re.compile(
            r"^[a-zA-Z0-9]+([._-]?[a-zA-Z0-9]+)*[._-]?$",  # 연속 특수문자 금지
        )
        PATTERN_END: ClassVar[Final[re.Pattern[str]]] = re.compile(
            r".*[a-zA-Z0-9]$",  # 문자/숫자로 끝
        )
    
        value: str
    
        def __post_init__(self) -> None:
            """생성 시점에 모든 검증 수행"""
            self._validate_username_length(self.value)
            self._validate_username_pattern(self.value)
    
        def _validate_username_length(self, username_value: str) -> None:
            if len(username_value) < self.MIN_LEN or len(username_value) > self.MAX_LEN:
                raise DomainTypeError(
                    f"Username must be between {self.MIN_LEN} and {self.MAX_LEN} characters."
                )
    
        def _validate_username_pattern(self, username_value: str) -> None:
            if not re.match(self.PATTERN_START, username_value):
                raise DomainTypeError(
                    "Username must start with a letter or digit."
                )
            if not re.fullmatch(self.PATTERN_ALLOWED_CHARS, username_value):
                raise DomainTypeError(
                    "Username can only contain letters, digits, dots, hyphens, and underscores."
                )
            if not re.fullmatch(self.PATTERN_NO_CONSECUTIVE_SPECIALS, username_value):
                raise DomainTypeError(
                    "Username cannot contain consecutive special characters."
                )
            if not re.match(self.PATTERN_END, username_value):
                raise DomainTypeError(
                    "Username must end with a letter or digit."
                )

    RawPassword - 민감 데이터 처리

    # src/app/domain/value_objects/raw_password.py
    from dataclasses import dataclass, field
    
    @dataclass(frozen=True, slots=True, repr=False)
    class RawPassword(ValueObject):
        """raises DomainTypeError"""
    
        MIN_LEN: ClassVar[Final[int]] = 6
    
        # repr=False로 로그 노출 방지
        value: bytes = field(init=False, repr=False)
    
        def __init__(self, value: str) -> None:
            """str 입력 → bytes 저장 (인코딩)"""
            self._validate_password_length(value)
            # frozen=True이지만 __init__에서는 object.__setattr__ 사용 가능
            object.__setattr__(self, "value", value.encode())
    
        def _validate_password_length(self, password_value: str) -> None:
            if len(password_value) < self.MIN_LEN:
                raise DomainTypeError(
                    f"Password must be at least {self.MIN_LEN} characters long."
                )

    설계 원칙

    원칙 구현 방법
    불변성 (Immutable) @dataclass(frozen=True) - 생성 후 변경 불가
    자기 검증 (Self-Validating) __post_init__ 또는 __init__에서 검증
    즉시 실패 (Fail Fast) 잘못된 값은 생성 시점에 예외 발생
    민감 데이터 보호 repr=False로 로그 노출 방지
    값 동등성 같은 값을 가지면 동일한 객체로 취급

    사용 예시

    # ✅ 유효한 값 - 정상 생성
    username = Username("john_doe")
    password = RawPassword("secret123")
    
    # ❌ 잘못된 값 - 즉시 예외 발생
    Username("ab")           # DomainTypeError: 5자 미만
    Username("__invalid")    # DomainTypeError: 특수문자로 시작
    Username("user..name")   # DomainTypeError: 연속 특수문자
    RawPassword("12345")     # DomainTypeError: 6자 미만
    
    # 민감 데이터 보호
    print(password)  # RawPassword(<hidden>) - 값이 노출되지 않음

    11. 우리 프로젝트 적용 방안

    11.1 현재 vs 목표 구조

    현재 (domains/auth/):

    domains/auth/
    ├── api/v1/endpoints/      # Presentation (혼합)
    ├── application/services/  # Application (혼합)
    ├── core/                  # 설정 + 보안 (혼합)
    └── infrastructure/        # Infrastructure

    목표:

    domains/auth/
    ├── domain/
    │   ├── entities/
    │   ├── value_objects/
    │   ├── ports/                    # Domain Ports
    │   └── services/
    │
    ├── application/
    │   ├── use_cases/
    │   │   ├── commands/             # Command Use Cases
    │   │   └── queries/              # Query Use Cases
    │   ├── ports/                    # Application Ports
    │   └── services/
    │
    ├── infrastructure/
    │   ├── adapters/                 # Port 구현체
    │   ├── persistence_postgres/
    │   ├── persistence_redis/
    │   └── security/
    │
    ├── presentation/
    │   └── http/
    │       ├── controllers/
    │       ├── dependencies/
    │       └── errors/
    │
    ├── workers/                      # Worker Entry Points
    │   ├── consumers/
    │   └── jobs/
    │
    └── setup/
        ├── config.py
        └── di.py

    11.2 Port-Adapter 매핑 예시

    현재 목표 Port 목표 Adapter
    UserRepository IUserCommandGateway PostgresUserDataMapper
    UserRepository IUserQueryGateway PostgresUserReader
    TokenService ITokenService JwtTokenService
    OAuthStateStore IStateStore RedisStateStore
    TokenBlacklist ITokenBlacklist RedisTokenBlacklist

    12. 전체 파일 카탈로그

    예제 프로젝트의 모든 파일을 계층, 역할, 의존성으로 분류한 상세 목록입니다.

    12.1 Domain Layer (src/app/domain/)

    도메인 레이어는 외부 의존성 ZERO - 순수 Python만 사용합니다.

    Entities (domain/entities/)

    파일 역할 의존성
    base.py Entity 베이스 클래스 (ID 기반 동등성) 없음
    user.py User 엔티티 (Aggregate Root) base.py, value_objects/*

    Value Objects (domain/value_objects/)

    파일 역할 의존성
    base.py Value Object 베이스 클래스 (값 기반 동등성) 없음
    user_id.py UserId VO (UUID 래퍼) base.py
    username.py Username VO (검증 로직 포함) base.py
    raw_password.py RawPassword VO (평문 비밀번호) base.py
    user_password_hash.py UserPasswordHash VO (해시된 비밀번호) base.py

    Enums (domain/enums/)

    파일 역할 의존성
    user_role.py UserRole 열거형 (USER, ADMIN, SUPER_ADMIN) 없음

    Ports (domain/ports/)

    파일 역할 구현체 위치
    password_hasher.py PasswordHasher Protocol infrastructure/adapters/password_hasher_bcrypt.py
    user_id_generator.py UserIdGenerator Protocol infrastructure/adapters/user_id_generator_uuid.py

    Services (domain/services/)

    파일 역할 의존성
    user.py UserService (사용자 생성, 비밀번호 설정 등 도메인 로직) domain/ports/*, domain/entities/*

    Exceptions (domain/exceptions/)

    파일 역할 의존성
    base.py DomainError 베이스 클래스 없음
    user.py User 관련 도메인 예외들 base.py

    12.2 Application Layer (src/app/application/)

    애플리케이션 레이어는 Domain에만 의존합니다.

    Commands (application/commands/)

    파일 Use Case Input Output 핵심 의존성
    create_user.py CreateUserInteractor CreateUserRequest CreateUserResponse UserCommandGateway, UserService, Flusher
    activate_user.py ActivateUserInteractor ActivateUserRequest None UserCommandGateway, Flusher
    deactivate_user.py DeactivateUserInteractor DeactivateUserRequest None UserCommandGateway, AccessRevoker
    set_user_password.py SetUserPasswordInteractor SetUserPasswordRequest None UserCommandGateway, UserService
    grant_admin.py GrantAdminInteractor GrantAdminRequest None UserCommandGateway
    revoke_admin.py RevokeAdminInteractor RevokeAdminRequest None UserCommandGateway

    Queries (application/queries/)

    파일 Use Case Input Output 핵심 의존성
    list_users.py ListUsersQueryService ListUsersRequest ListUsersQM UserQueryGateway

    Ports (application/common/ports/)

    파일 Protocol 책임 구현체
    user_command_gateway.py UserCommandGateway 사용자 쓰기 작업 SqlaUserDataMapper
    user_query_gateway.py UserQueryGateway 사용자 읽기 작업 SqlaUserReader
    flusher.py Flusher DB 변경사항 플러시 SqlaMainFlusher
    transaction_manager.py TransactionManager 트랜잭션 커밋/롤백 SqlaMainTransactionManager
    identity_provider.py IdentityProvider 현재 사용자 ID 제공 AuthSessionIdentityProvider
    access_revoker.py AccessRevoker 사용자 접근 취소 AuthSessionAccessRevoker

    Services (application/common/services/)

    파일 역할 의존성
    current_user.py CurrentUserService (현재 로그인 사용자 조회) IdentityProvider, UserCommandGateway
    constants.py 애플리케이션 상수 없음

    Authorization (application/common/services/authorization/)

    파일 역할 의존성
    base.py Permission Protocol 정의 없음
    authorize.py authorize() 헬퍼 함수 AuthorizationError
    permissions.py 구체적 Permission 구현들 (CanManageRole 등) base.py
    composite.py AllOf, AnyOf 복합 Permission base.py
    role_hierarchy.py 역할 계층 정의 UserRole

    Query Params (application/common/query_params/)

    파일 역할 의존성
    offset_pagination.py OffsetPaginationParams 데이터클래스 없음
    sorting.py SortingParams, SortingOrder 없음

    Exceptions (application/common/exceptions/)

    파일 역할 의존성
    base.py ApplicationError 베이스 없음
    authorization.py AuthorizationError base.py
    query.py SortingError 등 base.py

    12.3 Infrastructure Layer (src/app/infrastructure/)

    인프라스트럭처 레이어는 모든 외부 시스템과의 연결을 담당합니다.

    Core Adapters (infrastructure/adapters/)

    파일 구현 대상 기술 의존성
    user_data_mapper_sqla.py UserCommandGateway SQLAlchemy application/common/ports/*
    user_reader_sqla.py UserQueryGateway SQLAlchemy application/common/ports/*
    main_flusher_sqla.py Flusher SQLAlchemy application/common/ports/*
    main_transaction_manager_sqla.py TransactionManager SQLAlchemy application/common/ports/*
    password_hasher_bcrypt.py PasswordHasher bcrypt domain/ports/*
    user_id_generator_uuid.py UserIdGenerator uuid domain/ports/*
    types.py 타입 앨리어스 (MainAsyncSession, HasherThreadPoolExecutor, HasherSemaphore) NewType SQLAlchemy, asyncio
    constants.py 에러 메시지 상수 - 없음

    Persistence (infrastructure/persistence_sqla/)

    경로 역할
    registry.py SQLAlchemy mapper_registry 설정
    mappings/all.py 모든 매핑 import 및 start_mappers()
    mappings/user.py User Entity ↔ users 테이블 매핑
    mappings/auth_session.py AuthSession ↔ auth_sessions 테이블 매핑
    alembic/env.py Alembic 마이그레이션 환경
    alembic/versions/*.py 데이터베이스 마이그레이션 스크립트

    Auth Module (infrastructure/auth/)

    handlers/ - 인증 비즈니스 로직 (Infrastructure 레벨):

    파일 역할 의존성
    log_in.py LogInHandler (로그인 처리) AuthSessionService, UserCommandGateway
    log_out.py LogOutHandler (로그아웃 처리) AuthSessionService
    sign_up.py SignUpHandler (회원가입 처리) UserService, UserCommandGateway
    change_password.py ChangePasswordHandler UserService, UserCommandGateway
    constants.py 에러 메시지 상수 없음

    adapters/ - Auth 전용 어댑터:

    파일 구현 대상 역할
    identity_provider.py IdentityProvider 세션에서 현재 사용자 ID 추출
    access_revoker.py AccessRevoker 사용자의 모든 세션 삭제
    data_mapper_sqla.py AuthSessionGateway AuthSession CRUD
    transaction_manager_sqla.py AuthTransactionManager Auth DB 트랜잭션
    types.py 타입 앨리어스 AuthAsyncSession 등

    session/ - 세션 관리:

    파일 역할
    model.py AuthSession 모델 (Infrastructure 레벨 엔티티)
    service.py AuthSessionService (세션 생성, 검증, 갱신)
    id_generator_str.py 문자열 세션 ID 생성기
    timer_utc.py UTC 시간 제공자

    session/ports/ - Infrastructure 내부 Port:

    파일 Protocol 구현체
    gateway.py AuthSessionGateway SqlaAuthSessionDataMapper
    transaction_manager.py AuthSessionTransactionManager SqlaAuthSessionTransactionManager
    transport.py AuthSessionTransport JwtCookieAuthSessionTransport

    exceptions.py - Auth 모듈 예외:

    예외 용도
    AuthenticationError 인증 실패
    AlreadyAuthenticatedError 이미 인증됨
    ReAuthenticationError 재인증 필요
    AuthenticationChangeError 인증 변경 실패

    Infrastructure Exceptions (infrastructure/exceptions/)

    파일 역할
    base.py InfrastructureError 베이스
    gateway.py DataMapperError, ReaderError
    password_hasher.py PasswordHasherError

    12.4 Presentation Layer (src/app/presentation/)

    프레젠테이션 레이어는 HTTP 요청/응답 변환을 담당합니다.

    Controllers (presentation/http/controllers/)

    account/ - 계정 관련 엔드포인트:

    파일 엔드포인트 Use Case/Handler
    sign_up.py POST /account/sign-up SignUpHandler
    log_in.py POST /account/log-in LogInHandler
    log_out.py POST /account/log-out LogOutHandler
    change_password.py POST /account/change-password ChangePasswordHandler
    router.py account_router 정의 -

    users/ - 사용자 관리 엔드포인트:

    파일 엔드포인트 Use Case
    create_user.py POST /users CreateUserInteractor
    list_users.py GET /users ListUsersQueryService
    activate_user.py POST /users/{id}/activate ActivateUserInteractor
    deactivate_user.py POST /users/{id}/deactivate DeactivateUserInteractor
    grant_admin.py POST /users/{id}/grant-admin GrantAdminInteractor
    revoke_admin.py POST /users/{id}/revoke-admin RevokeAdminInteractor
    set_user_password.py POST /users/{id}/set-password SetUserPasswordInteractor
    router.py users_router 정의 -

    general/ - 일반 엔드포인트:

    파일 엔드포인트 역할
    health.py GET /health Health check
    router.py general_router 정의 -

    라우터 구조:

    파일 역할
    root_router.py 최상위 라우터 (general 포함)
    api_v1_router.py /api/v1 프리픽스 라우터 (account, users 포함)

    Auth HTTP Components (presentation/http/auth/)

    파일 역할
    access_token_processor_jwt.py JWT 토큰 생성/검증
    asgi_middleware.py AuthMiddleware (요청마다 세션 검증)
    cookie_params.py 쿠키 설정 (httponly, secure 등)
    openapi_marker.py OpenAPI 보안 스키마 마커
    constants.py JWT 관련 상수

    adapters/ - Presentation 전용 어댑터:

    파일 구현 대상 역할
    session_transport_jwt_cookie.py SessionTransport JWT를 HTTP 쿠키로 전송

    Error Handling (presentation/http/errors/)

    파일 역할
    translators.py 도메인/애플리케이션 예외 → HTTP 응답 변환
    callbacks.py FastAPI exception_handler 콜백

    12.5 Setup Layer (src/app/setup/)

    애플리케이션 부트스트랩 및 설정을 담당합니다.

    Config (setup/config/)

    파일 역할
    loader.py TOML 설정 파일 로딩
    settings.py Settings 통합 dataclass
    database.py DatabaseConfig (connection string 등)
    security.py SecurityConfig (JWT 키, 세션 TTL 등)
    logs.py LogsConfig (로그 레벨, 포맷)

    IoC Container (setup/ioc/)

    Dishka DI 컨테이너 Provider 정의:

    파일 역할 등록 대상
    settings.py 설정 Provider Settings, DatabaseConfig 등
    domain.py Domain Provider UserService, PasswordHasher, UserIdGenerator
    application.py Application Provider Command/Query Use Cases, CurrentUserService
    infrastructure.py Infrastructure Provider Gateway, Flusher, Session 등
    presentation.py Presentation Provider AccessTokenProcessor, SessionTransport
    provider_registry.py 모든 Provider 통합 Container 생성

    Dishka Scope 전략: (예제 프로젝트 참고용 - 우리 프로젝트에는 미적용)

    Scope 생명주기 사용 대상
    Scope.APP 애플리케이션 전체 AsyncEngine, SessionMaker, ThreadPoolExecutor, Settings
    Scope.REQUEST HTTP 요청 단위 AsyncSession, Use Cases, Handlers

    ⚠️ Note: 우리 프로젝트에서는 FastAPI의 기본 Depends() 패턴을 사용하며, Dishka DI 컨테이너는 적용하지 않습니다.

    App Factory (setup/app_factory.py)

    def create_app() -> FastAPI:
        # 1. Settings 로드
        # 2. Logging 설정
        # 3. SQLAlchemy Mapper 시작
        # 4. Dishka Container 생성
        # 5. FastAPI 앱 생성
        # 6. Middleware 추가
        # 7. Router 등록
        # 8. Exception Handler 등록
        return app

    12.6 Entry Point

    파일 역할
    src/app/run.py uvicorn 실행 진입점

    12.7 Tests (tests/)

    경로 역할
    tests/app/unit/domain/ Domain 레이어 단위 테스트
    tests/app/unit/application/ Application 레이어 단위 테스트
    tests/app/unit/infrastructure/ Infrastructure 레이어 단위 테스트
    tests/app/unit/setup/ Setup 단위 테스트
    tests/app/unit/factories/ 테스트 팩토리 (엔티티, VO 생성)
    tests/app/integration/ 통합 테스트
    tests/app/performance/ 성능 프로파일링 테스트

    12.8 의존성 흐름 요약

    ┌─────────────────────────────────────────────────────────────────────────────┐
    │                              DEPENDENCIES FLOW                               │
    ├─────────────────────────────────────────────────────────────────────────────┤
    │                                                                              │
    │  presentation/http/controllers/users/create_user.py                          │
    │       │                                                                      │
    │       ├── Depends(Dishka) ──────────────────────────────────────┐           │
    │       │                                                          │           │
    │       ▼                                                          │           │
    │  application/commands/create_user.py (CreateUserInteractor)      │           │
    │       │                                                          │           │
    │       ├── UserService           ◄── domain/services/user.py     │           │
    │       ├── UserCommandGateway    ◄── application/common/ports/   │ Dishka    │
    │       ├── Flusher               ◄── application/common/ports/   │ IoC       │
    │       └── TransactionManager    ◄── application/common/ports/   │ Container │
    │                                                                  │           │
    │       구현체 주입:                                                │           │
    │       ├── UserService           ◄── setup/ioc/domain.py         │           │
    │       ├── SqlaUserDataMapper    ◄── setup/ioc/infrastructure.py │           │
    │       ├── SqlaMainFlusher       ◄── setup/ioc/infrastructure.py │           │
    │       └── SqlaMainTxManager     ◄── setup/ioc/infrastructure.py │           │
    │                                                                  │           │
    │                                                                  ▼           │
    │  domain/services/user.py (UserService)                                       │
    │       │                                                                      │
    │       ├── PasswordHasher        ◄── domain/ports/                           │
    │       └── UserIdGenerator       ◄── domain/ports/                           │
    │                                                                              │
    │       구현체:                                                                 │
    │       ├── BcryptPasswordHasher  ◄── infrastructure/adapters/                │
    │       └── UuidUserIdGenerator   ◄── infrastructure/adapters/                │
    │                                                                              │
    └─────────────────────────────────────────────────────────────────────────────┘

    13. 참고 자료

    원본 자료 (Primary Sources)

    아키텍처:

    패턴 카탈로그:

    핵심 서적

    저자 연도 별칭
    Domain-Driven Design: Tackling Complexity in the Heart of Software Eric Evans 2003 Blue Book
    Implementing Domain-Driven Design Vaughn Vernon 2013 Red Book
    Patterns of Enterprise Application Architecture Martin Fowler 2002 PoEAA
    Clean Architecture: A Craftsman's Guide to Software Structure and Design Robert C. Martin 2017 -
    Clean Code: A Handbook of Agile Software Craftsmanship Robert C. Martin 2008 -

    예제 프로젝트

    추가 학습 자료

    댓글

ABOUT ME

🎓 부산대학교 정보컴퓨터공학과 학사: 2017.03 - 2023.08
☁️ Rakuten Symphony Jr. Cloud Engineer: 2024.12.09 - 2025.08.31
🏆 2025 AI 새싹톤 우수상 수상: 2025.10.30 - 2025.12.02
🌏 이코에코(Eco²) 백엔드/인프라 고도화 중: 2025.12 - Present

Designed by Mango