Skip to content

필사 모드: クリーンアーキテクチャとヘキサゴナルアーキテクチャの実践実装:ポートとアダプターパターンでドメイン独立性を確保

日本語
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

はじめに

ソフトウェアが成長するにつれて最初に崩壊するのは、通常アーキテクチャである。最初はシンプルなレイヤードアーキテクチャで始まったものの、ビジネスロジックがコントローラに浸透し、データベーススキーマの変更がドメインロジックに影響を与え、フレームワークのアップグレードがコードベース全体を揺るがす状況を多くの開発者が経験している。

クリーンアーキテクチャ(Clean Architecture)とヘキサゴナルアーキテクチャ(Hexagonal Architecture、Ports and Adapters)は、この問題を解決するための代表的なアプローチである。両アーキテクチャともに、コアビジネスロジックを外部の関心事(データベース、フレームワーク、UI)から隔離することを目標としている。

本記事では、両アーキテクチャの理論的背景を比較した上で、TypeScriptとPythonで実際に実装する方法をコードとともに解説する。

アーキテクチャ比較:ヘキサゴナル vs クリーン vs オニオン vs レイヤード

コア概念比較表

| 項目 | ヘキサゴナルアーキテクチャ | クリーンアーキテクチャ | オニオンアーキテクチャ | レイヤードアーキテクチャ |

| ---------------- | -------------------------- | ------------------------------------------------------------------- | -------------------------- | ---------------------------------- |

| **提案者** | Alistair Cockburn (2005) | Robert C. Martin (2012) | Jeffrey Palermo (2008) | 伝統的パターン |

| **核心原則** | ポートとアダプター | 依存性ルール | ドメイン中心レイヤー | レイヤー間単方向依存 |

| **レイヤー構造** | コア + ポート + アダプター | エンティティ/ユースケース/インターフェースアダプター/フレームワーク | ドメイン/サービス/インフラ | プレゼンテーション/ビジネス/データ |

| **依存性の方向** | 外側 -> 内側 | 外側 -> 内側 | 外側 -> 内側 | 上 -> 下 |

| **テスト容易性** | 高い | 高い | 高い | 普通 |

| **インフラ交換** | アダプター交換で簡単 | 外部レイヤー交換 | 外部レイヤー交換 | レイヤー全体の修正が必要 |

| **学習曲線** | 普通 | 高い | 普通 | 低い |

| **適切な規模** | 中~大規模 | 大規模 | 中~大規模 | 小~中規模 |

クリーンアーキテクチャの依存性ルール

クリーンアーキテクチャの核心は**依存性ルール(Dependency Rule)**である。依存性は必ず外側から内側に向かわなければならない。

内側レイヤーから外側レイヤーまで4層で構成される:

1. **Entities(エンティティ)**: エンタープライズビジネスルール。外部依存性は一切ない。

2. **Use Cases(ユースケース)**: アプリケーションビジネスルール。エンティティにのみ依存する。

3. **Interface Adapters(インターフェースアダプター)**: ユースケースと外部世界を接続。コントローラ、プレゼンター、ゲートウェイがここに位置する。

4. **Frameworks & Drivers(フレームワーク)**: Webフレームワーク、DB、UI等の外部ツール。

ヘキサゴナルアーキテクチャのポートとアダプター

ヘキサゴナルアーキテクチャはアプリケーションを六角形(Hexagon)で表現し、2種類のポートを定義する:

- **インバウンドポート(Driving Port)**: 外部からアプリケーションを呼び出すためのインターフェース(例:ユースケースインターフェース)

- **アウトバウンドポート(Driven Port)**: アプリケーションが外部システムにアクセスするためのインターフェース(例:リポジトリインターフェース)

各ポートには対応するアダプターがある:

- **インバウンドアダプター(Driving Adapter)**: HTTPコントローラ、CLI、メッセージリスナー等

- **アウトバウンドアダプター(Driven Adapter)**: DBリポジトリ、外部APIクライアント、メッセージパブリッシャー等

TypeScript実践実装

プロジェクト構造

src/

domain/ # ドメインレイヤー(エンティティ、バリューオブジェクト)

entities/

Order.ts

Product.ts

value-objects/

Money.ts

OrderStatus.ts

application/ # アプリケーションレイヤー(ユースケース、ポート)

ports/

inbound/

CreateOrderUseCase.ts

