프로덕션에서도 효과적인 컨텍스트 구성 방법 — ALPS와 ADR로 추상화 경계 지키기
2026년 07월 25일 작성TL;DR
- PRD는 제품 전체의 가설을, ADR은 기능 하나의 요구사항을 담는다.
- ADR의 목표는 같은 코드가 아니라 같은 요구사항을 지키는 코드다.
- 요구사항이 ADR 레이어로 다 내려가면 PRD는 지운다.
시작하며
최근 모델이 좋아지면서 에이전틱 개발에 필요했던 human in the loop의 많은 부분이 모델과 도구 안으로 흡수되고 있다.1 개인적으로 이 방향을 가장 빠르게 밀어붙이는 곳은 Anthropic이라고 생각한다.
이 글은 계속 써온 렌즈 위에 있다. 소프트웨어 개발은 비즈니스 요구사항을 코드로 바꾸는 컴파일 과정이고, 에이전틱 엔지니어링은 그 과정에서 사람을 덜어내는 방향이다.2
이 렌즈를 쓰면 문서의 역할이 자동으로 정해진다. 컴파일러에게 필요한 건 입력, 즉 요구사항이다. 출력인 코드를 문서에 옮겨 적는 일은 컴파일러를 두 번 돌리는 것과 같다.
그래서 나는 세 가지 원칙을 중요하게 본다. 저장소에 남는 모든 텍스트는 일급 컨텍스트이며 항상 최신이어야 한다. 비즈니스·기능·비기능 요구사항은 source of truth이고, 코드는 그 결과물이어야 한다. 요약과 태스크 같은 구현 중간 산출물은 저장하지 않고 세션 안에서 휘발시켜야 한다.
문제는 요구사항과 코드의 추상화 수준이 너무 다르다는 데 있다. 요구사항에서 코드로 의존성은 흐르되 둘이 서로 직접 참조하면 안 되고, 그 사이의 매핑은 코딩 에이전트가 매번 동적으로 수행해야 코드베이스가 깨끗하게 남는다.
그래서 요구사항 문서를 두 종류로 나눈다. PRD는 제품 전체의 요구사항을 담아 Explore 단계의 가설 검증에 쓰고, ADR은 기능 하나의 요구사항을 담아 구현된 코드를 판정하는 데 쓴다. 범위와 용도가 다르다.
이 원칙을 실제 프로젝트에 적용하려고 만든 도구가 ALPS Writer3와 adr-writer다. 오늘은 이 도구가 어떤 관점에서 나왔고, 그 관점이 포맷과 워크플로우에 어떻게 반영됐는지 이야기해보려고 한다. PRD와 ADR을 서로 다른 레이어로 갈라두는 이유부터 실제 사용법까지 차례로 살펴본다.4
1. 컨텍스트에도 레이어가 있다
먼저 그림 하나로 시작하자. 요구사항 문서라고 뭉쳐 불렀지만, 그 안에도 레이어가 나뉜다.
flowchart TD
PRD["<b>PRD</b> — 제품 전체의 가설<br/><i>수명: 제품 가설이 유효한 동안</i>"]
ADR["<b>ADR</b> — 기능 하나의 요구사항<br/><i>수명: 요구사항과 전제가 유효한 동안</i>"]
CODE["<b>코드</b> — 지금 어떻게 동작하나<br/><i>수명: 다음 변경까지</i>"]
PRD -->|"목표 · 범위로 제약"| ADR
ADR -->|"요구사항으로 제약하고<br/>준수 여부를 판정"| CODE
CODE -->|"실행 · 테스트"| RUN["운영에서 관찰"]
RUN -.->|"제품 가설이 틀렸다는 증거"| PRD
RUN -.->|"요구사항의 전제가 틀렸다는 증거"| ADR
레이어마다 변하는 속도가 다르다. 이게 레이어를 나누는 유일한 이유다.
제품 전체의 요구사항은 몇 달에 한 번 바뀌고, 기능 하나의 요구사항은 몇 주에 한 번 바뀌고, 코드는 매일 바뀐다.
달력의 숫자보다 중요한 건 무엇이 바뀔 때 함께 고쳐야 하는가다. 내가 구분하는 세 레이어의 수명은 다음과 같다.
| 레이어 | 범위 | 담는 질문 | 보통의 수명 | 넣지 않는 것 |
|---|---|---|---|---|
| PRD | 제품 전체 | 무엇을 왜 만들고, 성공은 무엇인가 | 제품 가설·범위가 유효한 동안(대개 몇 달) | 파일 경로, 함수, 구현 순서 |
| ADR | 기능 하나 | 이 기능이 지켜야 할 요구사항은 무엇인가 | 요구사항과 그 전제가 유효한 동안(대개 몇 주~몇 달) | 코드로 바로 알 수 있는 구조·상수 |
| 코드 | 구현 | 지금 시스템은 어떻게 동작하는가 | 다음 구현 변경까지(몇 시간~며칠) | PRD·ADR 번호의 역참조 |
여기서 긴 수명은 문서를 고정한다는 뜻이 아니다. 운영에서 제품 가설이나 결정의 전제가 틀렸다는 증거가 나오면 해당 레이어를 갱신한다. 반대로 함수명이나 파일 배치만 달라졌다면 PRD와 ADR은 손대지 않고 그대로 유효해야 한다.
그래서 논리적 의존 방향은 한 방향이어야 한다. 요구사항이 결정을 제약하고, 결정이 코드를 제약한다. 반대로 매일 바뀌는 코드가 상위 문서에 영향을 주면 몇 달에 한 번 바뀌어야 할 문서를 매일 고쳐야 한다.
클린 아키텍처가 도메인을 프레임워크로부터 지키는 이유와 같다. 도메인 로직이 ORM 클래스를 직접 참조하면 DB를 바꿀 때 비즈니스 규칙이 흔들린다. 그래서 화살표를 안쪽으로만 향하게 만든다.
문서도 똑같다. PRD가 코드를 참조하면, 리팩터링 한 번에 PRD가 틀린 문서가 된다. 코드 역시 PRD나 ADR의 파일명·번호를 직접 참조하지 않는다.
그림으로 겹쳐 놓으면 이렇게 된다. 안쪽이 느리게 바뀌고 바깥이 빨리 바뀐다는 점, 그리고 화살표가 한 방향뿐이라는 점이 클린 아키텍처와 같다.
여기서 클린 아키텍처와 다른 게 하나 있다. 클린 아키텍처에서는 바깥 레이어가 안쪽의 인터페이스를 코드로 참조한다. 컴파일러가 그 화살표를 검사해준다.
문서에는 그런 컴파일러가 없다. 그래서 화살표를 아예 긋지 않는 쪽을 택했다. 코드는 ADR을 모르고, ADR은 PRD의 파일명을 모른다.
대신 그 자리를 코딩 에이전트가 맡는다. 요구사항과 코드를 함께 읽고 둘 사이의 매핑을 그때그때 복원한다. 저장해둔 매핑이 없으니 stale해질 것도 없다.
그리고 사람은 틀린 문서를 고치지 않는다. 그냥 안 본다.
내가 실제로 침범한 사례
EncBird에 Office Raid라는 게임 모드를 붙일 때 쓴 PRD가 그랬다. 621줄이었고, 세계관과 게임 룰까지는 멀쩡했는데 중간부터 이런 게 들어갔다.
## Pinia 스토어 (`stores/officeRaid.ts`)
const coffee = ref(3) // ☕ 남은 커피
const flow = ref(0) // 연속 정답
const questionPool = ref<RaidQuestion[]>([])
## 구현 순서 (권장)
| 1 | 타입 정의 (`office-raid.ts`) | 없음 |
| 2 | Pinia 스토어 (`officeRaid.ts`) + composable | 타입 |
| 3 | 타이틀 화면 (flashcards/stats fetch) | 스토어 |
...
| 12 | 모바일 최적화 (터치, 반응형) | 전체 완성 후 |
PRD 안에 변수 선언과 파일 경로와 12단계 태스크 목록이 들어갔다. 쓸 때는 친절한 문서라고 생각했다.
결과는 이랬다.
flowchart LR
subgraph 문서
P["PRD 621줄<br/>세계관 + 룰 + 스토어 코드<br/>+ API 표 + 12단계 태스크"]
end
subgraph 현실
C1["코드: coffee ref(3)<br/>→ MAX_COFFEE 상수로 추출"]
C2["코드: 난이도<br/>→ CEFR 레벨 기반 티어 도입"]
C3["코드: 파일 구조<br/>→ composable 분리"]
end
P -.->|"첫날은 일치"| C1
C1 -->|"리팩터링"| X["PRD의 그 문단이 사실과 어긋남"]
C2 --> X
C3 --> X
X --> D["아무도 PRD를 안 봄<br/>커밋 3개 후 방치"]
실제로 지금 코드의 난이도 체계는 사용자의 CEFR 레벨로 티어를 나눈다. PRD에는 CEFR이라는 단어가 한 번도 안 나온다. 구현하면서 생긴 결정인데, PRD를 고칠 방법이 없었다. 그 문서는 이미 코드를 설명하는 문서가 되어 있었고, 코드가 앞서 나가버렸으니까.
같은 기능의 ADR은 반대였다. 142줄이고, 스토어 변수도 파일 경로도 없다. 대신 대안 6개와 그중 3개가 “초기 채택 후 폐기”된 이유가 적혀 있다.
| SRS due date 기반 표현 조회 | 기각 (초기 채택 후 폐기) | "많이 공부할수록 플레이 불가" 역설 발생 |
| Expression Gate (15개 미만 차단) | 기각 (초기 채택 후 폐기) | 신규 사용자 차단, 기본 표현으로 대체 |
이 문장들은 코드를 아무리 리팩터링해도 사실과 어긋나지 않는다. 그래서 두 달 뒤 후속 결정(무료 플레이, 게임 종료 후 표현 추천)을 추가할 때도 그 ADR 옆에 새 ADR을 놓는 것으로 끝났다. PRD는 손대지 않았다.
PRD는 방치되고 ADR은 계속 쓰이게 된 차이는 성실함이 아니라 그 문서가 어느 레이어에 머물렀느냐였다.
RFTCR 글에서 C4 모델을 예로 들며 “단계별 입출력의 추상화 수준을 명확히 정의해야 한다”고 썼는데5, 그때는 그게 프로세스 표준화 얘기라고 생각했다. 지금은 문서의 수명을 결정하는 문제라고 본다.
ALPS Writer는 여기서 출발했다. PRD 작성을 편하게 해주는 생성기가 필요해서가 아니라, 요구사항을 코드의 변화로부터 지키면서 에이전트가 계속 사용할 수 있는 컨텍스트로 만들기 위해서였다.
2. PRD는 가설을 검증하는 문서다
먼저 이 문서가 무엇을 담는지부터 정하자. PRD는 프로젝트 전체에 대한 비즈니스 요구사항과 기능·비기능 요구사항의 상세다. 개별 기능이 아니라 제품 하나를 통째로 놓고 본다.
그래서 PRD가 가장 유용한 지점은 Explore 단계다. 켄트 벡의 3X에서 수익 곡선이 아직 평평한 구간, 가치 있는 아이디어를 싸고 빠른 실험으로 찾는 시기다.6
이 단계에서 제품은 전부 가설이다. 누가 쓸지, 무엇이 성공인지, 무엇을 만들지 않을지가 모두 검증 대상이다. 이 가설들을 한 문서에 모아 서로 앞뒤가 맞는지 확인하는 것이 PRD의 일이다.
중요한 건 이게 구현 지시서가 아니라는 점이다. 가설을 세우고 검증하는 도구이므로, 파일 경로나 변수 선언이 들어갈 자리가 없다. 그런 게 들어가면 가설 문서가 아니라 코드 설명서가 된다.
Office Raid PRD가 정확히 그렇게 변했다.
재현의 대상은 코드가 아니다
이 지점에서 ALPS 프로세스 전체의 목표를 분명히 해둘 필요가 있다. 같은 문서로 프로세스를 여러 번 돌렸을 때 알고리즘까지 똑같은 코드가 나오는 것이 목표가 아니다.
오히려 알고리즘이나 자료구조 같은 구현 디테일은 매번 달라도 된다. 목표는 나온 코드가 내가 준 비즈니스 요구사항과 기능·비기능 요구사항을 준수하는 것이다.
이 차이가 문서에 무엇을 적을지를 결정한다.
| 재현하려는 것 | 문서에 적어야 하는 것 | 결과 |
|---|---|---|
| 같은 코드 | 알고리즘, 상수, 파일 배치까지 | 코드를 두 벌 쓴다. 한 벌은 반드시 낡는다 |
| 같은 요구사항 준수 | 지켜야 할 것과 그 판정 기준 | 구현은 자유롭고 문서는 오래 유효하다 |
정렬을 예로 들면, “응답은 최신순이어야 한다”는 요구사항이고 “quicksort를 쓴다”는 구현이다. 후자를 적어두면 나중에 정렬 방식을 바꿀 때 문서가 틀리지만, 전자는 그대로 유효하다.
모델이 좋아질수록 이 방향이 유리해진다. 구현을 지정해줄 필요가 줄어들고, 무엇을 지켜야 하는지만 정확히 주는 쪽이 더 나은 결과를 낸다.
그럼 PRD를 어떻게 자기 레이어에 머물게 하면서 에이전트가 읽기 좋은 source of truth로 만들 수 있을까. ALPS Writer는 여기에 두 가지로 답한다. 포맷을 고정하는 것, 그리고 작성 책임을 뒤집는 것이다.
PRD가 어긋나기 시작하는 지점은 보통 두 군데다.
첫째, 포맷이 매번 새로 발명된다. 팀마다, 문서마다 PRD 모양이 다르다. 섹션 이름도 다르고 상세 수준도 다르다.
에이전트는 이 문서가 무엇을 주장하는 문서인지부터 추측해야 한다. 사람도 내용을 쓰기 전에 형식을 다시 결정하느라 시간을 쓴다.
둘째, 품질이 작성자 역량에 묶인다. 경험 많은 PO가 쓴 PRD는 에이전트의 자유도를 적당히 조여주고, 그렇지 않은 PRD는 과잉 해석할 여지를 남긴다.
같은 제품인데 누가 문서를 썼느냐에 따라 생성된 코드가 달라진다.
ALPS(Agentic Lean Product Spec)는 이 둘을 각각 다르게 공격한다. 포맷은 고정해서 매번 결정하지 않게 하고, 작성 루프는 뒤집어서 완결성을 사람이 아니라 에이전트가 책임지게 한다. 앞에서 말한 관점이 각각 템플릿과 대화 프로토콜이라는 도구의 기능으로 내려온 셈이다.
고정된 포맷 — 9개 섹션
ALPS는 MVP 하나를 기술하는 데 필요한 것을 9개 섹션으로 고정한다.
| # | 섹션 | 담는 것 |
|---|---|---|
| 1 | Overview | 제품 맥락, 타깃 사용자, 핵심 문제 |
| 2 | MVP Goals and Key Metrics | 성공의 측정 가능한 정의 |
| 3 | Demo Scenario | 나머지 문서를 붙잡아주는 구체적 시나리오 |
| 4 | High-Level Architecture | 시스템 형태 — 구성요소, 경계, 데이터 흐름 |
| 5 | Design Specification | 화면·플로우·에러 상태 |
| 6 | Requirements Summary | 기능·비기능 요구사항 종합 |
| 7 | Feature-Level Specification | 기능별 vertical slice (UI → API → Data) |
| 8 | MVP Metrics | Section 2 목표에 대응하는 계측 |
| 9 | Out of Scope | 의도적으로 만들지 않을 것 |
9개 섹션은 목차로만 나열된 게 아니라 서로를 검증한다. 목표는 시나리오와 요구사항으로 내려가고, 구현 가능한 기능은 다시 같은 목표를 측정하는 지표로 돌아와야 한다.
flowchart LR
O["1. Overview<br/>사용자 · 문제"] --> G["2. Goals<br/>목표 · 성공 기준"]
G --> S["3. Demo Scenario<br/>성공 장면"]
S --> R["6. Requirements<br/>기능 · 비기능 요구사항"]
A["4. Architecture<br/>경계 · 데이터 흐름"] --> F["7. Feature Spec<br/>vertical slice"]
D["5. Design<br/>화면 · 에러 상태"] --> F
R --> F
F --> M["8. MVP Metrics<br/>실제 계측"]
M -->|"목표를 측정하는가?"| G
X["9. Out of Scope<br/>의도적 경계"] -.->|"범위 제한"| R
X -.->|"범위 제한"| F
섹션을 나눈 방식에 세 가지 원칙이 깔려 있다.
하나, 추상화 수준을 섞지 않는다. 비즈니스 요구사항과 설계와 기술 요구사항이 각각 다른 섹션에 산다. “사용자가 즐거움을 느끼게”와 “POST /api/orders는 멱등키를 받는다”가 한 문단에 섞이지 않는다.
둘, 기능을 vertical slice로 자른다. Section 7의 각 기능은 UI에서 API를 거쳐 데이터까지 관통한다. 그래야 기능 하나가 독립적으로 구현·테스트·배포된다.
셋, 하지 않을 것을 일급으로 둔다. Section 9(Out of Scope)는 각주가 아니다. 에이전트가 하지 말아야 할 일을 명시적으로 적어두지 않으면, 팀이 일부러 미뤄둔 영역까지 코드가 번져나간다.
질문하는 쪽을 뒤집는다
포맷보다 더 중요한 게 작성 방식이라고 생각한다.
보통 우리가 AI로 문서를 쓸 때는 사람이 묻고 AI가 답한다. 이 구조에서는 사람이 무엇을 물어야 하는지 아는 만큼만 문서가 채워진다. 문서 품질의 상한이 작성자의 질문 실력이 된다.
ALPS Writer는 이걸 반대로 돌린다.
flowchart LR
subgraph A["보통의 문서 작성"]
direction TB
H1["사람: 무엇을 물을지 결정"] --> AI1["AI: 그럴듯한 초안 생성"]
AI1 --> G1["빈틈 = 사람이 묻지 않은 것"]
end
subgraph B["ALPS 작성 루프"]
direction TB
T["템플릿 + 섹션별 질문 가이드"] --> AI2["에이전트: 한 번에 1~2개 질문"]
AI2 --> H2["사람: 답변"]
H2 --> C["완성본 제시 → 확인 후에만 저장"]
C -->|다음 섹션| AI2
C --> G2["빈틈 = 템플릿이 놓친 것"]
end
- 에이전트가 묻고 사람이 답한다. 9개 섹션을 순서대로 돌면서 한 턴에 1~2개씩 좁은 질문을 던진다.
- 에이전트가 섹션을 임의로 생성하지 않는다. 완성본을 보여주고 사람이 확인해야 저장된다.
- 섹션별 대화 가이드가 도구 안에 들어 있다. 프로젝트가 바뀌어도 질문 흐름이 같다.
이러면 문서 품질의 상한이 작성자의 PRD 실력에서 템플릿과 질문 프로토콜로 옮겨간다. 도구를 개선하면 다음 프로젝트의 문서가 같이 좋아진다는 뜻이라, 나는 이게 이 도구의 핵심 설계라고 생각한다.
부수 효과로 문서 작성이 인터뷰가 된다. 백지 앞에서 “아키텍처 뭐 쓰지”를 고민하는 것보다, “지도 타일은 어디서 받아오실 건가요”에 답하는 게 훨씬 쉽다.
실제로 돌려보면
설치는 플러그인 두 개를 넣는 것으로 끝난다. npm이나 빌드 단계가 없다.
/plugin marketplace add haandol/alps-writer-plugins
/plugin install alps-writer@alps-writer # PRD 작성 (MCP 서버 + /alps-init)
/plugin install adr-writer@alps-writer # ADR 사이클 (/adr-new, /adr-impl, hook)
Codex를 쓴다면 codex plugin add alps-writer@alps-writer 형태로 같은 두 플러그인을 설치하고, 스킬을 $alps-init으로 부른다.
/alps-init을 치면 대화가 시작된다. 성경길7이라는 사이드 프로젝트를 이걸로 시작했는데, 실제 흐름은 이랬다.
> /alps-init
## Section 1. Overview
이 섹션은 제품이 왜 존재하는지와 누구를 위한 것인지를 고정합니다.
먼저 이 프로젝트의 주된 목적이 무엇인가요?
> 성경을 장 단위로 읽는 걸 핵심 경험으로 하는 웹 서비스. 한 장 읽고
> 다음 장으로 넘기면 통독표에 자동으로 기록되게 하고 싶다.
좋습니다. 그럼 이 서비스를 가장 필요로 하는 사용자는 누구인가요?
> 통독에 도전하지만 꾸준히 이어가기 어려운 일반 신자.
> 지명이나 시대 배경이 낯설어서 맥락을 놓치는 사람들.
한 번에 하나씩 묻고, 하위 섹션이 다 채워지면 완성본을 보여준 뒤 확인을 받는다. 확인 전에는 파일에 아무것도 쓰지 않는다.
여기서 눈여겨볼 건 문서 순서와 질문 순서가 다르다는 점이다. 최종 문서는 1→9 번호 순이지만, 질문은 1 → 2 → 3 → 4 → 6 → 5 → 7 → 8 → 9 순으로 진행된다.
Section 5(Design)가 Section 6.1에서 정의한 Feature ID를 재사용하기 때문에, 요구사항을 먼저 확정하고 설계를 묻는다.
Section 6.1에서 기능 목록에 ID가 붙고, 6.3에서 그 기능들 사이의 의존성을 Mermaid로 그린다. 성경길에서 실제로 저장된 그래프는 이렇다.
graph TD
F1[F1: 장 단위 본문 읽기]
F2[F2: 장 넘기기 & 완독 처리]
F3[F3: 통독 진도표]
F4[F4: 이어읽기 진입]
F5[F5: 온디맨드 맥락 레이어]
F6[F6: 지도 시각화]
F8[F8: 진도 로컬 저장/복원]
F2 -->|depends on| F1
F5 -->|depends on| F1
F6 -->|depends on| F5
F3 -->|depends on| F8
F4 -->|depends on| F8
F2 -->|writes progress via| F8
이 그래프가 나중에 구현 순서를 강제한다. 그림 하나 그려놓고 끝나지 않는다는 점이 중요한데, 뒤에서 다시 나온다.
그리고 여기 절제 규칙이 하나 있다. Section 7은 “무엇”만 적고 “왜 그렇게 골랐는지”는 적지 않는다. 라이브러리 선택, 알고리즘, 컬럼 목록, 튜닝 상수는 여기 들어가지 않는다.
판별 기준은 단순하다. DB를 바꿔도 여전히 참인 문장이면 PRD에 남고, 선택을 정당화하는 문장이면 ADR로 간다.
Office Raid PRD가 어긴 게 정확히 이 규칙이다. coffee = ref(3)은 DB를 바꾸면 사실과 어긋날 수도 있는 문장이었다.
3. ADR은 판정 기준이다
여기서부터 뒷부분이다. ADR(Architecture Decision Record)은 이름이 거창한데 실물은 단순하다. 기능 하나의 요구사항을 기록한 마크다운 파일 하나다.
PRD가 제품 전체를 놓고 봤다면, ADR은 개별 기능 단위로 비즈니스 요구사항과 기능·비기능 요구사항을 담는다. 담는 내용의 종류는 같고 범위가 다르다.
앞서 말한 “같은 코드가 아니라 같은 요구사항 준수를 재현한다”는 목표가 가장 직접적으로 적용되는 문서가 이것이다. 구현 방법은 여러 개여도 되고, 구현의 세부는 개발하는 과정에 맡긴다. 캐시를 Redis로 하든 메모리로 하든 요구사항을 지키면 통과다.
그래서 ADR의 실제 용도는 구현된 결과가 비즈니스 요구사항과 기능·비기능 요구사항을 준수하는지 판단하는 기준이다. 설계도가 아니라 채점표에 가깝다.
이 한 문장이 ADR의 거의 모든 규칙을 설명한다.
그런데 이게 왜 추상화인가. 추상화는 세부를 숨기고 계약만 남기는 일이다. 인터페이스가 구현 클래스를 숨기고 시그니처만 남기는 것처럼.
ADR이 숨기는 것은 구현이고, 남기는 것은 지켜야 할 요구사항과 그것을 고른 이유다.
EncBird의 사전 기능을 예로 들면 이렇게 갈린다.
| ADR에 남는 요구사항 | 구현에 맡기는 것 |
|---|---|
| 유효하지 않은 입력의 판정은 결정론적이어야 한다 | 판정 함수의 이름과 위치 |
| 동일 입력 재시도에 LLM을 다시 호출하지 않는다 | 캐시 키 포맷, TTL 값, 자료구조 |
| 많이 공부한 사용자가 플레이 불가에 빠지지 않는다 | 표현을 골라오는 조회 쿼리 |
왼쪽은 캐시를 Redis에서 메모리로 바꿔도 그대로 지켜야 하는 것이다. 오른쪽은 리팩터링 한 번에 바뀐다.
그리고 왼쪽은 구현이 끝난 뒤 검사할 수 있는 문장이다. 세 번째 항목을 예로 들면, SRS due date로 표현을 조회하는 방식은 “많이 공부할수록 플레이 불가” 역설을 일으켜서 이 요구사항을 위반한다. 그래서 기각됐다. 코드가 어떻게 생겼든 이 판정은 그대로 작동한다.
이 분리가 만드는 효과가 하나 더 있다. 요구사항과 코드가 서로를 직접 참조하지 않아도 된다. 요구사항에서 코드로 향하는 의존 방향은 유지되지만, 둘 사이의 구체적인 매핑은 저장해두지 않는다. 에이전트가 ADR의 결정과 현재 코드를 함께 읽어 그때그때 복원한다.
막상 쓰려고 하면 “무엇까지 적어야 하나”에서 막힌다. 판단 기준은 두 개다.
flowchart TD
Q["어떤 사실을 기록하려 한다"]
Q --> T1{"에이전트가 코드를<br/>그대로 읽으면<br/>알 수 있나?"}
T1 -->|"예"| NO1["ADR에 쓰지 않는다<br/>(코드가 답이다)"]
T1 -->|"아니오"| T2{"코드에서 이 값이 바뀌면<br/>결정 자체가 바뀌나?"}
T2 -->|"아니오"| NO2["ADR에 쓰지 않는다<br/>(그냥 구현 디테일)"]
T2 -->|"예"| YES["ADR에 남긴다<br/>(회색지대)"]
두 질문을 차례로 통과한 것만 남긴다. 이걸 adr-writer는 두 단계 필터라고 부른다.
한 질문만 쓰면 안 되는 이유가 있다. 두 번째 질문만 적용하면 “코드 읽으면 아는 결정 사실”까지 ADR에 들어와서, 코드를 고칠 때마다 ADR을 고쳐야 한다. 첫 질문으로 먼저 걸러야 한다.
이 필터를 통과하는 영역을 회색지대라고 부른다. 비즈니스 요구사항과 코드 사이에 떠 있는, 어느 쪽을 읽어도 안 나오는 것들이다.
flowchart LR
A["<b>PRD — 제품 전체</b><br/>무엇을 왜 만드나<br/>누구를 위해<br/>성공은 무엇인가<br/><i>가설을 검증한다</i>"]
B["<b>ADR — 기능 하나</b><br/>지켜야 할 비즈니스 요구사항<br/>기술 · 비기술 요구사항<br/>상태 전이 · 도메인 불변식<br/>왜 이 대안이고 왜 다른 길이 아닌가<br/><i>구현을 판정한다</i>"]
C["<b>코드 — 구현</b><br/>지금 무엇을 하나<br/>함수 · 타입 · 상수<br/>파일 구조 · 스키마<br/><i>요구사항을 만족시킨다</i>"]
A --- B --- C
구체적으로 ADR에 넣지 않는 것들이 규칙으로 정해져 있다. 실전에서 제일 자주 어기는 항목들이라 그대로 옮겨둔다.
전부 “같은 코드를 재현하려는 시도”라는 공통점이 있다.
| 금지 | 대신 |
|---|---|
파일 경로 (app/components/Card.vue) |
폴더 단위까지만 |
| 코드 스니펫 (Go struct, TS interface) | 코드 자체가 정답 |
| 함수/클래스 책임 분담 | 코드와 docstring |
구현 상수 (MAX_RETRY = 3) |
개념만 (“재시도는 유한하다”) |
| 환경 변수 이름·설정 키 | 설정 문서 |
| 알고리즘 의사코드 | 함수 본문 |
그리고 코드는 ADR 번호를 인용하지 않는다. 이게 처음엔 이상하게 느껴졌다. 트레이서빌리티를 포기하는 것 같으니까.
이유는 방향 문제다. 코드 주석에 // ADR dictionary/0012 가 들어가면, ADR을 정비할 때(번호 재정렬, 통합, 분할) 코드를 고쳐야 한다. 점선이 실선이 된다.
Bad (코드 안): // ADR dictionary/0012: invalid 입력은 마커 캐시로 차단한다
Good (코드 안): // invalid 입력은 결정론적이므로 캐시해 동일 입력 재시도 시 LLM 호출을 회피한다
Good (커밋 메시지): feat(dictionary): cache invalid phrase result (ADR dictionary/0012)
추적은 git이 한다. 코드가 궁금하면 git blame → 커밋 메시지 → ADR로 올라간다. 화살표가 여전히 한 방향이다.
4. 경계를 지키는 장치들
원칙만으로는 경계가 안 지켜진다. 나도 Office Raid PRD를 쓸 때 “PRD는 무엇만 적는다”는 걸 몰랐던 게 아니다. 쓰다 보니 넘어간 거다.
그래서 adr-writer에는 사람의 의지에 의존하지 않는 장치가 몇 개 있다.
한 방향으로만 흐르는 import
PRD에서 ADR로 넘어갈 때 /feature-to-adr를 실행한다. Section 7의 기능들을 ADR 초안으로 옮기는 단계다.
이 명령이 하는 일은 얇다. 기능 이름에서 카테고리 키를 뽑고(장 단위 본문 읽기 → chapter-reading), 6.3 그래프를 위상 순서로 정렬하고, ADR 작성 자체는 /adr-new에 넘긴다.
여기 반드시 짚어야 할 규칙이 하나 있다. 이 import는 일회성이다.
flowchart LR
P["ALPS PRD<br/>Section 7"] -->|"기능당 단 1회 import"| A["ADR"]
A -->|"구현"| C["코드"]
A -->|"이후 결정 변경은<br/>ADR을 직접 수정"| A
X["재import ❌<br/>PRD를 고쳐서<br/>다시 가져오지 않는다"] -.- P
기능 하나를 ADR로 옮기고 나면, 그 기능의 요구사항을 소유하는 쪽은 ADR이다. PRD가 나중에 바뀌면 다시 import하지 않고 해당 ADR을 직접 고친다.
재import를 허용하면 안정적인 레이어(PRD)가 변덕스러운 레이어(코드)를 끌고 다니게 된다. Office Raid PRD가 방치된 것과 같은 경로다.
성경길의 지금 상태를 보면 이 규칙이 어떻게 작동했는지 보인다. 처음 import된 8개 카테고리가 지금은 15개 카테고리 19개 ADR로 늘어났다.
늘어난 7개는 PRD를 고쳐서 다시 가져온 게 아니다. 만들면서 새로 생긴 결정들이다 — 앱 전역 키보드 제어, 테마 선호, URL로 진도 넘기기, 옛말 풀이, 도량형 환산.
전부 /adr-new로 ADR 레벨에서 직접 추가됐다. ALPS 문서는 import 이후 한 줄도 수정하지 않았고, 결국 저장소에서 지웠다.
PRD는 출발점이고 종착점이 아니다. 이 도구를 쓰면서 제일 늦게 이해한 부분이다.
의존성이 구현 순서를 강제한다
6.3에서 그린 그래프는 .mapping.json의 dependsOn 필드로 옮겨간다.
"chapter-turn": {
"feature": "장 넘기기 & 완독 처리",
"adrs": [{ "path": "docs/adr/chapter-turn/0001-chapter-turn-completion.md",
"status": "Accepted (2026-07-13)", "summary": "..." }],
"dependsOn": ["chapter-reading", "progress-storage"]
}
/adr-impl chapter-turn을 실행하면 에이전트가 먼저 이걸 확인한다.
flowchart TD
S["/adr-impl chapter-turn"] --> D{"dependsOn의 선행 ADR이<br/>모두 Accepted인가?"}
D -->|"아니오"| B["구현 중단<br/>선행부터 구현할지 사용자에게 확인"]
B --> T["위상 정렬 후<br/>가장 깊은 선행부터 순서대로"]
D -->|"예"| P["계획 수립 → 구현 → 테스트"]
T --> P
P --> PASS{"테스트 통과?"}
PASS -->|"아니오"| P
PASS -->|"예"| ACC["Status: Proposed → Accepted<br/>자동 승격"]
chapter-reading과 progress-storage가 아직 Proposed(미구현)면 거기부터 구현한다. 사용자가 입력한 순서보다 의존 순서가 우선한다.
Section 6.3에 그린 화살표가 구현 순서를 강제하는 조건이 되는 것이다. 다이어그램이 장식이 아니라 실행되는 제약이 됐다는 점이 이 연결의 핵심이라고 생각한다.
Status도 의도 표명이 아니다. Accepted는 “구현하고 테스트가 통과했다”는 사실이고, 테스트가 실패하면 승격되지 않는다.
매 턴마다 다시 주입한다
경계는 시간이 지나면 잊힌다. 특히 세션이 길어지고 컨텍스트가 압축되면 에이전트가 “ADR 먼저”라는 규칙을 잊는다.
그래서 adr-writer에는 UserPromptSubmit 훅이 하나 있다. 사용자가 메시지를 보낼 때마다 ADR 인덱스 스냅샷과 “동작을 바꾸기 전에 관련 ADR을 먼저 읽거나 작성하라”는 지시를 다시 주입한다.
이 훅은 편집을 막지 않는다. 그냥 매 턴 다시 말해준다. 사람이 잊는 것도 문제지만 에이전트가 잊는 게 더 자주 일어나기 때문이다.
사람의 판단이 필요 없는 검사는 기계가 한다
ADR 형식이 맞는지, 대안이 2개 이상 있는지, 본문 Status와 인덱스의 status가 일치하는지, 코드에 ADR 역참조가 새로 생기지 않았는지 — 이런 건 판단이 필요 없다.
그래서 LLM이 아니라 스크립트가 검사한다.
node <plugin>/scripts/adr-structure-lint.mjs [category] # 구조 + 불변식
bash <plugin>/scripts/adr-invariants.sh # 역참조 검사만
특히 두 번째 스크립트가 하는 일이 이 글의 주제다. 코드가 ADR을 인용하는지, ADR이 PRD를 인용하는지 검사한다. 역방향 화살표가 생기면 실패한다.
한 방향 의존을 원칙이 아니라 깨질 수 있는 검사로 만들어두는 것. 이게 사람의 성실함에 의존하지 않는 유일한 방법이라고 생각한다.
5. 태스크는 휘발되고 있다
여기서 예전 글을 하나 정정해야 한다.
RFTCR에서 나는 다섯 단계를 제안했다. Requirement → Feature → Task → Code → Reflect.5 그중 Task 단계에 대해 “TaskMaster 같은 도구로 세부 작업을 뽑고 의존 순서를 관리하라”고 썼다.
지금은 그 단계가 문서로 남을 필요가 거의 없다고 생각한다.
flowchart TD
subgraph OLD["2025년 초 — 태스크가 산출물이던 때"]
direction LR
R1["Requirement"] --> F1["Feature"] --> T1["<b>Task 목록</b><br/>파일로 저장<br/>사람이 관리"] --> C1["Code"]
end
subgraph NEW["지금 — 태스크가 실행 중에만 존재"]
direction LR
R2["PRD"] --> A2["ADR<br/>(결정)"] --> AG["코딩 에이전트<br/><i>계획 → 구현 → 테스트</i><br/>태스크는 세션 안에서만 존재"] --> C2["Code"]
end
OLD -.->|"코딩 에이전트가 흡수"| NEW
이유는 단순하다. 태스크 분해에서 사람이 하던 일 대부분을 코딩 에이전트가 세션 안에서 처리하게 됐다.
/adr-impl 하나를 실행하면 에이전트가 ADR을 읽고, vertical slice를 뽑고, 관련 코드를 찾고, 변경 계획을 세워 보여주고, 승인받고, 구현하고, 테스트를 돌린다. 이 과정에서 태스크는 확실히 존재한다. 다만 파일로 남지 않는다.
그게 나쁜 게 아니라 오히려 맞는 방향이라고 생각한다. 태스크는 원래 가장 빨리 변하는 레이어였다. 파일로 남기는 순간 아무도 안 보는 문서가 하나 더 생긴다.
Office Raid PRD의 12단계 태스크 표가 정확히 그랬다. 그 표는 실제 구현 순서와 며칠 만에 어긋났고, 이후 아무도 안 봤다. PRD의 수명을 태스크의 수명으로 끌어내린 것이다.
지금 남겨야 하는 건 태스크나 코드 요약본이 아니라 태스크를 다시 만들어낼 수 있는 상위 정보다. 요약본은 생성되는 순간부터 stale해지고 원문과 drift할 가능성이 생긴다. 최신 모델은 요구사항과 코드를 직접 읽고 둘을 매핑할 능력이 충분하므로, 별도의 지속 컨텍스트로 복제해둘 실익이 작다.
| 남기는 것 | 변화 속도 | 왜 |
|---|---|---|
| PRD | 느림 | 무엇을 왜 만드나 |
| ADR | 중간 | 왜 그 대안인가, 도메인 규칙 |
| 의존성 그래프 | 중간 | 구현 순서를 매번 재계산 가능 |
| 빠름 | 에이전트가 매번 다시 뽑는다 |
그래서 이 글의 도구들에는 태스크 파일을 만드는 명령이 없다. dependsOn 그래프만 남기고, 순서는 실행할 때마다 다시 계산한다.
RFTCR의 다섯 단계 중 T가 문서에서 사라지고 실행으로 녹아든 것. 이건 프레임워크가 틀렸다기보다 그 자리를 도구가 채운 결과라고 본다.
6. 요구사항이 아래 레이어로 내려가면 위 레이어는 지운다
플러그인이 두 개로 쪼개져 있는 이유도 경계 때문이다. adr-writer는 ALPS를 전혀 모르고, 결합은 /feature-to-adr 한 방향뿐이다.
flowchart TD
Q{"제품 방향이<br/>문서로 존재하나?"}
Q -->|"아니오 — 새 제품"| ALPS["/alps-init<br/>ALPS 9개 섹션 작성"]
ALPS --> F2A["/feature-to-adr<br/>기능당 1회 import"]
F2A --> DROP["ALPS 문서 제거<br/><i>요구사항이 ADR 레이어로 내려감</i>"]
Q -->|"예 — 이미 굴러가는 서비스"| NEW["/adr-new <category><br/>결정 하나만 작성"]
DROP --> NEW
NEW --> IMPL["/adr-impl<br/>dependsOn 순서로 구현 + 테스트"]
IMPL --> REVIEW["/adr-impl-review<br/>필요성·충분성 반증 검토"]
REVIEW --> SYNC["/adr-sync<br/>코드와 ADR 정합"]
SYNC -->|다음 사이클| NEW
내 프로젝트들의 지금 상태는 이렇다.
| 프로젝트 | 첫 커밋 | ALPS 문서 | ADR 현황 |
|---|---|---|---|
| 성경길 | 2026-07 | 없음 | 15 카테고리 / 19 ADR (약 1,200줄) |
| PixelBank | 2025-09 | 없음 | 10 카테고리 / 약 60 ADR (약 5,200줄) |
| EncBird | 2023-02 | 없음 | 20 카테고리 / 약 130 ADR (약 17,000줄) |
| ALPS/adr-writer | 2026-03 | 없음 | 없음 (도구 저장소) |
| 합계 | 약 210 ADR / 약 23,000줄 |
제품 세 곳 다 PRD부터 썼는데, 지금은 어디에도 ALPS 문서가 없다. /feature-to-adr로 기능을 다 넘긴 뒤 지웠다.
도구 저장소인 ALPS Writer와 adr-writer에는 애초에 둘 다 없다. 여기서 결정은 docs/의 설계 문서로 관리한다. 남의 프로젝트에 ADR을 쓰게 하는 도구라고 해서 자기 저장소에도 같은 구조를 강요할 이유는 없다고 봤다.
PRD를 처음 지울 때는 아까웠다. 공들여 쓴 9개 섹션인데.
그런데 남겨둘 이유를 찾지 못했다. 두 문서의 역할이 다르기 때문이다.
PRD는 Explore 단계에서 가설을 세우고 검증하는 문서다. 기능이 ADR로 다 넘어간 시점은 그 가설들이 이미 검증을 통과했다는 뜻이고, 검증이 끝난 가설은 더 이상 가설이 아니다.
그리고 구현된 코드를 판정하는 일은 PRD가 하지 않는다. 그건 기능 단위 요구사항을 담은 ADR의 일이다. 결정이 바뀔 때마다 고친 쪽도 ADR이었다.
즉 PRD가 담고 있던 내용은 전부 아래 레이어로 내려간 상태다. 제품 단위 가설은 검증 결과로, 기능 단위 요구사항은 ADR로. 그런데도 위 레이어에 파일을 남겨두면 같은 내용을 두 추상화 수준에서 이중으로 관리하게 되고, 실제로 갱신되는 쪽은 아래뿐이다. 위는 import 시점의 스냅샷으로 굳어서 읽으면 오히려 현재와 어긋난 정보를 준다.
이게 앞서 말한 “저장소에 남는 텍스트는 모두 최신이어야 한다”는 원칙의 결론이기도 하다. 최신으로 유지할 수 없는 문서라면 최신으로 유지하는 대신 지우는 선택지가 있다.
Office Raid PRD의 실패도 다시 보면 같은 얘기다. 그건 새 제품의 PRD가 아니라 3년 된 시스템의 한 기능에 대한 PRD였다. 애초에 ADR 레이어에서 다뤄야 할 범위를 PRD 레이어에 올려놓고, 그마저 정리하지 않았다.
그래서 지금은 이렇게 쓴다. 제품 방향이 머릿속에만 있으면 PRD부터, 이미 글로 있으면 결정부터. 어느 쪽이든 ADR로 넘어간 뒤에는 PRD를 남기지 않는다.
ADR 개수 차이는 그냥 프로젝트 나이 차이다. 개수 자체가 목표는 아니고, 요구사항이 코드보다 먼저 글로 남았는지가 중요하다고 본다.
길이도 나이를 따라간다. ADR 평균이 성경길 62줄, PixelBank 88줄, EncBird 129줄이다. 결정이 쌓이면서 지켜야 할 요구사항과 기각된 대안이 함께 늘기 때문인데, 이게 좋은 신호인지는 아직 판단이 안 선다.
마치며
지금까지 쓴 내용은 정답이 아니다. 내가 생각한 방식대로 동작하는 시스템을 만들다 보니 이런 형태가 나온 것에 가깝다.
요구사항이 ADR 레이어로 내려가면 PRD를 지운다는 결론도, 태스크를 파일로 남기지 않는다는 선택도, 내 프로젝트들과 내 작업 방식에서 나왔다. 다른 제약을 가진 사람은 다른 답에 도달할 것이다.
애초에 에이전틱 엔지니어링에서는 자유도가 거의 무한하다고 생각한다. 비즈니스 요구사항을 만족하는 코드만 나오면 어떤 방식이든 되기 때문이다. 이 글에서 ADR의 목표를 “같은 코드가 아니라 같은 요구사항을 지키는 코드”라고 했는데, 그 기준은 만드는 방식 자체에도 똑같이 적용된다.
그래서 이 글의 내용도 하나의 실험 결과로 읽어주시면 좋겠다. 여러 방식을 직접 돌려보고 각자의 인사이트를 실험해보는 시간이 되었으면 한다.
무엇을 시도하든, 요구사항 하나가 코드보다 먼저 글로 남는 것에서 시작하면 된다.
-
에이전틱 엔지니어링과 과도기적 기술들 — human in the loop이 모델과 도구로 흡수되는 방향에 대한 이전 글. ↩
-
현상을 해석하는 렌즈, 그리고 에이전틱 엔지니어링 — 소프트웨어 개발을 비즈니스 요구사항의 컴파일 과정으로 보는 렌즈. 이 글의 문서 구분은 그 렌즈에서 따라 나온 결과다. 컴파일러 비유의 출발점은 에이전틱 엔지니어링과 과도기적 기술들. ↩
-
ALPS Writer Plugins — 마켓플레이스, 사용 가이드, 의존성 모델 문서. ADR 형식 자체의 원전은 Documenting Architecture Decisions. ↩
-
EncBird에 하네스를 한 겹씩 씌워온 과정 — PRD·ADR을 다른 모든 자동화의 기준점으로 놓는 이유와 PRD → ADR → 코드 단방향 의존성을 다룬다. ↩
-
RFTCR - 에이전트 주도 소프트웨어 개발을 위한 새로운 SDLC 프레임워크 — 단계별 입출력의 추상화 수준을 C4 모델처럼 고정해야 한다는 논지. 이 글의 5절은 그중 Task 단계에 대한 정정이다. ↩ ↩2
-
켄트 벡의 The Product Development Triathlon (2016) — Explore·Expand·Extract 세 단계로 나누는 3X 모델. 이 모델을 조직 관점으로 확장한 내용은 조직의 AI 도입을 보는 렌즈 참고. ↩
-
성경길 Bible Atlas — ALPS 문서를 먼저 쓰고 시작한 사이드 프로젝트. 지금은 ADR만 남아 있다. ↩