클린 아키텍처를 위한 파이썬 디자인 패턴

SOLID 설계 패턴을 사용하여 유지보수가 쉬운 Python 앱을 구축하세요.

클린 아키텍처는 관심사 분리와 의존성 관리에 중점을 두어 개발자가 확장성 있고 유지보수가 쉬운 애플리케이션을 구축하는 방식을 혁신적으로 바꾸었습니다.

파이썬에서는 이러한 원칙이 언어의 동적 특성과 결합하여 비즈니스 요구사항에 따라 변화하는 유연하고 테스트 가능한 시스템을 만들어내며, 기술 부채가 되지 않도록 합니다.

활기찬 기술 컨퍼런스 홀

파이썬에서의 클린 아키텍처 이해

클린 아키텍처는 로버트 C. 마틴(유니클로 보브)이 도입한 것으로, 소프트웨어를 중심으로 한 층으로 구성하며, 의존성은 핵심 비즈니스 로직을 향해 내부로 가리킵니다. 이 아키텍처 패턴은 애플리케이션의 핵심 비즈니스 규칙이 프레임워크, 데이터베이스, 외부 서비스와 독립적으로 유지되도록 보장합니다.

핵심 철학

기본 원칙은 간단하지만 강력합니다: 비즈니스 로직은 인프라에 의존해서는 안 됩니다. 도메인 엔티티, 사용 사례, 비즈니스 규칙은 PostgreSQL이나 MongoDB, FastAPI나 Flask, AWS나 Azure를 사용하든간에 작동해야 합니다.

파이썬에서는 이 철학이 언어의 “덕 타이핑"과 프로토콜 중심 프로그래밍과 완벽하게 일치하며, 정적 타이핑 언어에서 요구되는 의식적인 분리 없이 깨끗한 분리를 가능하게 합니다.

클린 아키텍처의 네 가지 층

엔티티 층 (도메인): 기업 전체에 걸쳐 있는 비즈니스 규칙을 가진 순수한 비즈니스 객체입니다. 이들은 외부 의존성이 없는 POJO(Plain Old Python Objects)입니다.

사용 사례 층 (애플리케이션): 엔티티와 외부 서비스 사이의 데이터 흐름을 조정하는 애플리케이션 특정 비즈니스 규칙입니다.

인터페이스 어댑터 층: 사용 사례와 엔티티에 가장 편리한 형식과 외부 기관이 요구하는 형식 사이를 데이터를 변환합니다.

프레임워크 및 드라이버 층: 데이터베이스, 웹 프레임워크, 외부 API와 같은 모든 외부 세부 사항이 포함됩니다.

파이썬에서의 SOLID 원칙

SOLID 원칙은 클린 아키텍처의 기초를 형성합니다. 각 원칙이 파이썬에서 어떻게 나타나는지 살펴보겠습니다. 디자인 패턴에 대한 포괄적인 개요를 원하시면 파이썬 디자인 패턴 가이드를 참조하십시오.

단일 책임 원칙 (SRP)

각 클래스는 변경의 한 가지 이유를 가져야 합니다:

# 나쁜 예: 여러 책임
class UserManager:
    def create_user(self, user_data):
        # 사용자 생성
        pass
    
    def send_welcome_email(self, user):
        # 이메일 전송
        pass
    
    def log_creation(self, user):
        # 파일에 로그 기록
        pass

# 좋은 예: 책임 분리
class UserService:
    def __init__(self, repository, email_service, logger):
        self.repository = repository
        self.email_service = email_service
        self.logger = logger
    
    def create_user(self, user_data):
        user = User(**user_data)
        self.repository.save(user)
        self.email_service.send_welcome(user)
        self.logger.info(f"사용자 생성됨: {user.id}")
        return user

개방/폐쇄 원칙 (OCP)

소프트웨어 엔티티는 확장에는 열려 있어야 하지만 수정에는 닫혀 있어야 합니다:

from abc import ABC, abstractmethod
from typing import Protocol

# 프로토콜 사용 (Python 3.8+)
class PaymentProcessor(Protocol):
    def process_payment(self, amount: float) -> bool:
        ...

class CreditCardProcessor:
    def process_payment(self, amount: float) -> bool:
        # 신용카드 로직
        return True

class PayPalProcessor:
    def process_payment(self, amount: float) -> bool:
        # PayPal 로직
        return True

# 기존 코드를 수정하지 않고 쉽게 확장 가능
class CryptoProcessor:
    def process_payment(self, amount: float) -> bool:
        # 암호화폐 로직
        return True

리스코프 치환 원칙 (LSP)

객체는 그 하위 타입으로 대체될 수 있어야 하며, 프로그램이 깨지지 않아야 합니다:

from abc import ABC, abstractmethod

class DataStore(ABC):
    @abstractmethod
    def save(self, key: str, value: str) -> None:
        pass
    
    @abstractmethod
    def get(self, key: str) -> str:
        pass

class PostgreSQLStore(DataStore):
    def save(self, key: str, value: str) -> None:
        # PostgreSQL 구현
        pass
    
    def get(self, key: str) -> str:
        # PostgreSQL 구현
        return ""

class RedisStore(DataStore):
    def save(self, key: str, value: str) -> None:
        # Redis 구현
        pass
    
    def get(self, key: str) -> str:
        # Redis 구현
        return ""

# 둘 다 대체 가능
def process_data(store: DataStore, key: str, value: str):
    store.save(key, value)
    return store.get(key)

인터페이스 분리 원칙 (ISP)

클라이언트는 사용하지 않는 인터페이스에 의존해서는 안 됩니다:

# 나쁜 예: 두꺼운 인터페이스
class Worker(ABC):
    @abstractmethod
    def work(self): pass
    
    @abstractmethod
    def eat(self): pass
    
    @abstractmethod
    def sleep(self): pass

# 좋은 예: 분리된 인터페이스
class Workable(Protocol):
    def work(self) -> None: ...

class Eatable(Protocol):
    def eat(self) -> None: ...

class Human:
    def work(self) -> None:
        print("작업 중")
    
    def eat(self) -> None:
        print("식사 중")

class Robot:
    def work(self) -> None:
        print("작업 중")
    # eat 메서드 필요 없음

의존성 역전 원칙 (DIP)

고수준 모듈은 저수준 모듈에 의존해서는 안 됩니다. 둘 다 추상화에 의존해야 합니다:

from typing import Protocol

# 추상화
class EmailSender(Protocol):
    def send(self, to: str, subject: str, body: str) -> None:
        ...

# 저수준 모듈
class SMTPEmailSender:
    def send(self, to: str, subject: str, body: str) -> None:
        # SMTP 구현
        pass

# 고수준 모듈은 추상화에 의존
class UserRegistrationService:
    def __init__(self, email_sender: EmailSender):
        self.email_sender = email_sender
    
    def register(self, email: str, name: str):
        # 등록 로직
        self.email_sender.send(
            to=email,
            subject="환영합니다!",
            body=f"안녕하세요 {name}"
        )

리포지토리 패턴: 데이터 접근 추상화

리포지토리 패턴은 도메인 객체에 대한 컬렉션처럼 인터페이스를 제공하며, 데이터 저장의 세부 사항을 숨깁니다.

기본 리포지토리 구현

from abc import ABC, abstractmethod
from typing import List, Optional
from dataclasses import dataclass
from uuid import UUID, uuid4

@dataclass
class User:
    id: UUID
    email: str
    name: str
    is_active: bool = True

class UserRepository(ABC):
    @abstractmethod
    def save(self, user: User) -> User:
        pass
    
    @abstractmethod
    def get_by_id(self, user_id: UUID) -> Optional[User]:
        pass
    
    @abstractmethod
    def get_by_email(self, email: str) -> Optional[用户]:
        pass
    
    @abstractmethod
    def list_all(self) -> List[User]:
        pass
    
    @abstractmethod
    def delete(self, user_id: UUID) -> bool:
        pass