GetOrderUseCase.ts

outbound/

OrderRepository.ts

PaymentGateway.ts

EventPublisher.ts

services/

CreateOrderService.ts

GetOrderService.ts

infrastructure/ # インフラストラクチャレイヤー(アダプター)

adapters/

inbound/

http/

OrderController.ts

messaging/

OrderEventListener.ts

outbound/

persistence/

TypeOrmOrderRepository.ts

payment/

StripePaymentAdapter.ts

messaging/

KafkaEventPublisher.ts

config/

DependencyContainer.ts

ドメインレイヤー:エンティティとバリューオブジェクト

// domain/value-objects/Money.ts

export class Money {

private constructor(

private readonly amount: number,

private readonly currency: string

) {

if (amount < 0) {

throw new Error('Amount cannot be negative')

}

}

static of(amount: number, currency: string = 'KRW'): Money {

return new Money(amount, currency)

}

add(other: Money): Money {

this.ensureSameCurrency(other)

return Money.of(this.amount + other.amount, this.currency)

}

multiply(factor: number): Money {

return Money.of(this.amount * factor, this.currency)

}

isGreaterThan(other: Money): boolean {

this.ensureSameCurrency(other)

return this.amount > other.amount

}

getAmount(): number {

return this.amount

}

private ensureSameCurrency(other: Money): void {

if (this.currency !== other.currency) {

throw new Error('Currency mismatch')

}

}

}

// domain/value-objects/OrderStatus.ts

export enum OrderStatus {

PENDING = 'PENDING',

CONFIRMED = 'CONFIRMED',

PAID = 'PAID',

SHIPPED = 'SHIPPED',

DELIVERED = 'DELIVERED',

CANCELLED = 'CANCELLED',

}

// domain/entities/Order.ts

export interface OrderItem {

productId: string

productName: string

quantity: number

unitPrice: Money

}

export class Order {

private constructor(

private readonly id: string,

private readonly customerId: string,

private items: OrderItem[],

private status: OrderStatus,

private readonly createdAt: Date

) {}

static create(id: string, customerId: string, items: OrderItem[]): Order {

if (items.length === 0) {

throw new Error('Order must have at least one item')

}

return new Order(id, customerId, items, OrderStatus.PENDING, new Date())

}

confirm(): void {

if (this.status !== OrderStatus.PENDING) {

throw new Error('Only pending orders can be confirmed')

}

this.status = OrderStatus.CONFIRMED

}

pay(): void {

if (this.status !== OrderStatus.CONFIRMED) {

throw new Error('Only confirmed orders can be paid')

}

this.status = OrderStatus.PAID

}

cancel(): void {

if (this.status === OrderStatus.SHIPPED || this.status === OrderStatus.DELIVERED) {

throw new Error('Cannot cancel shipped or delivered orders')

}

this.status = OrderStatus.CANCELLED

}

getTotalAmount(): Money {

return this.items.reduce(

(total, item) => total.add(item.unitPrice.multiply(item.quantity)),

Money.of(0)

)

}

getId(): string {

return this.id

}

getCustomerId(): string {

return this.customerId

}

getStatus(): OrderStatus {

return this.status

}

getItems(): ReadonlyArray<OrderItem> {

return [...this.items]

}

}

アプリケーションレイヤー:ポートとユースケース

// application/ports/inbound/CreateOrderUseCase.ts

export interface CreateOrderCommand {

customerId: string

items: Array<{

productId: string

productName: string

quantity: number

unitPrice: number

}>

}

export interface CreateOrderResult {

orderId: string

totalAmount: number

status: string

}

export interface CreateOrderUseCase {

execute(command: CreateOrderCommand): Promise<CreateOrderResult>

}

// application/ports/outbound/OrderRepository.ts

export interface OrderRepository {

save(order: Order): Promise<void>

findById(id: string): Promise<Order | null>

findByCustomerId(customerId: string): Promise<Order[]>

}

// application/ports/outbound/PaymentGateway.ts

export interface PaymentRequest {

orderId: string

amount: number

currency: string

}

export interface PaymentResult {

transactionId: string

success: boolean

errorMessage?: string

}

export interface PaymentGateway {

processPayment(request: PaymentRequest): Promise<PaymentResult>

}

// application/ports/outbound/EventPublisher.ts

export interface DomainEvent {

eventType: string

aggregateId: string

payload: Record<string, unknown>

occurredAt: Date

}

