Structured Coding Rules — 에이전트 지향 코드 작성 규칙
Structured Coding Rules — Agent-Oriented Code Style
이 문서의 목적 Purpose
LLM 또는 에이전트가 코드를 작성할 때 일관되게 따라야 할 구조 규칙입니다. 모듈 단일 책임, 레이어 방향, async-first 동시성, 회로 차단·타임아웃, fail-fast 에러 처리, dry-run 시뮬레이션, 구조화 로그까지 — Pre-Code 체크리스트(§10) 7항목으로 환원됩니다. 본문 전체를 복사해 메모리·시스템 프롬프트에 붙여 넣으면, 이후 모든 코드 생성이 이 규칙을 따른다고 가정할 수 있습니다.
Structural rules to follow whenever an LLM or agent writes code. Single responsibility, layer direction, async-first concurrency, circuit breaker / timeout, fail-fast error handling, dry-run simulation, structured logging — all reducible to the seven items of the Pre-Code Checklist (§10). Copy this body into memory or the system prompt and treat all subsequent code generation as governed by it.
1. 모듈 설계 1. Module Design
| 규칙 | Rule | 세부 내용 | Detail |
|---|---|---|---|
| Single Responsibility | Single Responsibility | 한 모듈 = 하나의 능력. 한 단위에 transport·logic·storage를 섞지 않는다. | One module = one capability. Never mix transport, logic, storage in the same unit. |
| Interface Contract | Interface Contract | 구현 전에 입출력 타입을 정의한다. 호출자는 내부를 알지 않아야 한다. | Define input/output types before implementation. Caller must not know internals. |
| Dependency Direction | Dependency Direction | High-level → Low-level. 역방향 금지. 의존성은 주입(inject)하고 직접 import하지 않는다. | High-level → Low-level only. Never reverse. Inject dependencies; don't hard-import. |
| Bounded Size | Bounded Size | 함수 ≤ 30줄, 파일 ≤ 300줄. 위반 시 분해 먼저. | Function ≤ 30 lines. File ≤ 300 lines. Violating either = decompose first. |
레이어 순서 (호출은 아래 방향으로만):
Layer order (call downward only):
orchestrator → agent → service → client → storage
2. 에이전트 구조 2. Agent Structure
모든 에이전트는 다음 세 메서드를 가져야 합니다.
Every agent must have:
init() — resource acquisition, idempotent run() / execute() — single entry point, returns structured result shutdown() — graceful cleanup, always callable
상태 규칙
State Rules
- 에이전트는 자신의 상태만 소유. 에이전트 간 가변 공유 상태 금지.
- Agent owns its state; no shared mutable state between agents.
- 통신은 반환값 또는 메시지 큐로. 전역 변수 사용 금지.
- Communicate via return values or message queues, not globals.
- 외부 상태(DB·API)는 전용 service 레이어를 통해서만 접근.
- External state (DB, API) accessed only through a dedicated service layer.
3. 동시성 3. Concurrency
| 패턴 | Pattern | 사용 시점 | When |
|---|---|---|---|
async/await | async/await |
I/O-bound (API·DB·파일). 기본 선택지. | I/O-bound (API calls, DB, file) — default choice. |
ThreadPoolExecutor(max_workers=N) |
ThreadPoolExecutor(max_workers=N) |
async로 만들 수 없는 blocking I/O. N은 명시적으로 제한. |
Blocking I/O that can't be made async; bound N explicitly. |
ProcessPoolExecutor | ProcessPoolExecutor |
CPU-bound 작업 전용. | CPU-bound only. |
asyncio.gather() | asyncio.gather() |
같은 이벤트 루프에서 병렬 fan-out. | Fan-out parallel tasks with shared event loop. |
강행 규칙
Hard Rules
- 락 없이 가변 객체를 스레드 간 공유하지 않는다.
- Never share mutable objects across threads without a lock.
- 모든 외부 호출에 명시적 timeout을 둔다.
- Always set explicit timeout on every external call.
- 모든 thread/process 풀에 상한을 둔다. 무한 풀 = 자원 누수.
- Bound all thread/process pools — unbounded = resource leak.
- polling/wait 루프에는 상한을 명시. 외부 출력 파싱으로 끝나는 종료 조건은 형식이 바뀌면 무한 루프가 된다. 다음 중 하나를 사용:
- Polling/wait loops require an explicit upper bound. Termination conditions that parse external output silently turn into infinite loops if the format changes. Use one of:
# 1) Counter for i in $(seq N); do COND && break; sleep K; done # 2) timeout wrapper timeout N bash -c 'until COND; do sleep K; done' # 3) Deadline end=$(($(date +%s) + N)) while [ $(date +%s) -lt $end ]; do ...; done
4. 회복탄력성 (Resilience) 4. Resilience
Circuit Breaker — track fail count; trip after threshold; auto-reset after cooldown Retry — exponential backoff, max 3 attempts, idempotent ops only Timeout — every network/IO call has explicit deadline Fallback — define degraded behavior before writing the happy path
모든 에이전트는 다음 헬스 인터페이스를 노출합니다.
Every agent exposes:
health() → {"status": "ok" | "degraded" | "tripped", "fails": int}
5. 에러 처리 5. Error Handling
- Fail fast: 입력 검증은 깊은 곳이 아니라 경계(에이전트 진입점)에서 한다.
- Fail fast: validate inputs at the boundary (agent entry point), not deep inside.
- Guard clauses 우선: 잘못된 상태는 즉시 early return — 깊은 nesting 금지.
- Guard clauses first: early return on invalid state — no deep nesting.
- 예외 삼키기 금지:
except Exception: pass는 금지. 로그+재발생 또는 에러 타입 반환. - Never swallow:
except Exception: passis banned; log + re-raise or return error type. - Typed errors: 호출자가 판단해야 하는 경우 raise 대신 구조화된 에러 반환 —
{"ok": False, "error": "reason"} - Typed errors: return structured error
{"ok": False, "error": "reason"}over raising where caller must decide.
6. 네이밍과 상수 6. Naming & Constants
- 이름은 목적을 명시:
fetch_balance_with_ttl()—get_data()금지. - Names state purpose:
fetch_balance_with_ttl(), notget_data(). - 매직 넘버 금지:
MAX_RETRIES = 3,TTL_SECONDS = 60. - No magic numbers:
MAX_RETRIES = 3,TTL_SECONDS = 60. - 비동기 함수는 동작 동사로 시작:
fetch_,send_,load_. 동기 헬퍼는_parse_,_validate_. - Async functions prefixed with action verb:
fetch_,send_,load_; sync helpers:_parse_,_validate_.
7. 유지보수성 (독립 수정 가능성) 7. Maintainability — Independent Modification
목표: 한 모듈을 다른 모듈을 건드리지 않고 변경 가능.
Goal: change one module without touching any other.
| 규칙 | Rule | 강제 방법 | How to enforce |
|---|---|---|---|
| 추상에 의존 | Depend on abstractions | 호출자는 인터페이스/프로토콜에 의존, 구체 클래스에 의존하지 않음. 호출자 변경 없이 구현 교체 가능. | Caller depends on an interface/protocol, not a concrete class. Swap implementation without changing caller. |
| 내부 은닉 | Hide internals | 공개 계약에 없는 것은 _private 접두. 다른 모듈의 내부 상태에 손대지 않음. |
_private prefix for anything not in the public contract. Never reach into another module's internal state. |
| 모듈 간 부수효과 금지 | No cross-module side effects | 다른 모듈 소유 상태를 바꾸지 말 것. 값을 반환하고, 소유자가 결정하게. | A module must not mutate state owned by another module. Return a value; let the owner decide. |
| 설정은 경계에 | Configuration at the boundary | 상수·임계치·엔드포인트는 config/env에. 코드를 건드리지 않고 동작 변경 가능하게. | Constants, thresholds, endpoints live in config/env — not buried in logic. |
| 인터페이스는 안정, 내부는 변동 | Stable interfaces, volatile internals | 인터페이스 시그니처 = 계약, 거의 안 바뀜. 구현 = 언제든 리팩토링 가능. | Interface signature = contract, rarely changes. Implementation = free to refactor anytime. |
수정 테스트 (머지 전): "이 모듈을 같은 인터페이스로 처음부터 다시 작성한다면, 다른 파일을 건드려야 하는가?" → Yes면 경계가 잘못된 것.
Modification test (before merging): "If I rewrote this module from scratch with the same interface, would any other file need to change?" → If yes, the boundary is wrong.
8. 모듈 격리와 시뮬레이션 8. Module Isolation & Simulation
동작 모듈과 깨진 모듈을 구분하는 법
Distinguishing Working vs Broken Modules
모든 모듈은 다음을 구현합니다.
Every module must implement:
smoke_test() → {"ok": bool, "module": str, "checks": [{"name", "ok", "detail"}]}
- 자기 자신의 의존성만 검사 (DB 도달 가능? API 키 유효? config 로드?)
- Tests own dependencies only (DB reachable? API key valid? config loaded?)
- 2초 미만, 부수효과 없음.
- Runs in < 2s, no side effects.
health()= 런타임 회로 상태.smoke_test()= 이 모듈이 동작 가능한가?health()= runtime circuit state;smoke_test()= can this module operate at all?
병렬 에이전트 디버깅 패턴:
Scan pattern for debugging parallel agents:
results = await asyncio.gather(*[m.smoke_test() for m in all_modules]) broken = [r for r in results if not r["ok"]] # isolate failing modules immediately
시뮬레이션 / Dry-Run
Simulation / Dry-Run
외부 시스템에 접촉하는 모든 모듈은 dry_run: bool = False를 받습니다.
Every module that touches external systems accepts dry_run: bool = False:
dry_run=False (production) |
dry_run=True (simulation) |
|---|---|
| 실제 API 호출 | 의도를 로그로 남기고 mock 응답 반환 |
| Real API call | Log intent, return mock response |
| DB write | SQL/payload만 로그, write 생략 |
| DB write | Log SQL/payload, skip write |
| 메시지 발송 | stdout 출력, 발송 생략 |
| Message send | Print to stdout, skip send |
- 모든 로직은 dry_run에서도 실행 — 마지막 외부 호출만 생략한다.
- All logic executes in dry_run — only the final external call is skipped.
- dry_run 출력 구조는 실제와 동일해야 한다 (같은 키, 현실적인 값).
- dry_run output must be identical in structure to real output (same keys, realistic values).
- 리팩토링 없이 dry_run을 추가할 수 없다면 외부 호출이 너무 깊다 — 추출하라.
- If you can't add dry_run without refactoring, the external call is too deep — extract it.
Mock 주입 지점
Mock Injection Point
의존성 주입(§1)이 mock 경계입니다. 클라이언트를 hard-import하면 mock 불가능 — 경계를 먼저 고치세요.
Dependency injection (§1) is the mock boundary. If a module hard-imports a client, it cannot be mocked — fix the boundary first.
# Bad: from external_api import client; client.call(...) # Good: def __init__(self, client): self._client = client # inject mock in tests/dry_run
9. 관측성 (디버깅 용이성) 9. Observability — Debuggability
구조화 로그 포맷 — 자유 텍스트 로그는 금지:
Structured log format — free-text logs are banned:
{agent, operation, status, duration_ms, error, inputs_summary}
| 로깅 시점 | When to log | 내용 | What |
|---|---|---|---|
| 외부 호출 직전 | Every external call (before) | agent, operation, key inputs |
agent, operation, key inputs |
| 외부 호출 직후 | Every external call (after) | + status (ok/fail), duration_ms |
+ status (ok/fail), duration_ms |
| 에이전트 상태 전이 | Agent state transition | prev_state → new_state, reason |
prev_state → new_state, reason |
| 에러 | Error | error type, message, inputs that caused it |
error type, message, inputs that caused it |
컨텍스트 전파
Context Propagation
- 모든
run()은 선택적request_id/trace_id를 받는다. - Every
run()accepts optionalrequest_id/trace_id. - 아래 호출 전부에 그대로 내려보낸다 — 중간에 새로 생성하지 않는다.
- Pass it down to every child call — never generate a new one mid-flow.
- 병렬 태스크는 같은 부모
trace_id와 고유한task_id를 가진다. - Parallel tasks: each gets the same parent
trace_id+ uniquetask_id.
에러 메시지가 답해야 할 것: 입력은 무엇이었는가, 무엇이 실패했는가, 어디에서.
Error messages must answer: what was the input, what failed, where.
# Bad: raise ValueError("balance fetch failed")
# Good: raise ValueError(f"balance fetch failed: account={acct}, status={resp.status}")
디버깅 가능성 테스트: "로그만으로 코드 실행 없이 실패를 재현할 수 있는가?" → No면 컨텍스트를 더 추가하라.
Debuggability test: "Given only the logs, can I reproduce the failure without running the code?" → If no, add more context to the error/log.
10. Pre-Code 체크리스트 10. Pre-Code Checklist
함수 한 줄을 쓰기 전에 7항목을 통과해야 합니다.
Before writing any function, pass these seven checks:
- Layer — 이 함수는 어느 레이어에 속하는가?
- Layer — which layer does this belong to?
- Contract — 시그니처와 반환 타입을 먼저 정의했는가?
- Contract — define signature + return type first.
- Concurrency — async or sync? 공유 상태 위험은?
- Concurrency — async or sync? shared state risk?
- Failure modes — 무엇이 깨지는가? timeout? null? 부분 결과?
- Failure modes — what breaks? timeout? null? partial result?
- Independence — 호출자를 바꾸지 않고 이 모듈을 다시 쓸 수 있는가?
- Independence — can this module be rewritten without changing its callers?
- Observability — 운영에서 디버그할 때 어떤 로그·trace_id가 필요한가?
- Observability — what logs/trace_id will I need to debug this in production?
- Self-check — 단일 책임? 가변 공유 상태 없음? timeout 설정?
- Self-check — single responsibility? no shared mutable state? timeout set?
이 7항목을 모두 통과한 코드만이 "구조화된 코드"입니다. 하나라도 미통과면 머지 전에 되돌아가세요.
Only code that passes all seven items qualifies as "structured". Miss any one, and circle back before merging.
