Architectural Constraints — 구조적 강제
핵심 아이디어
Skill이 "이렇게 해야 한다"고 말해주는 것이라면, Architectural Constraints는 "이렇게 하지 않으면 빌드가 깨진다"는 것이다.
권고가 아니라 강제.
Skill: "레이어 간 의존성을 지켜라" → agent가 따를 수도, 무시할 수도 있음
AC: structural test가 위반 감지 → CI 실패 → merge 불가
세 가지 수단
1. 레이어드 아키텍처 + 의존성 방향 강제
OpenAI 사례:
Types → Config → Repo → Service → Runtime → UI
각 레이어는 왼쪽 레이어만 import 가능.
Service가 UI를 import하면 structural test가 잡는다.
왜 필요한가: agent는 "빠른 해결"을 선호해서 레이어를 무시하고 직접 참조하는 코드를 짜는 경향이 있다. 강제하지 않으면 시간이 지날수록 의존성이 뒤엉킨다.
2. 커스텀 Linter
표준 lint 외에 프로젝트 고유의 규칙을 코드로 정의:
- "이 모듈에서는 raw SQL을 직접 쓰지 말 것"
- "외부 API 호출은 반드시 retry 로직을 포함할 것"
- "이 디렉토리의 파일은 100줄을 넘지 말 것"
agent가 규칙을 어기면 lint 실패 → 진행 불가.
3. CI Validation
structural test + lint를 CI 파이프라인에 넣어 PR merge 전 자동 검증. agent가 CI를 돌리고 결과를 보면서 스스로 수정하는 루프 형성.
v2 문제와의 관계
제약 없는 경우:
v1 → 잘 작동
v2 → agent가 편의상 레이어 무시 → 의존성 오염
v3 → 오염 위에 오염 → 리팩토링 비용 폭발
제약 있는 경우:
v1 → 잘 작동
v2 → agent가 레이어 무시 시도 → structural test 실패
→ agent 스스로 수정 → 깨끗한 구조 유지
v3 → 동일하게 유지
인간이 매번 "레이어 지켰어?" 확인하지 않아도 시스템이 알아서 막아준다.
구체적인 적용 방법
Python 예시 (레이어 의존성 검사)
# tests/test_architecture.py
import ast, os
LAYER_ORDER = ["types", "config", "repo", "service", "runtime"]
def get_imports(filepath):
with open(filepath) as f:
tree = ast.parse(f.read())
return [n.names[0].name.split('.')[0]
for n in ast.walk(tree)
if isinstance(n, ast.Import)]
def test_layer_dependencies():
for layer_idx, layer in enumerate(LAYER_ORDER):
layer_path = f"src/{layer}"
if not os.path.exists(layer_path):
continue
for root, _, files in os.walk(layer_path):
for file in files:
if not file.endswith('.py'):
continue
imports = get_imports(os.path.join(root, file))
for imp in imports:
if imp in LAYER_ORDER:
imp_idx = LAYER_ORDER.index(imp)
assert imp_idx < layer_idx, \
f"{layer}/{file} imports {imp} — 레이어 위반"
Architectural Constraints 후보 선정 기준
"이건 항상 지켜야 하는데 매번 agent에게 말해야 한다" → Constraint 후보
예시:
- DB 접근은 반드시 레포 레이어를 통할 것
- 설정값은 config 모듈에서만 읽을 것
- 특정 디렉토리는 특정 모듈만 건드릴 것
→ Skill에 쓰는 대신 test 코드로 표현하면 강제가 된다.
Harness 레이어와의 관계
- Architectural Constraints는 Knowledge layer와 Execution layer 사이의 경계를 기계적으로 강제하는 장치
- Skill이 규칙을 정의하면, AC가 그 규칙이 실제로 지켜지는지 검증한다
관련 개념
- overview — Harness Engineering 전체 지도
- harness-layer-structure — 레이어 구조 전체
- context-history-documentation — Knowledge layer 설계 (ADR, Skill)
- garbage-collection-agents — AC 위반을 주기적으로 찾는 agent
"test 코드로 표현한다"는 것의 의미
Skill(md) vs. 테스트 코드의 차이
# Skill로 쓰는 경우 (AGENTS.md)
- DB 접근은 반드시 repo/ 레이어를 통할 것
- service/ 에서 sqlite3를 직접 import하지 말 것
→ agent가 읽고 따를 수도, 잊을 수도 있다. 강제가 아니다.
# 테스트 코드로 쓰는 경우 (tests/test_architecture.py)
def test_service_does_not_import_sqlite():
violations = []
for root, _, files in os.walk("src/service"):
for f in files:
if not f.endswith(".py"): continue
tree = ast.parse(open(os.path.join(root, f)).read())
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for alias in node.names:
if alias.name == "sqlite3":
violations.append(f"{root}/{f}")
assert not violations, f"sqlite3 직접 import 금지: {violations}"
# → agent가 위반하는 순간 pytest 실패 → CI 실패 → merge 불가
핵심 차이
- Skill: 말로 규칙을 알려준다
- AC test: 위반하면 빌드가 깨진다 — 코드가 막는다
파일 형태 — MD가 아니다
AC의 실체는 실제 코드 파일이다.
| 언어 | 파일 위치 | 도구 |
|---|---|---|
| Python | tests/test_architecture.py | pytest + ast 모듈 |
| TypeScript/JS | tests/architecture.test.ts | jest + dependency-cruiser |
| Java | src/test/.../ArchitectureTest.java | ArchUnit |
| 언어 무관 | .github/workflows/ci.yml | CI에서 자동 실행 |
프로젝트 구조
프로젝트/
├── tests/
│ ├── test_architecture.py ← AC 정의 (Python 코드)
│ └── test_unit.py ← 일반 테스트
├── .github/
│ └── workflows/
│ └── ci.yml ← AC를 자동 실행
└── AGENTS.md ← "tests/test_architecture.py를 항상 통과시켜라"
AGENTS.md는 "이 파일을 통과시켜라"고 안내만 한다.
실제 규칙은 .py 파일에 있다.
실행 흐름
인간이 규칙을 결정
↓
test_architecture.py에 코드로 표현 (한 번만)
↓
agent가 코드를 짤 때마다 pytest 실행
↓
위반 → 테스트 실패 → agent가 스스로 수정
↓
인간 개입 없음
미연에 다 적을 필요 없다 — AC는 진화하는 시스템
핵심 전환
AC는 선제적으로 완성하는 시스템이 아니라, 사후에 진화하는 시스템이다.
agent가 규칙을 어김 (틈새 발견)
↓
인간이 "이건 항상 막아야 하네" 판단
↓
test_architecture.py에 테스트 추가
↓
이 실수는 영원히 재발 불가
↓
(반복)
법이 사건 이후에 만들어지는 것처럼, AC도 위반이 발견된 후에 추가한다.
법 비유의 한계와 AC의 유리한 점
| | 법 | AC | |---|---|---| | 규칙 | 자연어 | 코드 | | 집행 | 불완전 (판사, 집행 기관) | 100% 기계적 (CI) | | 해석 오류 | 있음 | 없음 | | 틈새 발생 원인 | 해석의 여지 | 아직 규칙화 안 된 영역만 |
틈새는 여전히 존재하지만, 발견 즉시 규칙으로 메울 수 있다.
세 단계 방어선
1단계: AC test — 알려진 규칙을 기계적으로 강제
2단계: GC agent — AC에 없는 위반을 주기적으로 탐색
3단계: 인간 검토 — 예외 처리, 새 발견 → 1단계에 추가
발견 → 규칙화 → 자동화 → (반복)
↑___________________________|
실용적 접근
- 프로젝트 시작: AC 3개여도 괜찮다
- 시간이 지날수록 실제 위반에서 나온 규칙이 쌓인다
- 현실에서 검증된 규칙 10개 > 미리 설계한 완벽한 규칙 50개
Git의 역할 — 안전망 + 맥락 전달
1. 안전망
agent가 코드를 망가뜨렸을 때 돌아올 수 있는 체크포인트. coding agent가 매 세션 끝에 commit을 강제하는 이유.
세션 1: 기능 A 구현 → commit
세션 2: 기능 B 구현 중 망함 → git revert → 세션 1 상태로 복구
세션 3: 다시 시도
agent가 스스로 git log → "여기서 잘못됐네" → git revert로 복구하는 루프를 돌 수 있다.
2. 맥락 전달
claude-progress.txt와 함께 git log가 새 세션 agent에게 히스토리를 전달한다.
git log --oneline -20
# → "지난 세션에서 인증 모듈 만들고, 그 다음 DB 연결 추가했구나"
Commit 단위가 중요한 이유
feature 하나 = commit 하나를 강제하는 이유:
좋은 경우:
feat: 로그인 기능 구현 (feature_list #3)
feat: 세션 토큰 검증 추가 (feature_list #7)
→ 문제 생기면 딱 그 commit만 되돌리면 됨
나쁜 경우:
feat: 여러 기능 한꺼번에 구현
→ 어느 부분이 문제인지 모름, 되돌리면 다른 것도 날아감
세 방어선의 작동 시점
AC test → 위반을 사전에 차단 (빌드 시)
Git → 위반이 빠져나갔을 때 복구 (실행 후)
GC agent → 장기간 누적된 drift 발견 (주기적)
AC가 막지 못한 게 들어왔을 때, git이 되돌아갈 수 있는 여지를 준다.
Claude Code에서 지시를 무시하는 문제
왜 system prompt를 무시하는가
"progress.md 저장해라"는 권고다. 작업에 집중하다 보면 세션 끝 정리 작업이 희석된다. Context rot의 실제 사례 — context가 길어지면서 앞에 있던 지시의 영향력이 약해진다.
도구 선택
| 상황 | 맞는 도구 | |---|---| | 코드 구조 위반 | AC test | | 세션 종료 행동 강제 | Hooks | | Hooks 없을 때 | AGENTS.md 종료 체크리스트 | | 제품 기능 완료 추적 | feature_list.json |
Hooks — agent 의지와 무관하게 강제 실행
// .claude/settings.json
{
"hooks": {
"Stop": [
{
"type": "command",
"command": "python scripts/save_progress.py"
}
]
}
}
세션 종료 시 자동 실행. Claude Code가 까먹어도 상관없다. System prompt는 "부탁", Hook은 "강제 실행".
AGENTS.md 체크리스트 패턴 (Hooks 없을 때)
## 세션 종료 전 반드시 할 것 (이걸 안 하면 작업 미완성)
- [ ] git commit
- [ ] claude-progress.txt 업데이트
- [ ] feature_list.json passes 필드 업데이트
"이걸 안 하면 작업 미완성"이 핵심. "해라"보다 완료의 기준을 명확히 제시하는 게 더 효과적이다.
Hooks + LLM으로 progress 자동 저장
흐름
Claude Code 세션 종료 (Stop 이벤트)
↓
Hook → save_progress.py 실행
↓
git diff / git log로 변경사항 수집
Claude API 호출 → 요약 생성
progress.md에 append
구현 예시
# scripts/save_progress.py
import subprocess, anthropic
from datetime import datetime
git_log = subprocess.run(
["git", "log", "--oneline", "-5"], capture_output=True, text=True
).stdout
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=500,
messages=[{
"role": "user",
"content": f"""
git 정보를 바탕으로 progress.md에 추가할 세션 요약 작성:
{git_log}
형식:
## {datetime.now().strftime('%Y-%m-%d')} 세션
- 완료:
- 다음:
- 주의:
"""
}]
)
with open("claude-progress.txt", "a") as f:
f.write("\n" + response.content[0].text)
한계
Stop 이벤트가 항상 신뢰할 수 있는 타이밍에 발생하지 않을 수 있다. 강제 종료 시 Hook이 실행 안 될 수도 있음.
→ Hook + AGENTS.md 체크리스트 병행이 안전하다.