export interface EventPublisher {

publish(event: DomainEvent): Promise<void>

}

// application/services/CreateOrderService.ts

CreateOrderUseCase,

CreateOrderCommand,

CreateOrderResult,

} from '../ports/inbound/CreateOrderUseCase'

export class CreateOrderService implements CreateOrderUseCase {

constructor(

private readonly orderRepository: OrderRepository,

private readonly paymentGateway: PaymentGateway,

private readonly eventPublisher: EventPublisher

) {}

async execute(command: CreateOrderCommand): Promise<CreateOrderResult> {

// 1. ドメインオブジェクトの作成

const orderItems: OrderItem[] = command.items.map((item) => ({

productId: item.productId,

productName: item.productName,

quantity: item.quantity,

unitPrice: Money.of(item.unitPrice),

}))

const order = Order.create(uuidv4(), command.customerId, orderItems)

// 2. 注文確認

order.confirm()

// 3. 決済処理

const paymentResult = await this.paymentGateway.processPayment({

orderId: order.getId(),

amount: order.getTotalAmount().getAmount(),

currency: 'KRW',

})

if (!paymentResult.success) {

order.cancel()

await this.orderRepository.save(order)

throw new Error('Payment failed: ' + (paymentResult.errorMessage || 'Unknown error'))

}

// 4. 決済完了処理

order.pay()

// 5. 保存

await this.orderRepository.save(order)

// 6. ドメインイベント発行

await this.eventPublisher.publish({

eventType: 'OrderCreated',

aggregateId: order.getId(),

payload: {

customerId: order.getCustomerId(),

totalAmount: order.getTotalAmount().getAmount(),

itemCount: order.getItems().length,

},

occurredAt: new Date(),

})

return {

orderId: order.getId(),

totalAmount: order.getTotalAmount().getAmount(),

status: order.getStatus(),

}

}

}

インフラストラクチャレイヤー:アダプター実装

// infrastructure/adapters/outbound/persistence/TypeOrmOrderRepository.ts

export class TypeOrmOrderRepository implements OrderRepository {

private readonly repository: Repository<OrderEntity>

constructor(dataSource: DataSource) {

this.repository = dataSource.getRepository(OrderEntity)

}

async save(order: Order): Promise<void> {

const entity = this.toEntity(order)

await this.repository.save(entity)

}

async findById(id: string): Promise<Order | null> {

const entity = await this.repository.findOne({

where: { id },

relations: ['items'],

})

return entity ? this.toDomain(entity) : null

}

async findByCustomerId(customerId: string): Promise<Order[]> {

const entities = await this.repository.find({

where: { customerId },

relations: ['items'],

})

return entities.map((e) => this.toDomain(e))

}

private toEntity(order: Order): OrderEntity {

const entity = new OrderEntity()

entity.id = order.getId()

entity.customerId = order.getCustomerId()

entity.status = order.getStatus()

entity.totalAmount = order.getTotalAmount().getAmount()

return entity

}

private toDomain(entity: OrderEntity): Order {

return Order.create(entity.id, entity.customerId, /* items mapping */ [])

}

}

// infrastructure/adapters/inbound/http/OrderController.ts

export class OrderController {

private readonly router: Router

constructor(private readonly createOrderUseCase: CreateOrderUseCase) {

this.router = Router()

this.setupRoutes()

}

private setupRoutes(): void {

this.router.post('/orders', this.createOrder.bind(this))

}

private async createOrder(req: Request, res: Response): Promise<void> {

try {

const result = await this.createOrderUseCase.execute({

customerId: req.body.customerId,

items: req.body.items,

})

res.status(201).json(result)

} catch (error) {

if (error instanceof Error) {

res.status(400).json({ error: error.message })

} else {

res.status(500).json({ error: 'Internal server error' })

}

}

}

getRouter(): Router {

return this.router

}

}

DIコンテナ設定

// infrastructure/config/DependencyContainer.ts