SQLAlchemy 구현

from sqlalchemy import create_engine, Column, String, Boolean
from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session

Base = declarative_base()

class UserModel(Base):
    __tablename__ = 'users'
    
    id = Column(PGUUID(as_uuid=True), primary_key=True)
    email = Column(String, unique=True, nullable=False)
    name = Column(String, nullable=False)
    is_active = Column(Boolean, default=True)

class SQLAlchemyUserRepository(UserRepository):
    def __init__(self, session: Session):
        self.session = session
    
    def save(self, user: User) -> User:
        user_model = UserModel(
            id=user.id,
            email=user.email,
            name=user.name,
            is_active=user.is_active
        )
        self.session.add(user_model)
        self.session.commit()
        return user
    
    def get_by_id(self, user_id: UUID) -> Optional[User]:
        user_model = self.session.query(UserModel).filter(
            UserModel.id == user_id
        ).first()
        
        if not user_model:
            return None
        
        return User(
            id=user_model.id,
            email=user_model.email,
            name=user_model.name,
            is_active=user_model.is_active
        )
    
    def get_by_email(self, email: str) -> Optional[User]:
        user_model = self.session.query(UserModel).filter(
            UserModel.email == email
        ).first()
        
        if not user_model:
            return None
        
        return User(
            id=user_model.id,
            email=user_model.email,
            name=user_model.name,
            is_active=user_model.is_active
        )
    
    def list_all(self) -> List[User]:
        users = self.session.query(UserModel).all()
        return [
            User(
                id=u.id,
                email=u.email,
                name=u.name,
                is_active=u.is_active
            )
            for u in users
        ]
    
    def delete(self, user_id: UUID) -> bool:
        result = self.session.query(UserModel).filter(
            UserModel.id == user_id
        ).delete()
        self.session.commit()
        return result > 0

테스트용 인메모리 리포지토리

class InMemoryUserRepository(UserRepository):
    def __init__(self):
        self.users: dict[UUID, User] = {}
    
    def save(self, user: User) -> User:
        self.users[user.id] = user
        return user
    
    def get_by_id(self, user_id: UUID) -> Optional[User]:
        return self.users.get(user_id)
    
    def get_by_email(self, email: str) -> Optional[User]:
        for user in self.users.values():
            if user.email == email:
                return user
        return None
    
    def list_all(self) -> List[User]:
        return list(self.users.values())
    
    def delete(self, user_id: UUID) -> bool:
        if user_id in self.users:
            del self.users[user_id]
            return True
        return False

서비스 층: 비즈니스 로직 조정

서비스 층은 사용 사례를 구현하고 리포지토리, 외부 서비스, 도메인 로직 사이의 흐름을 조정합니다.

from typing import Optional
from uuid import uuid4

class UserAlreadyExistsError(Exception):
    pass

class UserNotFoundError(Exception):
    pass

class UserService:
    def __init__(
        self,
        user_repository: UserRepository,
        email_service: EmailSender,
        event_publisher: 'EventPublisher'
    ):
        self.user_repository = user_repository
        self.email_service = email_service
        self.event_publisher = event_publisher
    
    def register_user(self, email: str, name: str) -> User:
        # 사용자 존재 여부 확인
        existing_user = self.user_repository.get_by_email(email)
        if existing_user:
            raise UserAlreadyExistsError(f"이메일 {email}을 가진 사용자가 이미 존재합니다")
        
        # 새 사용자 생성
        user = User(
            id=uuid4(),
            email=email,
            name=name,
            is_active=True
        )
        
        # 리포지토리에 저장
        user = self.user_repository.save(user)
        
        # 환영 이메일 전송
        self.email_service.send(
            to=user.email,
            subject="환영합니다!",
            body=f"안녕하세요 {user.name}, 저희 플랫폼에 오신 것을 환영합니다!"
        )
        
        # 이벤트 게시
        self.event_publisher.publish('user.registered', {
            'user_id': str(user.id),
            'email': user.email
        })
        
        return user
    
    def deactivate_user(self, user_id: UUID) -> User:
        user = self.user_repository.get_by_id(user_id)
        if not user:
            raise UserNotFoundError(f"사용자 {user_id}를 찾을 수 없습니다")
        
        user.is_active = False
        user = self.user_repository.save(user)
        
        self.event_publisher.publish('user.deactivated', {
            'user_id': str(user.id)
        })
        
        return user

