프롬프트에서 플랫폼까지: 내가 사용하는 아키텍처 규칙
Source: Dev.to
해당 링크에 포함된 기사 본문을 제공해 주시면, 요청하신 대로 한국어로 번역해 드리겠습니다. 현재는 본문 내용이 없으므로 번역을 진행할 수 없습니다. 부탁드립니다.
“빌드 → 서프라이즈 → 재구성 → 반복” 루프
루프는 초반에 놀랍습니다. 하지만 시간이 지나면 두 명의 광대가 서로를 능가하려고 장난을 치는 것처럼 느껴집니다: 점점 더 웃겨지고, 웃음이 끊이질 않지만… 어느 순간 한 명이 마지막 장난으로 화염방사기를 꺼내면서 웃음이 다소 어색해집니다.
이러한 반복은 재미있지만, 어느 순간 재미가 사라집니다. 그래서 저는 지침을 찾아보았습니다.
Source: …
LangGraph 튜토리얼 경험
대부분의 예제는 다음과 같은 과정을 보여줍니다:
- 그래프를 구축한다.
- 몇몇 노드를 정의한다.
- 노드들을 연결한다.
- 배포한다.
프로토타이핑에 좋습니다.
하지만 다음과 같은 상황에서는 어디에 무엇을 배치해야 할지 보여주지 않습니다:
- 8개의 노드
- 3개의 에이전트
- 5개의 도구
- 서브‑그래프 간 공유 상태
- 가드레일을 위한 미들웨어
- 프레임워크에 독립적인 플랫폼 레이어
검색해 보고, 조각들을 찾아보았지만 전체적인 그림은 없었습니다. 그래서 직접 만들었습니다.
확장 가능한 폴더 구조
아래는 내 LangGraph 컴포넌트의 구조입니다:
app/
├── agents/ # Agent factories (build_agent_*)
├── graphs/ # Graph definitions (main, subgraphs, phases)
├── nodes/ # Node factories (make_node_*)
├── states/ # Pydantic state models
├── tools/ # Tool definitions
├── middlewares/ # Cross‑cutting concerns (guardrails, redaction)
└── platform/
├── core/ # Pure types, contracts, policies (no wiring)
│ ├── contract/ # Validators: state, tools, prompts, phases
│ ├── dto/ # Pure data‑transfer objects
│ └── policy/ # Pure decision logic
├── adapters/ # Boundary translation (DTOs ↔ State)
├── runtime/ # Evidence hydration, state helpers
├── config/ # Environment, paths
└── observability # Logging
왜 이 구조인가?
LangGraph의 사고 모델을 그대로 반영합니다: agents는 agents이고, nodes는 nodes이며, graphs는 graphs. 오케스트레이션 레이어에서는 항목을 쉽게 찾을 수 있고 책임이 분리됩니다.
핵심 통찰은 platform/ 레이어입니다.
Source: …
플랫폼 레이어: 존재 이유
LangGraph 구성 요소들을 분리하는 것은 쉬웠지만, 배선을 분리하는 것은 어려웠습니다. 구조는 첫날에 나타난 것이 아니라 여러 차례 반복을 거친 뒤에 드러났습니다. 각 사이클마다 빠진 아키텍처 규칙이 발견되었고, 이러한 규칙이 없었기 때문에 새로운 구성 요소가 추가될수록 리팩터링이 점점 더 고통스러워졌습니다.
플랫폼 레이어가 없으면 – 모든 것이 뒤죽박죽이 된다
# WITHOUT PLATFORM LAYER – everything mixed together
def problem_framing_node(state: SageState) -> Command:
# Guardrail logic mixed with state management
if "unsafe" in state.messages[-1].content:
state.gating.guardrail = GuardrailResult(is_safe=False, ...)
# Evidence hydration mixed with node orchestration
store = get_store()
for item in phase_entry.evidence:
doc = store.get(item.namespace, item.key)
# ... inline hydration logic
# Validation mixed with execution
if "problem_framing" not in state.phases:
raise ValueError("Invalid state update!")
# ... good luck writing tests for it!
플랫폼 레이어가 있으면 – 깔끔하게 분리된다
# WITH PLATFORM LAYER – clean separation
def problem_framing_node(state: SageState) -> Command:
# Use platform contracts for validation
validate_state_update(update, owner="problem_framing")
# Use platform runtime helpers for evidence
bundle = collect_phase_evidence(state, phase="problem_framing")
# Use platform policies for decisions
guardrail = evaluate_guardrails(user_input)
# Use adapters for state translation
context = guardrail_to_gating(guardrail, user_input)
# Node only orchestrates – all logic lives in platform!
노드는 오케스트레이션만 수행하도록 변합니다. 도메인 로직도 없고, 스토어에 직접 접근도 없으며, 인라인 검증도 없습니다.
헥사고날 분할
문제를 해결한 패턴은 헥사고날(포트‑와‑어댑터) 아키텍처입니다. 코어는 순수하게 유지됩니다—프레임워크 의존성이 없고 외부 레이어로부터의 임포트도 없습니다. 다른 모든 요소는 코어에 의존할 수 있지만, 코어는 아무것도 의존하지 않습니다. 이렇게 하면 경계가 테스트 가능해지고 규칙을 강제할 수 있습니다.
┌─────────────────────────────────────────────────────────┐
│ APPLICATION LAYER │
│ (app/nodes, app/graphs, app/agents, app/middlewares) │
│ - LangGraph orchestration │
│ - Calls platform services via contracts │
└───────────────────────────────┬─────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ PLATFORM LAYER │
│ ┌───────────┐ ┌───────────┐ ┌─────────┐ ┌───────────┐ │
│ │ Adapters │ │ Runtime │ │ Config │ │Observabil.│ │
│ │ DTO↔State│ │ helpers │ │ env/paths│ │ logging │ │
│ └─────┬─────┘ └─────┬─────┘ └────┬────┘ └─────┬─────┘ │
│ │ │ │ │ │
│ └─────────────┴──────┬─────┴────────────┘ │
│ ▼ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Core (PURE – no framework dependencies) │ │
│ │ - Contracts and validators │ │
│ │ - Policy evaluation (pure functions) │ │
│ │ - DTOs (frozen dataclasses) │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
규칙: core/는 위쪽 레이어에서 아무것도 임포트하지 않습니다—앱 오케스트레이션(에이전트, 노드, 그래프 등), 배선, 어댑터가 없습니다. 의존성은 오직 내부로만 향합니다.
Source: …
가이드라인을 어떻게 적용할까?
간단합니다: 위반을 잡아낼 테스트를 작성합니다:
# tests/unit/architecture/test_core_purity.py
FORBIDDEN_IMPORTS = [
"app.state",
"app.graphs",
"app.nodes",
"app.agents",
# ... all app orchestration and platform wiring
]
def test_core_has_no_forbidden_imports():
"""Core 레이어는 순수해야 합니다 – 배선 의존성이 없어야 합니다."""
core_files = Path("app/platform/core").rglob("*.py")
for file in core_files:
content = file.read_text()
for forbidden in FORBIDDEN_IMPORTS:
assert forbidden not in content, (
f"{file} imports {forbidden} – core must stay pure"
)
경계를 어기면 테스트가 실패합니다. 예외는 없습니다.
가이드라인 외에도 런타임에 검증하는 계약을 정의할 수 있습니다.
검증 계약
core/contract/ 디렉터리에는 런타임에 계약 규칙을 강제하는 검증기가 들어 있습니다:
| 계약 | 수행 내용 |
|---|---|
validate_state_update() | 권한이 있는 소유자만 변이를 할 수 있도록 제한 |
validate_structured_response() | 영속화 전에 검증을 강제 |
validate_phase_registry() | 단계 키가 선언된 스키마와 일치하는지 확인 |
validate_allowlist_contains_schema() | 툴 허용 목록의 정확성을 보장 |
이것들은 선택 사항이 아닙니다 – 모든 노드가 호출합니다:
# Every state update goes through the contract
update = {"phases": {phase_key: phase_entry}}
validate_state_update(update, owner="problem_framing")
return Command(update=update, goto=next_node)
계약 자체도 테스트됩니다 (검증 로직, 단계 의존성, 무효화 연쇄). 전체 테스트 스위트는
test_state.py에서 확인하세요.
확장 가능한 테스트 구조
테스트는 유형(단위, 통합, e2e)과 카테고리(아키텍처, 오케스트레이션, 플랫폼)별로 조직됩니다. 이렇게 하면 커버리지 빈틈이 명확해지고 원하는 부분만 선택적으로 실행할 수 있습니다.
tests/
├── unit/
│ ├── architecture/ # 경계 강제
│ │ ├── test_core_purity.py
│ │ ├── test_adapter_boundary.py
│ │ └── test_import_time_construction.py
│ ├── orchestration/ # 에이전트, 노드, 그래프
│ └── platform/ # 코어 + 어댑터
├── integration/
│ ├── orchestration/
│ └── platform/
└── e2e/
Pytest 마커
# pyproject.toml
# 테스트 목적과 범위에 따라 카테고리화하기 위한 마커
markers = [
# 테스트 유형 마커 (범위별)
"unit: Fast, isolated tests with no external dependencies",
"integration: Tests crossing component boundaries (may use test fixtures)",
"e2e: End‑to‑end workflow tests (full pipeline validation)",
# 테스트 카테고리 마커 (조직적 구분)
"architecture: Hexagonal architecture enforcement (import rules, layer boundaries)",
"orchestration: LangGraph orchestration components (agents, nodes, graphs, middlewares, tools)",
"platform: Platform layer tests (hexagonal architecture – core, adapters, runtime)",
]
단위‑아키텍처 테스트만 실행하려면:
uv run pytest -m "unit and architecture"
아키텍처는 110개의 테스트로 검증됩니다 – 그 중 11개는 특별히 아키텍처 경계를 강제합니다.
이것이 가능하게 하는 것
여기서 흥미로운 부분이 나옵니다.
당신은 이렇게 생각할지도 모릅니다: 멋진 이야기지만, …