export class DependencyContainer {

private static instance: DependencyContainer

private constructor(private readonly dataSource: DataSource) {}

static async create(dataSource: DataSource): Promise<DependencyContainer> {

if (!DependencyContainer.instance) {

DependencyContainer.instance = new DependencyContainer(dataSource)

}

return DependencyContainer.instance

}

createOrderController(): OrderController {

// アウトバウンドアダプターの作成

const orderRepository = new TypeOrmOrderRepository(this.dataSource)

const paymentGateway = new StripePaymentAdapter()

const eventPublisher = new KafkaEventPublisher()

// ユースケースの作成(ポート実装体を注入)

const createOrderService = new CreateOrderService(

orderRepository,

paymentGateway,

eventPublisher

)

// インバウンドアダプターの作成

return new OrderController(createOrderService)

}

}

Python実践実装

プロジェクト構造とドメインレイヤー

domain/entities/order.py

from dataclasses import dataclass, field

from enum import Enum

from datetime import datetime

from typing import List

from domain.value_objects.money import Money

class OrderStatus(Enum):

PENDING = "PENDING"

CONFIRMED = "CONFIRMED"

PAID = "PAID"

SHIPPED = "SHIPPED"

CANCELLED = "CANCELLED"

@dataclass

class OrderItem:

product_id: str

product_name: str

quantity: int

unit_price: Money

class Order:

def __init__(

self,

order_id: str,

customer_id: str,

items: List[OrderItem],

status: OrderStatus = OrderStatus.PENDING,

created_at: datetime = None,

):

if not items:

raise ValueError("Order must have at least one item")

self._id = order_id

self._customer_id = customer_id

self._items = items

self._status = status

self._created_at = created_at or datetime.utcnow()

def confirm(self) -> None:

if self._status != OrderStatus.PENDING:

raise ValueError("Only pending orders can be confirmed")

self._status = OrderStatus.CONFIRMED

def pay(self) -> None:

if self._status != OrderStatus.CONFIRMED:

raise ValueError("Only confirmed orders can be paid")

self._status = OrderStatus.PAID

def cancel(self) -> None:

if self._status in (OrderStatus.SHIPPED,):

raise ValueError("Cannot cancel shipped orders")

self._status = OrderStatus.CANCELLED

@property

def total_amount(self) -> Money:

total = Money(0)

for item in self._items:

total = total.add(item.unit_price.multiply(item.quantity))

return total

@property

def id(self) -> str:

return self._id

@property

def customer_id(self) -> str:

return self._customer_id

@property

def status(self) -> OrderStatus:

return self._status

ポート定義(抽象クラス)

application/ports/outbound/order_repository.py

from abc import ABC, abstractmethod

from typing import Optional, List

from domain.entities.order import Order

class OrderRepository(ABC):

@abstractmethod

async def save(self, order: Order) -> None:

pass

@abstractmethod

async def find_by_id(self, order_id: str) -> Optional[Order]:

pass

@abstractmethod

async def find_by_customer_id(self, customer_id: str) -> List[Order]:

pass

application/ports/outbound/payment_gateway.py

from abc import ABC, abstractmethod

from dataclasses import dataclass

@dataclass

class PaymentRequest:

order_id: str

amount: float

currency: str

@dataclass

class PaymentResult:

transaction_id: str

success: bool

error_message: str = ""

class PaymentGateway(ABC):

@abstractmethod

async def process_payment(self, request: PaymentRequest) -> PaymentResult:

pass

ユースケース実装

application/services/create_order_service.py

from dataclasses import dataclass

from typing import List

from domain.entities.order import Order, OrderItem

from domain.value_objects.money import Money

from application.ports.outbound.order_repository import OrderRepository

from application.ports.outbound.payment_gateway import PaymentGateway, PaymentRequest

@dataclass

class CreateOrderCommand:

customer_id: str

items: List[dict]

@dataclass

class CreateOrderResult:

order_id: str

total_amount: float

status: str

class CreateOrderService:

def __init__(

self,

order_repository: OrderRepository,

payment_gateway: PaymentGateway,

):

self._order_repository = order_repository

self._payment_gateway = payment_gateway

async def execute(self, command: CreateOrderCommand) -> CreateOrderResult:

items = [

OrderItem(

product_id=item["product_id"],

product_name=item["product_name"],

quantity=item["quantity"],

unit_price=Money(item["unit_price"]),

)

for item in command.items

]

order = Order(

order_id=str(uuid.uuid4()),

customer_id=command.customer_id,

items=items,

)

order.confirm()

payment_result = await self._payment_gateway.process_payment(

PaymentRequest(

order_id=order.id,

amount=order.total_amount.amount,

currency="KRW",

)

)