파이썬에서의 의존성 주입

파이썬의 동적 특성은 중간 프레임워크 없이 의존성 주입을 간단하게 수행할 수 있도록 합니다.

생성자 주입

class OrderService:
    def __init__(
        self,
        order_repository: 'OrderRepository',
        payment_processor: PaymentProcessor,
        notification_service: 'NotificationService'
    ):
        self.order_repository = order_repository
        self.payment_processor = payment_processor
        self.notification_service = notification_service
    
    def place_order(self, order_data: dict):
        # 주입된 의존성 사용
        pass

간단한 의존성 컨테이너

from typing import Dict, Type, Callable, Any

class Container:
    def __init__(self):
        self._services: Dict[Type, Callable] = {}
        self._singletons: Dict[Type, Any] = {}
    
    def register(self, interface: Type, factory: Callable):
        self._services[interface] = factory
    
    def register_singleton(self, interface: Type, instance: Any):
        self._singletons[interface] = instance
    
    def resolve(self, interface: Type):
        if interface in self._singletons:
            return self._singletons[interface]
        
        factory = self._services.get(interface)
        if factory:
            return factory(self)
        
        raise ValueError(f"{interface}에 대한 등록을 찾을 수 없습니다")

# 사용법
def create_container() -> Container:
    container = Container()
    
    # 서비스 등록
    container.register_singleton(
        Session,
        sessionmaker(bind=create_engine('postgresql://...'))()
    )
    
    container.register(
        UserRepository,
        lambda c: SQLAlchemyUserRepository(c.resolve(Session))
    )
    
    container.register(
        EmailSender,
        lambda c: SMTPEmailSender()
    )
    
    container.register(
        UserService,
        lambda c: UserService(
            c.resolve(UserRepository),
            c.resolve(EmailSender),
            c.resolve(EventPublisher)
        )
    )
    
    return container

헥사곤 아키텍처 (포트 및 어댑터)

헥사곤 아키텍처는 외부 통신을 처리하는 어댑터와 중심에 있는 비즈니스 로직을 배치합니다.

포트 정의 (인터페이스)

# 입력 포트 (주요)
class CreateUserUseCase(Protocol):
    def execute(self, request: 'CreateUserRequest') -> 'CreateUserResponse':
        ...

# 출력 포트 (보조)
class UserPersistencePort(Protocol):
    def save(self, user: User) -> User:
        ...
    
    def find_by_email(self, email: str) -> Optional[User]:
        ...

어댑터 구현

from pydantic import BaseModel, EmailStr

# 입력 어댑터 (REST API)
from fastapi import FastAPI, Depends, HTTPException

class CreateUserRequest(BaseModel):
    email: EmailStr
    name: str

class CreateUserResponse(BaseModel):
    id: str
    email: str
    name: str

app = FastAPI()

@app.post("/users", response_model=CreateUserResponse)
def create_user(
    request: CreateUserRequest,
    user_service: UserService = Depends(get_user_service)
):
    try:
        user = user_service.register_user(
            email=request.email,
            name=request.name
        )
        return CreateUserResponse(
            id=str(user.id),
            email=user.email,
            name=user.name
        )
    except UserAlreadyExistsError as e:
        raise HTTPException(status_code=400, detail=str(e))

