9148c358d0
Topic_Agent/Topic_Blog/Topics/Topics_Biz/Topics_Meeting/Topics_Rag의 마크다운 지식 문서를 Topic_General/Topic_Programming/Topic_Graphic/Topic_Business 4개 카테고리로 재분류. - 중복 제거: frontmatter의 status:duplicate/merged + duplicate_of/redirect_to 필드로 자기 자신을 중복으로 선언한 리다이렉트 stub 1032개 제거, 완전 동일 내용 파일 472개 제거, 동일 파일명·다른 내용 충돌 시 더 큰(완전한) 버전만 유지(162개 제거) — 총 1639개 중복 제거. - 분류: 폴더 단위로 명확한 항목(AI_and_ML/Coding/Architecture 등 → Programming, Comfyui/Visual_Effects → Graphic, Topics_Biz/Topics_Meeting/사업 등 → Business, Poetic_Blog_Writing/창의성/Game_Design 등 → General)은 폴더 우선순위로, 나머지 혼재 폴더(Topic_Agent/Topic_Blog/Topics 루트/Thinking & Reasoning/Other/UI_UX_Assets)는 title/tags 키워드 스코어링으로 파일 단위 분류(불명확한 경우 General로 폴백). 원본 폴더명은 "From_*" 서브폴더로 보존해 추적 가능성 유지. - 최종 배치: Programming 2784 / General 1608 / Graphic 285 / Business 249 = 4926개 문서. - 에이전트 운영 상태(.astra/.agent/.obsidian/sessions/memory/_company/docs/lessons/_shared/src)는 지식 콘텐츠가 아니므로 재분류 대상에서 제외하고 원위치 유지. - Topics/Topic_email(상위 보호 폴더 Topic_email과 파일명 100% 중복) 삭제 — 보호 폴더 자체는 미변경. - 완전히 비게 된 Topic_Agent/Topic_Blog/Topics_Biz/Topics_Rag 폴더 제거.
6.7 KiB
6.7 KiB
id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
| id | title | category | status | source_trust_level | verification_status | created_at | updated_at | tags | tech_stack | applied_in | aliases | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| arch-hexagonal-clean | Hexagonal / Clean Architecture — Port / Adapter / 의존 방향 | Coding | draft | B | conceptual | 2026-05-09 | 2026-05-09 |
|
|
|
Hexagonal / Clean Architecture
도메인 = 중심, infrastructure = 가장자리. Port (interface) + Adapter (구현). 의존 방향 = 항상 안쪽 (도메인). 테스트 = adapter mock 으로 도메인만.
📖 핵심 개념
- Domain: 비즈니스 로직 + entity. 외부 의존 없음.
- Application: use case (orchestration).
- Adapter: HTTP / DB / queue / 외부 API.
- Port: domain ↔ adapter 사이 interface.
💻 코드 패턴
Folder
src/
domain/ (pure)
user.ts
order.ts
application/ (use case)
createOrder.ts
ports/ (interface)
orderRepository.ts
paymentGateway.ts
adapters/ (구현)
in/ (driving — HTTP, queue 받음)
httpController.ts
out/ (driven — DB, API 호출)
pgOrderRepository.ts
stripePaymentGateway.ts
config.ts
Domain (pure)
// domain/order.ts
export class Order {
constructor(public readonly id: string, public items: Item[], public status: OrderStatus) {}
addItem(item: Item) {
if (this.status !== 'open') throw new Error('order closed');
this.items.push(item);
}
total(): Decimal {
return this.items.reduce((sum, i) => sum.add(i.price.mul(i.qty)), new Decimal(0));
}
}
→ DB / HTTP / 시간 의존 X. 순수.
Port
// ports/orderRepository.ts
export interface OrderRepository {
find(id: string): Promise<Order | null>;
save(order: Order): Promise<void>;
}
// ports/paymentGateway.ts
export interface PaymentGateway {
charge(amount: Decimal, currency: string): Promise<{ id: string }>;
}
Application (use case)
// application/createOrder.ts
export class CreateOrderUseCase {
constructor(
private orders: OrderRepository,
private payment: PaymentGateway,
) {}
async execute(input: CreateOrderInput): Promise<Order> {
const order = new Order(uuid(), input.items, 'open');
const total = order.total();
await this.payment.charge(total, 'USD');
order.markPaid();
await this.orders.save(order);
return order;
}
}
Adapter — out (DB)
// adapters/out/pgOrderRepository.ts
export class PgOrderRepository implements OrderRepository {
constructor(private db: Drizzle) {}
async find(id: string): Promise<Order | null> {
const row = await this.db.select().from(ordersTable).where(eq(ordersTable.id, id)).limit(1);
if (!row[0]) return null;
return toDomain(row[0]); // mapper
}
async save(order: Order): Promise<void> {
await this.db.insert(ordersTable).values(toRow(order)).onConflictDoUpdate(...);
}
}
Adapter — out (External API)
// adapters/out/stripePaymentGateway.ts
export class StripePaymentGateway implements PaymentGateway {
constructor(private stripe: Stripe) {}
async charge(amount: Decimal, currency: string): Promise<{ id: string }> {
const intent = await this.stripe.paymentIntents.create({
amount: amount.mul(100).toNumber(),
currency,
});
return { id: intent.id };
}
}
Adapter — in (HTTP)
// adapters/in/httpController.ts
export function makeOrderRouter(useCase: CreateOrderUseCase) {
const router = express.Router();
router.post('/orders', async (req, res) => {
const input = CreateOrderSchema.parse(req.body);
const order = await useCase.execute(input);
res.json({ id: order.id, total: order.total().toString() });
});
return router;
}
Composition root (config)
// config.ts — 한 곳에서 의존 주입
const db = drizzle(new Pool(...));
const stripe = new Stripe(...);
const orderRepo = new PgOrderRepository(db);
const payment = new StripePaymentGateway(stripe);
const createOrder = new CreateOrderUseCase(orderRepo, payment);
const app = express();
app.use('/api', makeOrderRouter(createOrder));
Test (mock adapter)
class FakeOrderRepository implements OrderRepository {
private store = new Map<string, Order>();
async find(id: string) { return this.store.get(id) ?? null; }
async save(o: Order) { this.store.set(o.id, o); }
}
class FakePaymentGateway implements PaymentGateway {
charges: { amount: Decimal }[] = [];
async charge(amount: Decimal) {
this.charges.push({ amount });
return { id: 'fake-' + this.charges.length };
}
}
test('createOrder', async () => {
const orders = new FakeOrderRepository();
const payment = new FakePaymentGateway();
const uc = new CreateOrderUseCase(orders, payment);
const order = await uc.execute({ items: [...] });
expect(payment.charges).toHaveLength(1);
expect(await orders.find(order.id)).not.toBeNull();
});
→ DB / Stripe 없이 빠른 unit test.
의존 방향 강제 (lint)
// .eslintrc — dependency-cruiser 또는 import 규칙
// domain 가 ports/adapters import 금지
// application 이 adapters import 금지
{
"import/no-restricted-paths": ["error", {
"zones": [
{ "target": "src/domain", "from": "src/(application|adapters|ports)" },
{ "target": "src/application", "from": "src/adapters" },
]
}]
}
Onion (변형)
Domain → Domain Services → Application → Infrastructure
비슷한 의존 방향 — 이름만 다름.
🤔 의사결정 기준
| 상황 | 추천 |
|---|---|
| 큰 팀 / 복잡 도메인 | Hexagonal / Clean |
| 작은 CRUD | overkill — 일반 layered |
| 단순 script | 직접 |
| 마이크로서비스 (작은 service) | service 안 단순 layered |
| 큰 monolith | hexagonal 가 분리 |
| Multiple delivery (HTTP + queue + CLI) | 매우 적합 |
❌ 안티패턴
- Domain 안 ORM annotation: 의존 누설.
- Port = repo 1:1 매핑 모든 method: god interface. UseCase 별 작은 port.
- Adapter 가 도메인 직접 변형: domain mapper 따로.
- DTO = Domain entity: serialization 변경 = 도메인 변경.
- 모든 API 가 use case: 단순 read 도 use case — overhead. Query / Command 분리.
- Layer 가 너무 많음: 5 layer cross 매번. 2-3 layer.
- Composition root 분산: DI container 또는 한 곳.
🤖 LLM 활용 힌트
- Domain (pure) → Application → Adapters 3 layer.
- Port (interface) + Adapter 가 키워드.
- Test = mock adapter, real domain.