if not payment_result.success:

order.cancel()

await self._order_repository.save(order)

raise Exception(f"Payment failed: {payment_result.error_message}")

order.pay()

await self._order_repository.save(order)

return CreateOrderResult(

order_id=order.id,

total_amount=order.total_amount.amount,

status=order.status.value,

)

テスト戦略

ドメインユニットテスト

ドメインレイヤーは外部依存性がないため、純粋なユニットテストが可能である。

// tests/domain/entities/Order.test.ts

describe('Order', () => {

const sampleItems: OrderItem[] = [

{

productId: 'prod-1',

productName: 'Widget',

quantity: 2,

unitPrice: Money.of(10000),

},

]

it('should create an order with PENDING status', () => {

const order = Order.create('order-1', 'customer-1', sampleItems)

expect(order.getStatus()).toBe(OrderStatus.PENDING)

})

it('should throw when creating order with empty items', () => {

expect(() => Order.create('order-1', 'customer-1', [])).toThrow(

'Order must have at least one item'

)

})

it('should calculate total amount correctly', () => {

const order = Order.create('order-1', 'customer-1', sampleItems)

expect(order.getTotalAmount().getAmount()).toBe(20000)

})

it('should transition from PENDING to CONFIRMED', () => {

const order = Order.create('order-1', 'customer-1', sampleItems)

order.confirm()

expect(order.getStatus()).toBe(OrderStatus.CONFIRMED)

})

it('should not allow paying before confirming', () => {

const order = Order.create('order-1', 'customer-1', sampleItems)

expect(() => order.pay()).toThrow('Only confirmed orders can be paid')

})

})

ユースケース統合テスト(アダプターのモック)

// tests/application/services/CreateOrderService.test.ts

describe('CreateOrderService', () => {

let service: CreateOrderService

let mockOrderRepo: jest.Mocked<OrderRepository>

let mockPayment: jest.Mocked<PaymentGateway>

let mockEvents: jest.Mocked<EventPublisher>

beforeEach(() => {

mockOrderRepo = {

save: jest.fn().mockResolvedValue(undefined),

findById: jest.fn(),

findByCustomerId: jest.fn(),

}

mockPayment = {

processPayment: jest.fn().mockResolvedValue({

transactionId: 'tx-1',

success: true,

}),

}

mockEvents = {

publish: jest.fn().mockResolvedValue(undefined),

}

service = new CreateOrderService(mockOrderRepo, mockPayment, mockEvents)

})

it('should create and pay for an order successfully', async () => {

const result = await service.execute({

customerId: 'cust-1',

items: [

{

productId: 'prod-1',

productName: 'Widget',

quantity: 1,

unitPrice: 10000,

},

],

})

expect(result.status).toBe('PAID')

expect(mockOrderRepo.save).toHaveBeenCalledTimes(1)

expect(mockPayment.processPayment).toHaveBeenCalledTimes(1)

expect(mockEvents.publish).toHaveBeenCalledTimes(1)

})

it('should cancel order when payment fails', async () => {

mockPayment.processPayment.mockResolvedValue({

transactionId: '',

success: false,

errorMessage: 'Insufficient funds',

})

await expect(

service.execute({

customerId: 'cust-1',

items: [

{

productId: 'prod-1',

productName: 'Widget',

quantity: 1,

unitPrice: 10000,

},

],

})

).rejects.toThrow('Payment failed')

expect(mockOrderRepo.save).toHaveBeenCalledTimes(1)

})

})

アンチパターンと注意事項

アンチパターン1:ドメインがインフラに依存

// 悪い例:ドメインエンティティがTypeORMデコレータに依存

@Entity() // インフラフレームワーク依存!

export class Order {

@PrimaryColumn()

id: string

@Column()

status: string

}

// 良い例:ドメインエンティティは純粋なクラスとして維持

// 永続化マッピングはインフラレイヤーで別のEntityクラスで処理

export class Order {

private readonly id: string

private status: OrderStatus

// 純粋なビジネスロジックのみを含む

}

アンチパターン2:貧血ドメインモデル(Anemic Domain Model)

// 悪い例:ドメインエンティティが単なるデータ構造

export class Order {

id: string

status: string

items: OrderItem[]

totalAmount: number

// getter/setterのみ、ビジネスロジックなし

}

// ビジネスロジックがすべてサービスに集中