당신의 아키텍처가 예측 가능하고 강제될 때, 흥미로운 일이 일어납니다: 코딩 에이전트가 위험 요소가 아니라 유용한 존재가 됩니다.
- 모든 노드가 동일한 패턴을 따를 때…
- 모든 상태 업데이트가 검증자를 거칠 때…
- 모든 경계가 명확히 정의되고 테스트될 때…
…AI 에이전트는 테스트가 잡아내지 않는 한 실수로 당신의 아키텍처를 깨뜨릴 수 없습니다. 금지된 모듈을 가져오거나, 검증을 건너뛰거나, 계약을 우회할 수 없으며, 그렇게 하면 테스트 스위트를 통과하지 못합니다.
규칙은 단순한 문서 이상이 됩니다; 인간과 AI 모두를 위한 가드레일이 됩니다.
전체 내용이 필요하신가요?
- 46개의 아키텍처 원칙 (계층별): Architecture principles
- 플랫폼 계약 README: Platform contracts
- 아키텍처 테스트: Architecture tests
다음 단계
Claude Code를 깨지 못하는 아키텍처에 적용하면 어떻게 될까요.
CLAUDE.md 파일은 단순히 명령어들의 집합이 아니라, 개발 과정에서 컨텍스트를 보존하고 경계를 강제하는 계약입니다. 저는 이를 위해 측정 가능한 결과를 얻는 프레임워크를 구축했습니다.
다음: CLAUDE.md 성숙도 모델.
이 글은 SageCompass 구축 과정을 기록한 “프롬프트에서 플랫폼까지” 시리즈의 일부입니다. 프롤로그부터 시작하기.