도메인 주도 설계(Domain-Driven Design, DDD)는 복잡한 비즈니스 도메인을 효과적으로 모델링하기 위한 접근 방식입니다. 이 글에서는 주문 시스템을 예제로 DDD의 핵심 개념과 구현 방법을 살펴보겠습니다. 특히 바운디드 컨텍스트, 애그리게이트, 엔티티, 값 객체 등의 개념이 실제 코드에서 어떻게 구현되는지 알아보겠습니다.
비즈니스 도메인의 개념, 규칙, 행동을 소프트웨어로 표현한 것입니다. 도메인 모델은 기술적 구현 세부사항보다 비즈니스 개념에 중점을 둡니다.
개발자와 도메인 전문가가 공통으로 사용하는 언어로, 코드와 대화에서 일관되게 사용됩니다.
특정 도메인 모델이 적용되는 명확한 경계를 정의합니다. 각 바운디드 컨텍스트 내에서는 용어와 규칙이 일관되게 적용됩니다.
고유한 식별자를 가지는 객체로, 보통 데이터베이스 테이블로 표현됩니다. 엔티티는 다른 속성이 변경되어도 식별자를 통해 동일한 객체로 인식됩니다.
트랜잭션 일관성을 유지해야 하는 엔티티와 객체의 집합으로, 애그리게이트 루트를 통해서만 접근할 수 있습니다.
애그리게이트의 지속성을 관리하는 객체로, 데이터 액세스 로직을 캡슐화합니다.
각 바운디드 컨텍스트 내에서 비즈니스 로직과 유스케이스를 구현하는 객체입니다.
DDD 기반 시스템을 설계할 때 일반적으로 다음과 같은 단계를 거칩니다:
도메인 탐색 및 지식 추출
바운디드 컨텍스트 식별
도메인 모델링
폴더 구조 설계
src/{컨텍스트}/
)세부 구현
테스트 및 검증
이제 주문 시스템 예제를 통해 각 단계를 자세히 살펴보겠습니다.
DDD에서 폴더 구조는 바운디드 컨텍스트를 명확하게 반영해야 합니다. 우리의 주문 시스템은 다음과 같은 바운디드 컨텍스트로 구성됩니다:
src/
├── order/ # 주문 바운디드 컨텍스트
│ ├── entity.py # 주문 엔티티 (Order, OrderItem)
│ ├── domain.py # 도메인 객체 (OrderStatus, 값 객체)
│ ├── repository.py # 주문 리포지토리
│ └── service.py # 주문 서비스
│
├── customer/ # 고객 바운디드 컨텍스트
│ ├── entity.py # 고객 엔티티
│ ├── domain.py # 도메인 객체
│ ├── repository.py # 고객 리포지토리
│ └── service.py # 고객 서비스
│
├── product/ # 상품 바운디드 컨텍스트
│ ├── entity.py # 상품 엔티티
│ ├── domain.py # 도메인 객체
│ ├── repository.py # 상품 리포지토리
│ └── service.py # 상품 서비스
│
└── infrastructure/ # 인프라스트럭처 계층
└── database.py # 데이터베이스 연결 및 트랜잭션 관리
이 구조에서 각 폴더(order/
, customer/
, product/
)는 하나의 바운디드 컨텍스트를 나타냅니다. 각 바운디드 컨텍스트는 독립적인 도메인 모델, 리포지토리, 서비스를 가지고 있습니다.
바운디드 컨텍스트는:
반면 애그리게이트(Aggregate)는:
Order
와 OrderItem
이 하나의 애그리게이트를 형성하며, Order
가 애그리게이트 루트입니다정리하자면:
order
, product
, customer
는 각각 바운디드 컨텍스트입니다order
바운디드 컨텍스트 안에는 Order
-OrderItem
애그리게이트가 있습니다. (Order
가 애그리게이트 루트 엔티티)product
바운디드 컨텍스트 안에는 Product
가 애그리게이트이자 동시에 루트 애그리게이트 엔티티로 존재합니다.customer
바운디드 컨텍스트 안에는 Customer
가 애그리게이트이자 동시에 루트 애그리게이트 엔티티로 존재합니다.이제 각 바운디드 컨텍스트의 도메인 모델을 구현해 보겠습니다. SQLAlchemy를 사용한 구현을 살펴보겠습니다.
# src/order/domain.py
import enum
from datetime import datetime
class OrderStatus(enum.Enum):
CREATED = "created"
PAID = "paid"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"
# src/order/entity.py
from sqlalchemy import Column, String, Float, Integer, ForeignKey, DateTime, Enum as SQLAlchemyEnum
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
import uuid
from datetime import datetime
from src.infrastructure.database import Base
from src.order.domain import OrderStatus
class OrderItem(Base):
__tablename__ = "order_items"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
order_id = Column(UUID(as_uuid=True), ForeignKey("orders.id"), nullable=False)
product_id = Column(UUID(as_uuid=True), ForeignKey("products.id"), nullable=False)
quantity = Column(Integer, nullable=False)
unit_price = Column(Float, nullable=False)
# Relationships
order = relationship("Order", back_populates="items")
product = relationship("Product")
@property
def subtotal(self) -> float:
return self.quantity * self.unit_price
class Order(Base):
__tablename__ = "orders"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
customer_id = Column(UUID(as_uuid=True), ForeignKey("customers.id"), nullable=False)
status = Column(SQLAlchemyEnum(OrderStatus), default=OrderStatus.CREATED)
created_at = Column(DateTime, default=datetime.now)
total_amount = Column(Float, default=0)
# Relationships
customer = relationship("Customer")
items = relationship("OrderItem", back_populates="order", cascade="all, delete-orphan")
def add_item(self, product_id: uuid.UUID, quantity: int, unit_price: float) -> None:
"""주문에 상품 항목 추가"""
item = OrderItem(
product_id=product_id,
quantity=quantity,
unit_price=unit_price
)
self.items.append(item)
self.recalculate_total()
def recalculate_total(self) -> None:
"""주문 총액 재계산"""
self.total_amount = sum(item.subtotal for item in self.items)
def change_status(self, status: OrderStatus) -> None:
"""주문 상태 변경"""
self.status = status
# src/customer/entity.py
from sqlalchemy import Column, String, Integer
from sqlalchemy.dialects.postgresql import UUID
import uuid
from src.infrastructure.database import Base
class Customer(Base):
__tablename__ = "customers"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column(String, nullable=False)
email = Column(String, nullable=False, unique=True)
address = Column(String, nullable=False)
points = Column(Integer, default=0)
def add_points(self, points: int) -> None:
"""고객 포인트 적립"""
self.points += points
# src/product/entity.py
from sqlalchemy import Column, String, Float, Integer
from sqlalchemy.dialects.postgresql import UUID
import uuid
from src.infrastructure.database import Base
class Product(Base):
__tablename__ = "products"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column(String, nullable=False)
price = Column(Float, nullable=False)
stock_quantity = Column(Integer, default=0)
def decrease_stock(self, quantity: int) -> bool:
"""상품 재고 감소, 성공 여부 반환"""
if self.stock_quantity >= quantity:
self.stock_quantity -= quantity
return True
return False
엔티티: Order
, Customer
, Product
는 고유 식별자를 가진 엔티티입니다. OrderStatus
는 Enum으로 표현된 상태입니다.
애그리게이트 경계: Order
와 OrderItem
은 하나의 애그리게이트를 형성합니다. Order
가 애그리게이트 루트이며, OrderItem
은 Order
를 통해서만 접근할 수 있습니다.
비즈니스 규칙 캡슐화: 각 엔티티는 자신의 비즈니스 규칙을 캡슐화합니다. 예를 들어, Product
의 decrease_stock
메서드는 재고가 충분한지 확인하는 규칙을 캡슐화합니다.
바운디드 컨텍스트 내 일관성: 각 바운디드 컨텍스트 내의 모델은 일관된 용어와 규칙을 사용합니다.
repository는 도메인 모델과 데이터 저장소 사이의 매개체 역할을 합니다. 각 바운디드 컨텍스트는 자체 리포지토리를 가집니다.
# src/order/repository.py
from typing import Optional
from uuid import UUID
from sqlalchemy.orm import Session
from src.order.entity import Order
class OrderRepository:
def __init__(self, session: Session):
self.session = session
def find_by_id(self, order_id: UUID) -> Optional[Order]:
"""ID로 주문 조회"""
return self.session.query(Order).filter(Order.id == order_id).first()
def save(self, order: Order) -> None:
"""주문 저장 또는 업데이트"""
if order.id is None or not self.session.query(Order.id).filter(Order.id == order.id).first():
self.session.add(order)
# session.commit()은 서비스 계층에서 처리
# src/customer/repository.py
from typing import Optional
from uuid import UUID
from sqlalchemy.orm import Session
from src.customer.entity import Customer
class CustomerRepository:
def __init__(self, session: Session):
self.session = session
def find_by_id(self, customer_id: UUID) -> Optional[Customer]:
"""ID로 고객 조회"""
return self.session.query(Customer).filter(Customer.id == customer_id).first()
def save(self, customer: Customer) -> None:
"""고객 정보 저장 또는 업데이트"""
if customer.id is None or not self.session.query(Customer.id).filter(Customer.id == customer.id).first():
self.session.add(customer)
# session.commit()은 서비스 계층에서 처리
# src/product/repository.py
from typing import List, Optional
from uuid import UUID
from sqlalchemy.orm import Session
from src.product.entity import Product
class ProductRepository:
def __init__(self, session: Session):
self.session = session
def find_by_id(self, product_id: UUID) -> Optional[Product]:
"""ID로 상품 조회"""
return self.session.query(Product).filter(Product.id == product_id).first()
def find_by_ids(self, product_ids: List[UUID]) -> List[Product]:
"""여러 ID로 상품 목록 조회"""
return self.session.query(Product).filter(Product.id.in_(product_ids)).all()
def save(self, product: Product) -> None:
"""상품 정보 저장 또는 업데이트"""
if product.id is None or not self.session.query(Product.id).filter(Product.id == product.id).first():
self.session.add(product)
# session.commit()은 서비스 계층에서 처리
서비스 계층은 도메인 모델이 직접 처리하기 어려운 비즈니스 로직을 담당합니다. 각 바운디드 컨텍스트는 자체 서비스를 가집니다.
# src/customer/service.py
from typing import Optional
from uuid import UUID
from src.customer.entity import Customer
from src.customer.repository import CustomerRepository
class CustomerService:
def __init__(self, repository: CustomerRepository):
self.repository = repository
def get_customer(self, customer_id: UUID) -> Optional[Customer]:
"""고객 정보 조회"""
return self.repository.find_by_id(customer_id)
def add_points(self, customer_id: UUID, points: int) -> bool:
"""고객 포인트 적립"""
customer = self.repository.find_by_id(customer_id)
if not customer:
return False
customer.add_points(points)
self.repository.save(customer)
return True
# src/product/service.py
from typing import List, Dict, Optional, Tuple
from uuid import UUID
from src.product.entity import Product
from src.product.repository import ProductRepository
class ProductService:
def __init__(self, repository: ProductRepository):
self.repository = repository
def get_products(self, product_ids: List[UUID]) -> List[Product]:
"""여러 상품 정보 조회"""
return self.repository.find_by_ids(product_ids)
def get_product(self, product_id: UUID) -> Optional[Product]:
"""단일 상품 정보 조회"""
return self.repository.find_by_id(product_id)
def check_and_decrease_stock_bulk(self, items: List[Dict]) -> Tuple[bool, List[Product]]:
"""여러 상품의 재고 확인 및 차감"""
product_ids = [item['product_id'] for item in items]
products = self.repository.find_by_ids(product_ids)
# 상품 ID별 매핑
product_map = {product.id: product for product in products}
# 모든 상품의 재고 확인
for item in items:
product = product_map.get(item['product_id'])
if not product or product.stock_quantity < item['quantity']:
return False, []
# 모든 상품의 재고 차감
for item in items:
product = product_map[item['product_id']]
product.decrease_stock(item['quantity'])
self.repository.save(product)
return True, list(product_map.values())
# src/order/service.py
from typing import List, Dict, Optional
from uuid import UUID
from src.infrastructure.database import transaction
from src.order.entity import Order, OrderStatus
from src.order.repository import OrderRepository
from src.customer.service import CustomerService
from src.product.service import ProductService
class OrderService:
"""주문 서비스: 애그리게이트 루트 역할을 수행"""
def __init__(
self,
order_repository: OrderRepository,
customer_service: CustomerService,
product_service: ProductService
):
self.repository = order_repository
self.customer_service = customer_service
self.product_service = product_service
@transaction
def place_order(self, customer_id: UUID, items: List[Dict]) -> Optional[UUID]:
"""주문 생성 및 처리
이 메서드는 애그리게이트 경계 내에서 다음 작업을 수행:
1. 고객 검증
2. 주문 생성
3. 상품 재고 확인 및 차감
4. 고객 포인트 적립
모든 작업은 하나의 트랜잭션으로 처리되며, 어느 한 단계라도 실패하면
전체 트랜잭션이 롤백됩니다.
"""
# 1. 고객 검증
customer = self.customer_service.get_customer(customer_id)
if not customer:
return None
# 2. 주문 생성
order = Order(customer_id=customer_id)
# 3. 상품 정보 조회 및 주문 항목 추가
product_ids = [item['product_id'] for item in items]
products = self.product_service.get_products(product_ids)
if len(products) != len(product_ids):
return None # 일부 상품이 존재하지 않음
# 상품 ID별 매핑
product_map = {product.id: product for product in products}
# 주문 항목 추가
for item in items:
product = product_map[item['product_id']]
order.add_item(product.id, item['quantity'], product.price)
# 4. 상품 재고 확인 및 차감
success, _ = self.product_service.check_and_decrease_stock_bulk([
{'product_id': item.product_id, 'quantity': item.quantity}
for item in order.items
])
if not success:
# 트랜잭션은 데코레이터에 의해 롤백됨
return None # 재고 부족
# 5. 주문 저장
order.change_status(OrderStatus.PAID)
self.repository.save(order)
# 6. 고객 포인트 적립 (주문 금액의 1%)
points_to_add = int(order.total_amount * 0.01)
self.customer_service.add_points(customer.id, points_to_add)
return order.id
@transaction
def cancel_order(self, order_id: UUID) -> bool:
"""주문 취소"""
# 1. 주문 조회
order = self.repository.find_by_id(order_id)
if not order or order.status != OrderStatus.PAID:
return False
# 2. 주문 상태 변경
order.change_status(OrderStatus.CANCELLED)
self.repository.save(order)
return True
애그리게이트 루트로서의 서비스: OrderService.place_order
메서드는 주문 애그리게이트 루트 역할을 수행합니다. 주문 생성 시 여러 엔티티(주문, 고객, 상품)를 일관되게 변경합니다.
트랜잭션 관리: @transaction
데코레이터를 통해 트랜잭션을 관리합니다. 이는 애그리게이트 일관성을 보장하는 핵심 메커니즘입니다.
바운디드 컨텍스트 간 협력: OrderService
가 CustomerService
와 ProductService
를 주입받아 사용하는 것은 바운디드 컨텍스트 간 협력의 한 방식입니다.
도메인 규칙 적용: 서비스 계층은 엔티티의 단순한 CRUD를 넘어, 복잡한 비즈니스 규칙을 적용합니다. 예를 들어, 주문 처리 시 재고 확인, 포인트 적립 등의 규칙이 적용됩니다.
DDD에서는 바운디드 컨텍스트 간 통합을 위한 여러 패턴을 제공합니다. 우리 예제에서는 공유 커널(Shared Kernel
) 접근 방식을 사용했습니다.
공유 커널은 여러 바운디드 컨텍스트가 공유하는 모델이나 코드입니다. 우리 예제에서는 서비스 주입 방식을 통해 공유 커널을 구현했습니다:
def __init__(
self,
order_repository: OrderRepository,
customer_service: CustomerService, # 다른 컨텍스트의 서비스 주입
product_service: ProductService # 다른 컨텍스트의 서비스 주입
):
이 접근 방식은 간단하고 직관적이지만, 컨텍스트 간 결합도가 높아질 수 있다는 단점이 있습니다.
더 큰 시스템이나 더 엄격한 컨텍스트 분리가 필요한 경우, 다음과 같은 대안을 고려할 수 있습니다:
한 컨텍스트가 다른 컨텍스트의 모델을 직접 사용하지 않고, 변환 계층을 통해 통신합니다:
# 안티코럽션 계층 예시
class CustomerAdapter:
def __init__(self, customer_service: CustomerService):
self.customer_service = customer_service
def verify_customer(self, customer_id: UUID) -> bool:
# 주문 컨텍스트의 필요에 맞게 고객 서비스 변환
return self.customer_service.get_customer(customer_id) is not None
느슨한 결합을 위해 이벤트 발행/구독 패턴을 사용합니다:
# 이벤트 기반 통합 예시
# src/order/service.py
def place_order(self, customer_id: UUID, items: List[Dict]) -> Optional[UUID]:
# ... 주문 생성 로직 ...
# 이벤트 발행
self.event_publisher.publish(
"order.created",
{
"order_id": order.id,
"customer_id": customer_id,
"total_amount": order.total_amount
}
)
return order.id
# src/customer/event_handlers.py
def handle_order_created(event_data):
customer_id = event_data["customer_id"]
total_amount = event_data["total_amount"]
points_to_add = int(total_amount * 0.01)
customer_service = get_customer_service()
customer_service.add_points(customer_id, points_to_add)
DDD에서는 인프라스트럭처가 도메인 모델을 지원하는 역할을 합니다. 데이터베이스 연결, 트랜잭션 관리 등의 기술적 관심사를 도메인 모델과 분리함으로써, 도메인 모델이 특정 기술에 의존하지 않도록 합니다.
# src/infrastructure/database.py
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
# 모든 모델의 기본 클래스
Base = declarative_base()
# 트랜잭션 관리를 위한 데코레이터
def transaction(func):
"""트랜잭션 관리 데코레이터"""
# ...트랜잭션 관리 로직...
이러한 분리를 통해 도메인 로직은 기술 인프라의 변경에 영향을 받지 않고 비즈니스 요구사항에 집중할 수 있습니다.
지금까지 DDD 기반의 주문 시스템 구현을 살펴보았습니다. 이 접근 방식의 주요 이점과 고려사항을 정리해 보겠습니다.
비즈니스 중심 설계: 기술보다 비즈니스 도메인에 집중하여, 소프트웨어가 실제 비즈니스를 더 정확히 반영합니다.
모듈성과 유지보수성: 바운디드 컨텍스트를 통한 명확한 경계 설정으로 시스템의 모듈성과 유지보수성이 향상됩니다.
확장성: 각 바운디드 컨텍스트가 독립적으로 발전할 수 있어, 시스템 확장이 용이합니다.
도메인 전문가와의 효과적인 협업: 유비쿼터스 언어를 통해 개발자와 도메인 전문가 간 의사소통이 향상됩니다.
복잡성: DDD는 초기 학습 곡선이 가파르고, 간단한 CRUD 애플리케이션에는 과도할 수 있습니다.
성능 고려: 엄격한 경계와 캡슐화가 때로는 성능 최적화를 어렵게 할 수 있습니다.
통합 복잡성: 바운디드 컨텍스트 간 통합이 복잡해질 수 있으며, 적절한 통합 패턴 선택이 중요합니다.
DDD를 공부하면서 가장 오래 고민했던 부분은 Repository와 Entity의 관계였습니다. 일반적인 개발 방식에서는 하나의 Entity마다 하나의 Repository를 만드는 패턴을 많이 사용합니다. 예를 들어 Order와 OrderItem이 있다면, OrderRepository와 OrderItemRepository를 각각 만들어 책임을 분리하는 것이 직관적으로 느껴졌습니다.
그런데 DDD에서는 다른 접근법을 취합니다. 애그리게이트(Aggregate)라는 개념이 중심에 있고, 애그리게이트 루트(Aggregate Root)만이 Repository의 직접적인 대상이 됩니다. 즉, Order가 애그리게이트 루트라면 OrderRepository만 존재하고, OrderItem은 Order를 통해서만 접근하는 구조입니다.
이 패턴은 트랜잭션 일관성과 도메인 불변성을 유지하는 데 강점이 있지만, 실무에서는 몇 가지 어려움이 있습니다:
그래서 저는 다음과 같은 실용적인 접근법을 선호합니다:
# OrderItemRepository 구현
class OrderItemRepository:
def __init__(self, session: Session):
self.session = session
def find_by_id(self, item_id: UUID) -> Optional[OrderItem]:
return self.session.query(OrderItem).filter(OrderItem.id == item_id).first()
def find_by_order_id(self, order_id: UUID) -> List[OrderItem]:
return self.session.query(OrderItem).filter(OrderItem.order_id == order_id).all()
def find_by_product(self, product_id: UUID) -> List[OrderItem]:
"""특정 상품이 포함된 주문 항목 조회"""
return self.session.query(OrderItem).filter(OrderItem.product_id == product_id).all()
def save(self, item: OrderItem) -> None:
if item.id is None or not self.session.query(OrderItem.id).filter(OrderItem.id == item.id).first():
self.session.add(item)
# OrderItemService 구현
class OrderItemService:
def __init__(self, repository: OrderItemRepository):
self.repository = repository
def get_item(self, item_id: UUID) -> Optional[OrderItem]:
return self.repository.find_by_id(item_id)
def get_items_by_order(self, order_id: UUID) -> List[OrderItem]:
return self.repository.find_by_order_id(order_id)
def update_item_quantity(self, item_id: UUID, quantity: int) -> bool:
item = self.repository.find_by_id(item_id)
if not item:
return False
item.quantity = quantity
self.repository.save(item)
return True
# OrderService에 OrderItemService 주입
class OrderService:
def __init__(
self,
order_repository: OrderRepository,
order_item_service: OrderItemService, # OrderItemService 주입
customer_service: CustomerService,
product_service: ProductService
):
self.repository = order_repository
self.order_item_service = order_item_service
self.customer_service = customer_service
self.product_service = product_service
# 주문 도메인 로직 구현...
이 접근법에서 중요한 점은 트랜잭션 일관성을 어떻게 유지할 것인가 하는 문제입니다. DDD의 애그리게이트 패턴이 이 문제를 자연스럽게 해결하는 반면, 엔티티별 Repository 패턴에서는 추가 작업이 필요합니다:
@transaction
def update_order_with_items(self, order_id: UUID, updated_items: List[Dict]) -> bool:
# 주문 조회
order = self.repository.find_by_id(order_id)
if not order:
return False
# 주문 항목 업데이트
for item_data in updated_items:
item = self.order_item_service.get_item(item_data['id'])
if item and item.order_id == order_id: # 중요: 주문에 속한 항목인지 확인
item.quantity = item_data['quantity']
self.order_item_service.repository.save(item)
# 주문 총액 재계산
items = self.order_item_service.get_items_by_order(order_id)
order.total_amount = sum(item.quantity * item.unit_price for item in items)
self.repository.save(order)
return True
이런 식으로 트랜잭션 데코레이터와 명시적인 일관성 체크를 통해 애그리게이트의 일관성을 유지할 수 있습니다.
다음과 같은 기준으로 접근법을 선택하는 것이 좋다고 생각됩니다:
DDD의 개념적 순수함과 실용적인 개발 사이의 균형을 찾는 것이 중요합니다. 엄격한 DDD 원칙을 따르는 것도 좋지만, 프로젝트의 특성과 팀의 상황에 맞게 유연하게 적용하는 것이 현실적인 접근법이라고 생각합니다.
DDD는 단순한 설계 방법론이 아닌, 복잡한 비즈니스 도메인을 효과적으로 다루기 위한 철학적 접근법입니다. 바운디드 컨텍스트, 애그리게이트, 도메인 모델 등의 개념은 도메인 복잡성을 관리 가능한 단위로 나누고, 소프트웨어가 비즈니스 요구사항을 더 정확히 반영할 수 있게 합니다.
제 개인적인 선호는 시스템의 복잡한 부분에는 DDD 패턴을, 단순한 부분에는 엔티티별 Repository 패턴을 혼합해서 사용하는 것입니다. 그리고 무엇보다, 팀이 이해하고 효율적으로 작업할 수 있는 방식을 선택하는 것이 가장 중요합니다.
소프트웨어 설계에는 '완벽한' 방법론보다는 '적합한' 방법론이 있을 뿐이며, 각 프로젝트의 요구사항과 팀의 역량에 맞게 최적의 접근법을 찾아가는 과정이 진정한 소프트웨어 설계의 핵심이라고 생각합니다.