export class OrderService {

confirmOrder(order: Order): void {

if (order.status !== 'PENDING') throw new Error('...')

order.status = 'CONFIRMED'

order.totalAmount = this.calculateTotal(order.items)

}

}

// 良い例:ドメインエンティティ内にビジネスロジックをカプセル化

export class Order {

confirm(): void {

if (this.status !== OrderStatus.PENDING) {

throw new Error('Only pending orders can be confirmed')

}

this.status = OrderStatus.CONFIRMED

}

getTotalAmount(): Money {

return this.items.reduce(

(total, item) => total.add(item.unitPrice.multiply(item.quantity)),

Money.of(0)

)

}

}

アンチパターン3:過剰エンジニアリング(Over-engineering)

小規模なCRUDアプリケーションにヘキサゴナルアーキテクチャを適用すると、ボイラープレートコードが増加するだけで生産性が低下する。アーキテクチャの選択はプロジェクトの複雑さと寿命に合わせるべきである。

レイヤードアーキテクチャからのマイグレーション

段階的マイグレーション戦略

既存のレイヤードアーキテクチャからヘキサゴナル/クリーンアーキテクチャに移行する場合は、段階的にアプローチする必要がある。

1. **フェーズ1**: ドメインレイヤーの分離 -- ビジネスロジックをサービスからエンティティに移動

2. **フェーズ2**: ポート(インターフェース)の定義 -- リポジトリと外部サービスに対するインターフェースの抽出

3. **フェーズ3**: アダプターの実装 -- 既存の実装体をアダプターでラップ

4. **フェーズ4**: DIコンテナの設定 -- 依存性注入でポートとアダプターを接続

5. **フェーズ5**: テストの追加 -- ドメインユニットテストとポートモックテストの作成

バウンダリ決定の失敗事例

誤ったバウンダリ(境界)設定はアーキテクチャの利点を無効化する。例えば、一つのユースケースが多すぎるアウトバウンドポートに依存している場合、そのユースケースの責務が大きすぎるというシグナルである。ユースケースをより小さな単位に分割するのが望ましい。

逆に、ポートを過度に細分化するとポートの数が爆発的に増加し、管理が困難になる。一般的に、1つのAggregate Rootにつき1つのRepositoryポート、1つの外部システムにつき1つのGatewayポートが適切である。

本番環境チェックリスト

アーキテクチャ原則

- [ ] 依存性が常に外側から内側に向いているか確認

- [ ] ドメインレイヤーにフレームワークのimportがないか確認

- [ ] ドメインエンティティが十分なビジネスロジックをカプセル化しているか確認

- [ ] ポートインターフェースがドメイン用語で定義されているか確認(技術用語ではなく)

- [ ] DIコンテナがComposition Rootでのみ使用されているか確認

テスト

- [ ] ドメインレイヤーユニットテストカバレッジ90%以上

- [ ] ユースケーステストでアウトバウンドポートがモックされているか確認

- [ ] アダプター統合テストが存在するか確認

- [ ] E2Eテストで全体フローが検証されているか確認

コード品質

- [ ] パッケージ間の循環依存がないか確認

- [ ] ユースケースごとの責務が単一であるか確認(SRP)

- [ ] ポートインターフェースが最小限のメソッドのみを定義しているか確認(ISP)

- [ ] ArchUnitまたは類似ツールでアーキテクチャルールの自動検証

参考資料

- [Robert C. Martin - The Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)

- [Alistair Cockburn - Hexagonal Architecture](https://alistair.cockburn.us/hexagonal-architecture/)

- [Hexagonal Architecture: Ports and Adapters - Achieving True Domain Independence](https://www.javacodegeeks.com/2025/12/hexagonal-architecture-ports-and-adapters-achieving-true-domain-independence.html)

- [Hexagonal Architecture and Clean Architecture with Examples](https://dev.to/dyarleniber/hexagonal-architecture-and-clean-architecture-with-examples-48oi)

- [Explicit Architecture: DDD, Hexagonal, Onion, Clean, CQRS](https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/)

현재 단락 (1/669)

ソフトウェアが成長するにつれて最初に崩壊するのは、通常アーキテクチャである。最初はシンプルなレイヤードアーキテクチャで始まったものの、ビジネスロジックがコントローラに浸透し、データベーススキーマの変更...

작성 글자: 0원문 글자: 18,741작성 단락: 0/669