# 출력 어댑터 (데이터베이스)
# SQLAlchemyUserRepository로 이미 구현됨

도메인 주도 설계 패턴

값 객체

속성에 의해 정의된 불변 객체:

from dataclasses import dataclass
from typing import Pattern
import re

@dataclass(frozen=True)
class Email:
    value: str
    
    EMAIL_PATTERN: Pattern = re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
    
    def __post_init__(self):
        if not self.EMAIL_PATTERN.match(self.value):
            raise ValueError(f"잘못된 이메일: {self.value}")
    
    def __str__(self):
        return self.value

@dataclass(frozen=True)
class Money:
    amount: float
    currency: str
    
    def __post_init__(self):
        if self.amount < 0:
            raise ValueError("금액은 음수일 수 없습니다")
        if self.currency not in ['USD', 'EUR', 'GBP']:
            raise ValueError(f"지원되지 않는 통화: {self.currency}")
    
    def add(self, other: 'Money') -> 'Money':
        if self.currency != other.currency:
            raise ValueError("다른 통화를 더할 수 없습니다")
        return Money(self.amount + other.amount, self.currency)

집합체

도메인 객체의 클러스터로, 단일 단위로 처리됩니다:

from dataclasses import dataclass, field
from typing import List
from datetime import datetime

@dataclass
class OrderItem:
    product_id: UUID
    quantity: int
    price: Money
    
    def total(self) -> Money:
        return Money(
            self.price.amount * self.quantity,
            self.price.currency
        )

@dataclass
class Order:
    id: UUID
    customer_id: UUID
    items: List[OrderItem] = field(default_factory=list)
    status: str = "pending"
    created_at: datetime = field(default_factory=datetime.now)
    
    def add_item(self, product_id: UUID, quantity: int, price: Money):
        item = OrderItem(product_id, quantity, price)
        self.items.append(item)
    
    def remove_item(self, product_id: UUID):
        self.items = [
            item for item in self.items 
            if item.product_id != product_id
        ]
    
    def total(self) -> Money:
        if not self.items:
            return Money(0, "USD")
        
        return sum(
            (item.total() for item in self.items),
            Money(0, self.items[0].price.currency)
        )
    
    def confirm(self):
        if not self.items:
            raise ValueError("빈 주문은 확인할 수 없습니다")
        if self.status != "pending":
            raise ValueError("이미 처리된 주문입니다")
        self.status = "confirmed"

도메인 이벤트

도메인 이벤트는 구성 요소 간 느슨한 결합을 가능하게 하며 이벤트 기반 아키텍처를 지원합니다. 프로덕션 규모의 이벤트 기반 시스템을 구현하려면 AWS Kinesis와 같은 서비스를 사용하여 이벤트 스트리밍을 고려하십시오—AWS Kinesis를 사용한 이벤트 기반 마이크로서비스 구축를 참조하십시오.

from dataclasses import dataclass
from datetime import datetime
from typing import List, Callable

@dataclass
class DomainEvent:
    occurred_at: datetime = field(default_factory=datetime.now)

@dataclass
class OrderConfirmed(DomainEvent):
    order_id: UUID
    customer_id: UUID
    total: Money

class EventPublisher:
    def __init__(self):
        self._handlers: Dict[Type, List[Callable]] = {}
    
    def subscribe(self, event_type: Type, handler: Callable):
        if event_type not in self._handlers:
            self._handlers[event_type] = []
        self._handlers[event_type].append(handler)
    
    def publish(self, event: DomainEvent):
        event_type = type(event)
        handlers = self._handlers.get(event_type, [])
        for handler in handlers:
            handler(event)

현대 Python 기능과 깔끔한 아키텍처

Python의 현대적인 기능은 깔끔한 아키텍처를 구현하는 데 더 우아하고 타입 안전하게 도움을 줍니다. Python 문법과 기능에 대한 빠른 참고가 필요하다면 Python Cheatsheet를 확인해 보세요.

