“DDD로 구현해 달라”고 요청하면 에이전트는 자신이 아는 방식으로 구조를 잡는다. 다음 에이전트도 그렇게 한다. 둘 다 DDD를 따른다고 설명했지만, 같은 역할의 파일이 서로 다른 폴더에 놓였다.
FSD를 쓰면서도 코드 위치를 고민했던 이유에서는 아키텍처 전환 뒤에도 남은 과제를 적었다. 폴더 나누는 기준을 FSD에서 DDD로 바꾼 뒤에도 그 규칙을 도구로 강제하지는 못했다. 사람하고 일할 때는 지난주 리뷰에서 나눈 말을 서로 기억하니 그럭저럭 됐다. 앞 글에서는 재사용 빈도를 기준으로 삼은 게 원인이었지만 이번 건은 원인이 더 앞에 있었다. 합의한 구조를 문서에 남기지 않았다는 점이다.
정한 구조를 어디에 적어 둬야 에이전트가 읽고 그대로 짜는가.
curvez의 architecture-setup은 정한 구조를 .curvez/architecture.md 파일 하나에 적어 둔다.
1. 말로 넘긴 구조는 어디까지 가나
스킬 문서 첫 줄이 이렇게 시작한다.
아키텍처 확정은 설계가 아니라 선택과 기록이다.
아키텍처를 정하려면 고민부터 해야 할 것 같지만, 이 스킬은 그 고민을 줄이려고 만들었다. 프리셋 하나를 골라 팀 어휘에 맞춰 몇 군데만 튼 다음, 기계가 검사할 수 있는 금지 import 목록으로 박아 둔다.
언제 쓰라고 적혀 있는지를 보면 의도가 더 분명하다. 목록의 마지막 줄이 “구현 에이전트가 ‘이 파일을 어느 레이어에 두는가’ 로 매번 다르게 판단할 때” 다. 문서가 없어서 생기는 문제라 문서를 만들어 막겠다는 거다.
2. 고를 프리셋이 하나뿐이다
curvez가 가진 아키텍처 프리셋은 ddd 하나다. 스킬 2단계는 “고를 것이 없으므로 이 단계는 확인만 하고 지나간다”로 되어 있다. 한 줄 더 붙어 있다. 다른 구조를 쓰고 싶다는 요청이 없으면 되묻지 마라.
규모가 작을 때도 프리셋을 바꾸지는 않는다. 화면과 업무 규칙 사이에서 흐름만 조립하는 application 레이어를 빼고 presentation → domain 두 층으로 줄인다. 레이어를 삭제하는 쪽이라 비용도 적다. 판정은 취향이 아니라 수치로 하는데, 라우트 수와 엔티티 수 양쪽이 기준 미만이면 두지 않고 하나라도 넘으면 둔다.
그런데 이 숫자가 두 곳에서 다르다. 프리셋 ddd.md는 “라우트 6개 이하 그리고 엔티티 4개 이하” 다. 에이전트 정의 curvez-architect.md는 “라우트 12개 미만이고 엔티티 8개 미만”이다. 프리셋 쪽은 “정확한 값은 에이전트 정의가 정본”이라고 넘겨 놨다. 정본을 따르면 12/8이다. 두 파일의 값이 다르다는 사실은 확인한 그대로 적어 둔다.
DDD를 안 쓰겠다고 하면 대안 프리셋이 없다. 그 프로젝트의 구조를 인터뷰로 처음부터 만든다. 산출물 형식은 똑같다.
프리셋이 없다는 것은 초안이 없다는 뜻이지 규약이 없다는 뜻이 아니다.
그렇게 만든 구조는 다음 프로젝트로 전파되지 않는다. 검증되지 않은 구조가 프리셋으로 올라가면 기본값으로 굳어서다.
3. 인터뷰에서 무엇을 묻나
인터뷰는 3문 이상 5문 이하다. 레이어명과 경계 규칙만 조정하고 프리셋 자체는 재설계하지 않는다. 후보로 올라와 있는 문항은 이런 것들이다.
- 레이어 이름을 팀 용어로 바꿀 것인가 (
application→usecase같은) - 공유 코드를 어느 레이어에 두고 무엇까지 넣을 것인가
- 경계 예외를 허용할 지점이 있는가, 있다면 어디까지인가
- (monorepo) 앱 간 공유를 패키지로 자를 것인가 폴더로 자를 것인가
묻지 않는 것도 못박아 뒀다. 어떤 기능을 만드는가는 requirements가 이미 정했다. 라이브러리는 researcher 담당이다. 폴더 이름의 단복수는 답이 무엇이든 경계가 안 바뀐다. 도메인이 프레임워크를 참조해도 되는가는 아예 협상 대상이 아니라 항상 금지다.
5문까지만 묻는 이유도 적혀 있다. 6문째부터는 프리셋 없이 처음부터 설계하는 것과 같아지고 사용자가 근거 없이 답을 지어내기 시작한다. 그렇게 나온 답이 문서에 확정으로 굳으면 되돌릴 근거조차 안 남는다.
서브에이전트는 사용자에게 직접 못 묻는다. 그래서 경로가 둘이다. 워커를 띄우기 전에 오케스트레이터가 5문을 한 번에 물어 답을 프롬프트에 실어 보낸다. 아니면 이미 도는 워커가 blocked_on에 질문을 담아 반환하고 오케스트레이터가 대신 묻고 재기동한다. 둘 다 안 되면 기본값으로 확정하고 건너뛴 문항마다 ## 결정 로그에 되돌릴 위치를 적는다.
4. 문서에 무엇이 들어가는가
.curvez/architecture.md는 헤딩 일곱 개를 이 문자열 그대로 담는다.
## 레이어 정의 ## 의존 방향 ## 금지 import ## 폴더 구조 ## 스택 매핑 ## 예외 ## 결정 로그
결정 로그의 되돌릴 위치를 .curvez/architecture.md:<헤딩> 형식으로 적기 때문에 문자열을 고정해 뒀다. 헤딩이 곧 앵커이고 자체 검증 grep도 이 문자열을 찾는다.
일곱 중 절차가 실제로 값을 꺼내 쓰는 건 ## 금지 import 표다. 열 순서가 고정이고 세 번째 열이 grep -E에 그대로 들어간다.
| 규칙 ID | 검사 경로 | 금지 패턴 (ERE) | 이유 |
| ARCH-001 | src/domain/ | from ['\"](next\|next/.*\|react\|react-dom) | 도메인은 프레임워크 교체에서 분리돼야 한다 |
이 한 줄의 뜻은 이렇다. src/domain/ 아래 파일에 from 'next' 나 from 'react' 같은 줄이 있으면 위반이다.
ARCH-001은 규칙에 붙인 번호다. 리뷰에서 말로 하던 “이건 domain에 두면 안 돼요”를 번호로 부를 수 있게 된다. 번호는 ARCH-001부터 이어 붙이고 최소 3건이다. 프리셋 기본 규칙은 도메인의 프레임워크 독립을 세 갈래로 나눠 검사한다. 프레임워크(001·002), 레이어 방향(003·004), I/O(005·006·007) 다. 하나만 빠져도 나머지 둘이 통과하면서 도메인은 독립이 아니게 된다.
함정도 하나 적혀 있다. 패턴 안의 |는 \|로 이스케이프한다. 안 하면 마크다운 표가 그걸 열 구분자로 읽는다. 게다가 검증 스크립트의 awk가 필드 구분자를 공백-파이프-공백으로 잡는다. from ['\"](next | react)는 from ['\"](next까지만 읽힌다. 괄호가 안 닫힌 정규식이 되고 만다.
파일은 Write로 전체를 다시 쓴다. 부분 수정을 허용했더니 레이어 정의만 바꾸고 금지 import 표는 그대로 두는 일이 나왔기 때문이다.
다시 돌릴 때는 규칙이 하나 더 붙는다. 본문은 덮어쓰고 ## 결정 로그는 한 줄도 안 지운다. 앞선 결정을 뒤집었으면 지우지 않는다. 새 행에 “ARCH-002를 폐기하고 …로 교체”처럼 적는다. 결정 로그가 사라지면 다음 재실행이 같은 질문을 다시 묻는다. 사용자는 이전과 다르게 답한다. 문서는 실행할 때마다 흔들려 버린다.
5. 규칙이 진짜 도는지는 어떻게 아나
문서를 쓴 직후에 세 가지를 실제로 돌리고 출력을 그대로 핸드오프에 옮긴다.
| 검사 | 통과값 |
|---|---|
| 필수 헤딩 7개 grep | MISSING 0건 |
^\| ARCH-[0-9]{3} \| 개수 |
3이상 |
3열 ERE를 grep -E에 실제로 먹여 보기 |
REGEX-BAD 0건 |
표 1. 문서를 쓴 직후에 돌리는 세 가지 검사
세 번째 검사에서는 표의 정규식을 /dev/null에 대고 한 번씩 실행해 본다. 적어 놓기만 해서는 실제로 도는지 알 수 없으니, 종료 코드로 문법 오류를 걸러낸다. awk가 오류 없이 돌았는데 출력이 한 줄도 없으면 표 행 형식이 어긋난 것이다. 원본은 “이 함정은 실행 테스트로 두 번 잡혔다” 고 적어 뒀다.
프리셋은 이 표로 규칙을 검사하고 ESLint로 집행하도록 역할을 나눠 놨다.
| ARCH 표 (grep) | ESLint | |
|---|---|---|
| 언제 | 감사·리뷰 시점 | 편집 시점, 커밋, CI |
| 정확도 | 문자열 매칭 | import 구문 파싱 |
| 역할 | 규칙이 무엇인지 선언 | 규칙을 집행 |
표 2. ARCH 표와 ESLint의 역할 구분
앞 글에서 후순위로 미뤄 뒀던 것을 여기서 해결한다. 확정 직후 eslint.config.mjs에 계층 규칙을 넣는다. 넣은 다음에 위반을 일부러 만들어 에러가 나는지 확인한다. 설정 오류로 규칙이 로드되지 않아도 lint는 조용히 통과한다. 원본은 이걸 “검사가 안 돌았는데 통과로 보이는 것”이라고 부른다. 가장 자주 나온 실패로 꼽았다.
6. 어떻게 부르나
“아키텍처 잡아줘”, “구조 설계해줘”, “레이어 나눠줘”, “폴더 구조 정해줘”, “DDD로 갈까” 중 아무거나면 뜬다. 구현 시작 전에 .curvez/architecture.md가 없어도 뜬다.
앞 편에서 만든 .curvez/profile.json이 있어야 진행할 수 있다. 스택을 추측해 만든 레이어 매핑과 금지 import는 전부 무효라서 그렇다. 프로파일을 만드는 건 bootstrap이다. 이 스킬은 그다음이다.
그렇게 해서 저장소에 남는 것이 아래 두 파일이다.
.curvez/architecture.md— 헤딩 7개짜리 결정 문서.curvez/handoff/curvez-architect.<YYYYMMDD-HHmmss>.json— 검증 명령과 실제 출력값
verification은 요약하지 말라고 못박아 뒀다. 소스 트리가 없어 검사 대상이 0개여도 실패가 아니다. "검사 대상 0개 (소스 미생성). 정규식 문법 검사만 통과"라고 그대로 적는다.
7. 도입 비용과 한계
여기까지만 보면 문서 하나 더 쓰는 일 같지만 실제로 쓰려면 감수할 것도 있다.
| 치르는 것 | 내용 |
|---|---|
| 고를 게 없다 | 프리셋이 ddd 하나다. 다른 구조는 인터뷰로 처음부터 만들어야 하고 그렇게 만든 구조는 그 프로젝트에만 남는다 |
| 정규식으로 못 쓰면 등급이 내려간다 | 표현할 수 없는 규칙은 ## 예외가 아니라 ## 권고로 간다. 검사할 수 없는 규칙은 위반해도 아무 일이 없고, 아무 일이 없는 규칙은 3주 뒤에 존재하지 않는다는 게 이유다 |
| grep은 놓치는 게 있다 | 문자열 매칭이라 주석 안의 import를 세고, 줄바꿈된 import를 놓치고, 동적 import()를 못 본다. 진짜 집행은 ESLint 설정을 따로 해야 붙는다 |
| 안 물어본 것은 기본값으로 굳는다 | 5문 상한이라 나머지는 프리셋 값이 그대로 확정된다. 되돌릴 위치가 적혀 있을 뿐, 저절로 되돌아오지는 않는다 |
| 위반 0건이 깨끗함은 아니다 | 소스가 아직 없으면 실측이 0건으로 나온다. 실제 위반 검출은 structure-audit이 코드가 생긴 뒤에 한다 |
표 3. 이 스킬을 쓸 때 감수하는 것
경계를 지키는 스킬로 보기 쉬운데, 이 스킬은 판정 기준을 만들 뿐이다. 그 기준으로 코드를 판정하는 건 다른 스킬이다. 한 스킬이 둘 다 하면 아직 없는 코드를 감사하거나 이미 확정된 문서를 다시 쓴다.
8. 다음 에이전트에게 구조를 전달하는 방법
정한 구조를 다음 에이전트에게 넘기려면 헤딩 문자열이 고정된 파일 하나와, 그 안에 grep -E가 읽을 수 있는 표 세 줄이 필요했다.

답글 남기기