research

Architectural Constraints — 구조적 강제

Architectural Constraints — 구조적 강제

핵심 아이디어

Skill이 "이렇게 해야 한다"고 말해주는 것이라면, Architectural Constraints는 "이렇게 하지 않으면 빌드가 깨진다"는 것이다.

권고가 아니라 강제.

Skill:   "레이어 간 의존성을 지켜라" → agent가 따를 수도, 무시할 수도 있음
AC:      structural test가 위반 감지 → CI 실패 → merge 불가

세 가지 수단

1. 레이어드 아키텍처 + 의존성 방향 강제

OpenAI 사례:

Types → Config → Repo → Service → Runtime → UI

각 레이어는 왼쪽 레이어만 import 가능. ServiceUI를 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가 그 규칙이 실제로 지켜지는지 검증한다

관련 개념

"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 체크리스트 병행이 안전하다.