타입 힌트와 프로토콜

from typing import Protocol, runtime_checkable

@runtime_checkable
class Serializable(Protocol):
    def to_dict(self) -> dict:
        ...
    
    @classmethod
    def from_dict(cls, data: dict) -> 'Serializable':
        ...

def serialize(obj: Serializable) -> dict:
    return obj.to_dict()

Pydantic을 이용한 검증

from pydantic import BaseModel, Field, validator
from typing import Optional

class CreateUserDTO(BaseModel):
    email: EmailStr
    name: str = Field(..., min_length=2, max_length=100)
    age: Optional[int] = Field(None, ge=0, le=150)
    
    @validator('name')
    def name_must_not_contain_numbers(cls, v):
        if any(char.isdigit() for char in v):
            raise ValueError('Name cannot contain numbers')
        return v
    
    class Config:
        frozen = True  # 불변으로 만듭니다

I/O 작업을 위한 Async/Await

Python의 async/await 문법은 깔끔한 아키텍처에서 I/O 경계 작업에 특히 강력하며, 데이터베이스와 외부 서비스와의 비차단 상호작용을 가능하게 합니다. Python 애플리케이션을 서버리스 플랫폼에 배포할 때 성능 특성에 대한 이해가 중요합니다—서버리스 애플리케이션을 AWS에서 JavaScript, Python, Golang을 사용하여 개발하는 방법에 대한 통찰을 얻기 위해 AWS lambda 성능: JavaScript vs Python vs Golang을 참조하세요.

from typing import List
import asyncio

class AsyncUserRepository(ABC):
    @abstractmethod
    async def save(self, user: User) -> User:
        pass
    
    @abstractmethod
    async def get_by_id(self, user_id: UUID) -> Optional[User]:
        pass

class AsyncUserService:
    def __init__(self, repository: AsyncUserRepository):
        self.repository = repository
    
    async def register_user(self, email: str, name: str) -> User:
        user = User(id=uuid4(), email=email, name=name)
        return await self.repository.save(user)
    
    async def get_users_batch(self, user_ids: List[UUID]) -> List[User]:
        tasks = [self.repository.get_by_id(uid) for uid in user_ids]
        results = await asyncio.gather(*tasks)
        return [u for u in results if u is not None]

프로젝트 구조 최선의 실천

깔끔한 아키텍처를 유지하기 위해 적절한 프로젝트 조직은 필수적입니다. 프로젝트 구조를 설정하기 전에 의존성 분리에 사용되는 Python 가상 환경을 사용하는 것을 확실히 해야 합니다. venv Cheatsheet는 가상 환경을 관리하는 데 필요한 모든 내용을 다룹니다. 현대적인 Python 프로젝트를 위해 uv - 새로운 Python 패키지, 프로젝트 및 환경 관리자를 고려해 보세요. 이는 더 빠른 패키지 관리 및 프로젝트 설정을 제공합니다.

my_application/
├── domain/                 # 기업 비즈니스 규칙
│   ├── __init__.py
│   ├── entities/
│   │   ├── __init__.py
│   │   ├── user.py
│   │   └── order.py
│   ├── value_objects/
│   │   ├── __init__.py
│   │   ├── email.py
│   │   └── money.py
│   ├── events/
│   │   ├── __init__.py
│   │   └── user_events.py
│   └── exceptions.py
├── application/            # 애플리케이션 비즈니스 규칙
│   ├── __init__.py
│   ├── use_cases/
│   │   ├── __init__.py
│   │   ├── create_user.py
│   │   └── place_order.py
│   ├── services/
│   │   ├── __init__.py
│   │   └── user_service.py
│   └── ports/
│       ├── __init__.py
│       ├── repositories.py
│       └── external_services.py
├── infrastructure/         # 외부 인터페이스
│   ├── __init__.py
│   ├── persistence/
│   │   ├── __init__.py
│   │   ├── sqlalchemy/
│   │   │   ├── models.py
│   │   │   └── repositories.py
│   │   └── mongodb/
│   │       └── repositories.py
│   ├── messaging/
│   │   ├── __init__.py
│   │   └── rabbitmq_publisher.py
│   ├── external_services/
│   │   ├── __init__.py
│   │   └── email_service.py
│   └── config.py
├── presentation/           # UI/API 계층
│   ├── __init__.py
│   ├── api/
│   │   ├── __init__.py
│   │   ├── dependencies.py
│   │   ├── routes/
│   │   │   ├── __init__.py
│   │   │   ├── users.py
│   │   │   └── orders.py
│   │   └── schemas/
│   │       ├── __init__.py
│   │       └── user_schemas.py
│   └── cli/
│       └── commands.py
├── tests/
│   ├── unit/
│   ├── integration/
│   └── e2e/
├── main.py                 # 애플리케이션 진입점
├── container.py            # 의존성 주입 설정
├── pyproject.toml
└── README.md

깔끔한 아키텍처 테스트

도메인 논리의 단위 테스트

import pytest
from uuid import uuid4

def test_user_creation():
    user = User(
        id=uuid4(),
        email="test@example.com",
        name="Test User"
    )
    assert user.email == "test@example.com"
    assert user.is_active is True

def test_order_total_calculation():
    order = Order(id=uuid4(), customer_id=uuid4())
    order.add_item(
        uuid4(),
        quantity=2,
        price=Money(10.0, "USD")
    )
    order.add_item(
        uuid4(),
        quantity=1,
        price=Money(5.0, "USD")
    )
    assert order.total().amount == 25.0

저장소와의 통합 테스트

@pytest.fixture
def in_memory_repository():
    return InMemoryUserRepository()

def test_user_repository_save_and_retrieve(in_memory_repository):
    user = User(
        id=uuid4(),
        email="test@example.com",
        name="Test User"
    )
    
    saved_user = in_memory_repository.save(user)
    retrieved_user = in_memory_repository.get_by_id(user.id)
    
    assert retrieved_user is not None
    assert retrieved_user.email == user.email

서비스 계층 테스트

from unittest.mock import Mock

def test_user_registration():
    # 준비
    mock_repository = Mock(spec=UserRepository)
    mock_repository.get_by_email.return_value = None
    mock_repository.save.return_value = User(
        id=uuid4(),
        email="test@example.com",
        name="Test"
    )
    
    mock_email = Mock(spec=EmailSender)
    mock_events = Mock(spec=EventPublisher)
    
    service = UserService(mock_repository, mock_email, mock_events)
    
    # 실행
    user = service.register_user("test@example.com", "Test")
    
    # 확인
    assert user.email == "test@example.com"
    mock_repository.save.assert_called_once()
    mock_email.send.assert_called_once()
    mock_events.publish.assert_called_once()

일반적인 함정과 피하는 방법

과도한 설계

간단한 CRUD 애플리케이션에 깔끔한 아키텍처를 구현하지 마세요. 간단하게 시작하고 복잡성이 증가할 때 리팩토링하세요.

누출된 추상화

도메인 엔티티가 데이터베이스 주석이나 프레임워크 특정 코드를 포함하지 않도록 보장하세요:

# 나쁜 예
from sqlalchemy import Column

@dataclass
class User:
    id: Column(Integer, primary_key=True)  # 프레임워크가 도메인에 누출됨

# 좋은 예
@dataclass
class User:
    id: UUID  # 순수한 도메인 객체

순환 의존성

인터페이스와 의존성 주입을 사용하여 계층 간 순환 의존성을 끊으세요.

맥락 무시

깔끔한 아키텍처는 모든 상황에 적용되는 것이 아닙니다. 프로젝트의 크기와 팀의 전문성에 따라 계층의 엄격성을 조정하세요.

유용한 링크