
품질을 위한 SDD - AI로 기업 수준의 퀄리티를 만들다 -
요약
AI를 활용한 개발에서 '동작하는 코드'를 넘어 '기업 수준의 품질'을 확보하기 위한 SDD(Spec-Driven Development) 방법론을 소개합니다. 속도와 자동화 대신 품질의 균질화와 내구성을 목표로 Claude Code를 활용해 사양 중심의 개발을 진행하는 과정을 다룹니다.
핵심 포인트
- 기존 SDD가 속도와 자동화에 집중한다면, 본 방식은 품질의 균질화에 집중함
- AI 생성 코드의 품질 편차를 줄이기 위해 사양(Spec)을 엄격하게 정의함
- Claude Code를 활용하여 대화하며 설계, 구현, 검증 단계를 단계별로 구축
- 단순 구현을 넘어 인증, 데이터 보안, 재현 가능한 배포 등 기업 수준의 요건 충족

「동작한다」와 「기업 수준의 퀄리티」는 별개
AI로 개발 속도가 빨라졌습니다. 프롬프트를 던지면 그럴싸한 앱이 몇 시간 만에 "동작"합니다.
하지만, 「동작한다」와 「기업 수준의 퀄리티로 계속 동작한다」 사이에는 아직 깊은 골이 있습니다.
인증은 제대로 견고한가. 데이터는 암호화되어 있으며, 파손되어도 복구 가능한가. 배포는 재현 가능한가. 누가 유지보수해도 품질이 떨어지지 않는가 ― 이 부분은 「동작한다」만으로는 전혀 담보되지 않습니다.
이 기사는 그 골을 **SDD (Spec-Driven Development / 사양 주도 개발)**로 메운 개인 개발 기록입니다. 개인 개발에서 어디까지 품질을 만들어낼 수 있는지를 하나의 소재로 전부 보여드립니다. 꽤 깁니다.
다만, 지금 유행하는 SDD와는 목적이 정반대입니다. 모두가 「빠르고 자동으로」 만들기 위해 SDD를 사용하는 가운데, 저는 「품질을 떨어뜨리지 않기 위해서」만으로 SDD를 하고 있습니다.
사용한 AI는 **Claude (Opus 4.8)**이며, Claude Code로 대화하면서 만들었습니다. 자동도 아니고 고속도 아닙니다. 하지만 완성된 결과물은 기업 수준의 퀄리티입니다.
기술자분들은 「개인이 이 정도까지 만들어내는가」라는 밀도를, 그렇지 않은 분들은 「AI로 이렇게 높은 품질의 시스템을 만들 수 있는가」라는 사실을 가져가실 수 있다면 기쁘겠습니다.
지금 유행하는 SDD와 나의 SDD는 목적이 반대
먼저, 현재 SDD의 상식을 확인하겠습니다. AI로 조사한 결과, 2026년 시점에서 주류인 SDD 툴(GitHub Spec Kit ★9만 초과, AWS Kiro, Tessl, BMAD…)은 거의 전부 이렇게 말하고 있습니다.
사양을 작성하면, 코드는 AI가
빠르고 반복적으로 재생성할 수 있다. 코드는 일회용 출력물이다.
AWS Kiro는 「40시간의 기능을 8시간 만에」라고 주장하고, GitHub Spec Kit은 「제로에서 다시 만드는 사이클이 차원이 다르게 줄어든다」고 말합니다. SDD = 속도와 자동화를 위한 메커니즘이라는 것이 세상의 이해입니다.
저의 SDD는 이 부분이 정반대입니다.
| 관점 | 정통 SDD (Spec Kit / Kiro / Tessl) | 본 프로젝트 |
|---|---|---|
| SDD의 목적 | 속도·자동화 (사양→코드 재생성을 고속으로 회전) | 품질의 균질화 (사람이든 AI든 품질의 천장을 같은 높이로) |
| 코드의 위치 설정 | 일회용·재생성 가능한 출력물 | 대화로 쌓아 올리는 내구 자산 |
| 규칙 파일 | constitution (불변의 원칙 집합·경량) | 검증 게이트·추적 규약·출하 검사까지 포함하는 운용의 정전 |
| 품질 게이트 | 체크리스트 중심 (※ 준수 보장은 없음) | 복수의 검증 게이트 + 요구사항의 grep 역조회 + 출하 전 검사 |
| 사람의 역할 | 사양을 리뷰함 | 판단을 쥐고 있음 (갈리는 설계는 AI에게 결정하게 하지 않음). 대화로 품질을 높임 |
| 검증의 주안점 | 사양 ↔ 구현의 정합성 | 「실제로 계속 동작함」의 증명 (CI Green을 통과하는 런타임 버그까지 찾아냄) |
한마디로 말하면, 모두는 SDD로 「빠르게 만든다」, 나는 SDD로 「품질을 만든다」.
대화형이기 때문에 솔직히 말해 빠르지 않습니다. 자동도 아닙니다. AI와 끈질기게 논의하며 사양 → 설계 → 구현 → 검사를 한 단계씩 다져 나갑니다. 그 대신, 누가 (사람이든 AI든) 만들어도 동일한 품질에 도달한다 ― 그것이 이 SDD의 목표입니다.
왜 이것이 효과적인가. AI에게 통째로 맡긴 생성은 매번 흔들립니다. 같은 지시라도 쓸 때마다 명명(Naming)·분할·에러 처리·HTTP 상태 코드가 미묘하게 변합니다. 그 흔들림, 즉 품질의 편차의 정체는 「사양으로 결정되지 않은 부분」을 AI가 그 자리의 추측으로 채우고 있기 때문입니다. 그래서 저는 사양으로 결정짓는 것에 전력을 다하고, AI에게는 「결정된 것을 정확하게 형상화하는」 역할과 「더 나은 판단을 제안하는」 역할만을 부여합니다. 판단의 확정은 사람이 합니다. 이것이 「품질을 위한 SDD」의 골격입니다.

무엇을 만들었는가 ― 겉모습은 메모장, 내용은 업무 시스템
만든 것은 자신을 위한 앱 플랫폼입니다.
- 지금은 「고급 메모장」. 인증·마이페이지·대시보드라는 토대 위에 메모 기능이 하나 올라와 있을 뿐입니다.
- 앞으로, 자신의 업무를 효율화하는 모듈을 자유롭게 추가해 나가는 것이 목적입니다. AI 모듈, 채팅… 등 토대를 파괴하지 않고 하나씩 추가·삭제할 수 있습니다.
- 동시에, 자신의 학습의 집대성이기도 합니다. 지금까지의 설계·구현·운용 경험을 하나의 토대로 통합했습니다.
이 기사의 주인공은 바로 이 격차입니다. 단 하나의 메모 기능만을 위해 인증(Authentication)·세션(Session)·비동기 처리(Asynchronous Processing)·모니터링(Monitoring)·출하 검사(Shipping Inspection)까지, 업무 시스템과 동일한 수준의 토대를 구축했다는 점입니다. 이후의 내용은 거의 전부 그 '하부 계층'에 관한 이야기입니다.
사양(docs/spec/)은 단계별로 번호를 매겨 나열하며, 이것이 유일한 정전(Canon)이 됩니다.
docs/spec/
00_전제/ 환경 전제 (AWS 계정·기존 자산)
01_요건/ 요구 정의 / 기능 요건(FR) / 비기능 요건(NFR)
...
기능은 인증·사용자 관리를 최우선으로 견고하게 만들고, 메모는 '토대가 제대로 작동하는가'를 확인하기 위한 최소한의 샘플이라는 위치를 차지합니다. 기능 요건(FR)은 번호로 관리합니다.
| FR 그룹 | 내용 |
|---|---|
| FR-AUTH-1〜8 | 초대 기반 활성화·이메일 확인·로그인·로그아웃·액세스 제어·PW 재설정·PW 변경·세션 관리 |
| ... |
사용자에게는 상태 모델이 있으며, 초대 중 → 유효 ⇄ 무효 → 삭제로 전이됩니다. 등록은 초대제만 허용하며 공개 셀프 등록은 제공하지 않습니다(외부인의 자기 등록을 방지하고, 후술할 열거 공격(Enumeration Attack)의 측면도 줄이기 위함). 탈퇴·삭제 시에는 PII(개인 식별 정보)를 연쇄 삭제합니다.
저자에 대해 짧게 소개하자면, 원래 디자인 출신이며 학습과 개인 개발을 거듭해 왔습니다. 그래서 이 기사에는 엔지니어링 이야기와 더불어 '디자인의 완성도'에 관한 이야기도 등장합니다(후술).
아키텍처 ― 기업급 퀄리티의 실체
'기업급 퀄리티'라고 말하는 만큼, 내용으로 증명하겠습니다. 형태는 **모듈러 모놀리스(Modular Monolith) + 가벼운 클린 아키텍처(Clean Architecture)**입니다. AWS 위에서 동작하는 구성은 개인용 메모장이라고는 믿기지 않을 정도의 밀도를 가집니다.
Bounded Context로 모듈 분리
기능은 **Bounded Context(모듈)**로 분할되어 있습니다. 핵심 영역(토대·버리지 않음)과 버릴 수 있는 기능 모듈을 나누는 것이 포인트입니다.
| 모듈 | 역할 | schema | 폐기 가능 |
|---|---|---|---|
| auth | 활성화·이메일 확인·로그인/로그아웃·PW 재설정/변경·세션 관리 | core | × |
| ... |
- 관리자는 별도 테이블로 분리하지 않고
role=admin인 사용자로 취급한다 (인증은 auth로 일원화) - 인가(Authorization)는 2축으로 관리한다. role(member/admin) + 사용자 단위의 앱 권한(기본값은 전체 허용, 비허용분만 기록)을 application 계층에서 판정한다
- 앱의 존재는 **코드 정의 레지스트리(Registry)**로 관리하며, 배치와 권한은 DB로 관리한다. 새로운 앱은 실행 시점에 만드는 것이 아니라 배포(Deploy) 시점에 늘린다
front는 Cookie를 전달할 뿐 세션 대조는 하지 않습니다(Stateless). 실체는 back과 Valkey에 있습니다.
4계층으로 의존성을 내부로 고정
각 모듈은 presentation → application → domain ← infrastructure의 4계층을 가집니다. 의존성은 내부로 향하도록 고정하며, domain은 순수 Python 상태를 유지하여 FastAPI / SQLAlchemy를 import 하지 않습니다. 이는 DB나 AI와 같은 외부 요소를 교체 가능하게 만들기 위함입니다.
- 1 유스케이스(Use Case) = 1 트랜잭션(Transaction). commit은 application 계층에서 한 곳에서 수행한다
- 리포지토리(Repository) 인터페이스는 domain에, 구현은 infrastructure에 둔다 (의존성 역전, Dependency Inversion)
- 메모와 같이 단순한 모듈은 4계층을 엄격히 나누지 않고 ORM 통합으로 간결하게 처리한다. domain을 나누는 것은 복잡한 모듈일 때만 적용한다 (과잉 설계 방지)
폐기 가능한 모듈 설계
- 모듈은 다른 모듈을 직접 import 하지 않는다. 공개는 창구(
shared/contracts)를 통해서만 이루어지며, 타입(Protocol/ABC)으로 정의하고 구현을 주입한다 - DB도 모듈마다 schema를 나누어 자신의 schema만 다룬다. 모듈을 가로지르는 외래 키(Foreign Key)는 설정하지 않으며, 공통 요소는
user_id참조만 사용한다 - 불필요해진 모듈은 schema째로 통째로 버릴 수 있다. 핵심(Core) 이외의 것은 복사하여 늘릴 수 있다
- 모듈 간 작업(예: 탈퇴 시 모든 데이터 연쇄 삭제)은 창구를 경유하여 동일 트랜잭션 내에서 정합성을 맞춘다. 도메인 이벤트(Domain Event)와 같은 복잡한 장치는 사용하지 않는다.
왜 모듈러 모놀리스(Modular Monolith)인가: 처음부터 마이크로서비스(Microservices)로 분산시키면 운영 부담이 너무 크다. 그렇다고 MVC 방식으로 레이어를 나누면 기능이 횡단적으로 흩어져서 버리기 어렵다. "하나의 배포(Deployment) 단위 안에서 기능을 나누고, 각 기능에 얇게 클린 아키텍처 (Clean Architecture)를 적용한다"는 것이 배우기 쉬움과 버리기 쉬움의 양립 지점이었습니다 (YAGNI = 필요해지기 전까지 복잡성을 더하지 않는다). 고통이 발생하는 기능만 나중에 정말로 분리하면 된다.
공개 영역을 좁히고, 공격 표면(Attack Surface)을 최소화
- 공개하는 것은
front (Next.js의 BFF) 뿐이다. 브라우저는 반드시 front를 통해서만 백엔드에 도달할 수 있다 -
back / worker / DB / 세션 기반은 모두 private subnet에 격리. 인터넷에서 직접 호출할 수 없다 - 전단에
WAFv2, 통신은 HTTPS, IAM은 최소 권한 (task/execution 분리), 비밀 정보는 Secrets Manager + KMS 암호화
무거운 처리는 비동기 워커(Worker)로
- 무거운 처리 (향후 AI 등)는 **별도의 ECS 서비스 + SQS (+ 실패를 격리하는 DLQ)**로 비동기화
- 워커는 멱등성 (Idempotency) 있게 만든다. SQS 표준은 중복 배송이 발생할 수 있으므로,
jobs테이블로 상태를 관리하며, 완료된 작업이 다시 오면 아무것도 하지 않고 ack한다. 결과 쓰기는 job_id로 덮어쓰며, 재실행 시 이중으로 생성되지 않도록 한다 - 가시성 타임아웃(Visibility Timeout)은 처리의 최악 시간보다 길게 설정한다 (짧으면 처리 중에 재배송되어 이중 실행됨)
"기업 수준의 퀄리티"라고 자부할 수 있는 근거
이 부분이 "동작한다"와 "기업 수준의 퀄리티"를 가르는 지점입니다.
| 관점 | 수행 내용 |
|---|---|
| 가용성 | Multi-AZ, Aurora PostgreSQL Serverless v2, ECS 오토스케일링 (dev/prod에서 수준을 전환) |
| 데이터 보호 | KMS 암호화, PITR (특정 시점 복원), 탈퇴 시 PII 연쇄 삭제 |
| 보안 | WAFv2, 최소 권한 IAM, Secrets Manager, private 격리. OWASP ASVS L2를 목표 |
| 모니터링·운영 | 구조화된 로그 (JSON) → CloudWatch, 알람 → SNS 알림, 감사 로그 (누가 언제 무엇을 바꿨는지), 상관 ID (Correlation ID) 추적 |
| 배포 | GitHub Actions + OIDC (장기 키를 가지지 않는 키리스 인증)를 통한 CD |
| 재현성 | 인프라는 Terraform으로 모든 리소스를 직접 기술 (약 2.6만 행). 동일한 구성을 변수 교체만으로 다른 환경에 통째로 복제 가능 |
그리고 개발 시에는, 로컬에서 AWS에 전혀 접촉하지 않고 모든 기능이 동작합니다. docker compose로 postgres / Valkey / S3 호환(MinIO) / SQS 호환(ElasticMQ) / 메일(Mailpit)을 띄우고, local / test / dev / prod의 4개 환경을 동일한 이미지로 구동한다. 클라우드 고유의 의존성은 어댑터(Adapter)에 격리되어 있으므로, 실행 시점에 "local 전용 코드"는 한 줄도 없습니다. prod를 정석으로 두고 dev는 디튜닝(Detune), test/local은 간이 구성으로 사용하는 관계입니다.
Terraform을 전부 직접 작성한 것도 공을 들인 부분입니다. 정해진 모듈 (terraform-aws-modules 등)에 의존하면 빠르지만, 파악할 수 없는 부분이 남는다. 학습과 완전한 파악을 위해 VPC부터 WAF, KMS, OIDC, Aurora, ElastiCache까지 직접 작성했습니다. 변수는 "필수 + 보안 + 환경 차이"로만 압축하여, dev 기본값 + prod 덮어쓰기 방식으로 최소한의 표면(Surface)을 유지하고 있습니다.
설계 판단은 "왜 그것을 선택했는지, 왜 다른 것은 포기했는지"까지 사양(Specification)으로 남겨두었습니다.
| 검토한 안 | 불채택 이유 |
|---|---|
| 레이어로 나누기 (MVC 방식) | 기능이 횡단적으로 흩어져서 버리기 어려움 |
| ... | 채택: 모듈러 모놀리스 + 가벼운 클린 아키텍처 |
| 버리기 쉬움과 배우기 쉬움을 양립. 나중에 분리나 엄격화도 가능 |
보안을 "사양"으로 견고하게 만들기
이 부분은 특히 힘을 주었기에 독립된 장으로 구성합니다. NFR (비기능 요구사항)로서, 검증 가능한 조건으로 하나씩 작성되어 있습니다. 분위기가 아니라, 표준에 연결하여 근거를 갖추고 있는 것이 이 장의 핵심입니다.
비밀번호 (NIST SP 800-63B 준수)
- Argon2id로 해싱. 평문 또는 가역 암호(reversible encryption)로 저장하지 않음 - 파라미터는 OWASP 최소 기준 이상(예: m=19456KiB, t=2, p=1). 값은 설정(configuration)에서 집중 관리
- 최소 15자 (NIST SP 800-63B Rev.4. MFA가 없는 password-only는 15자 이상). 기호 필수와 같은 합성 규칙이나 정기적 변경 강제는 하지 않음 (최신 가이드라인은 오히려 비권장) -
- 유출된 비밀번호를 차단 목록(denylist)으로 거부함
세션 (자체 제작한 얇은 계층)
- 실체는 Valkey의 서버 세션. Cookie는 내용이 없는 **불투명한 ID (opaque ID)**만 포함 (64bit 이상의 엔트로피, HttpOnly / Secure / SameSite) -
session:{sid}
에는 user_id / role / created_at / last_seen을 가지며, user_sessions:{user_id}를 통해 해당 사용자의 모든 세션을 일괄 무효화할 수 있음 - TTL은 2축으로 운영: 유휴(idle) 12시간 · 절대 7일. 개인용 디바이스 및 저위험(내용은 메모 중심) 환경이므로, 일상적인 사용 시 재로그인을 줄이면서도 상한선은 설정함 - 로그인 성공 시 기존 sid를 폐기하고 새 sid를 발급 (세션 고정 공격(Session Fixation) 방지) - 로그아웃 · 비밀번호 변경 · 비밀번호 재설정 · 역할(role) 변경 · 무효화 시 대상 사용자의 기존 세션을 무효화함. 본인은 세션 목록에서 개별 또는 모든 기기에 대해 무효화할 수 있음 (FR-AUTH-8)
왜 기성 세션 기능을 그대로 사용하지 않는가: fastapi-users의 표준 RedisStrategy 단독으로는 '역할(role) 유지 · 전체 무효화 · 2축 TTL · ID 재발급'을 충족할 수 없기 때문입니다. 따라서 fastapi-users는 인증 코어와 해싱에만 한정하여 사용하고, 세션은 얇은 자체 제작 계층에서 제어하도록 역할을 분담했습니다.
중요 작업의 재인증 및 인가
- 본인의 계정 작업(비밀번호 변경 · 이메일 변경 · 탈퇴)은 매번 현재 비밀번호로 재인증. 세션을 길게 유지하는 대신, 필요한 부분만 강력하게 보안을 적용함 - 관리자 작업(사용자 관리 · 앱 권한)은 **역할(role) 인가 + 감사 로그(audit log)**로 보호함
로그인 시도 제한 및 열거 공격 저항성 (Enumeration Resistance)
- 계정 단위 5회 실패 시 일시 잠금 + 지수 백오프 (exponential backoff). 여기에 IP 단위의 스로틀링(throttle)을 병행하여, 정상 사용자를 의도적으로 잠그는 DoS 공격을 방지함. 카운터는 Redis에 저장하며, 키(key)에 이메일 생값을 사용하지 않음 - 열거 저항성: 로그인 · 비밀번호 재설정 · 이메일 변경 시, 응답 · 동작 · 응답 시간을 통해 이메일 등록 여부를 추측할 수 없도록 함. 초대 중 · 무효 · 삭제된 계정도 상태를 밝히지 않고 일관되게 실패 처리함 - 등록은 초대제로 운영되며 공개 폼이 없으므로, 자기 등록을 통한 열거 공격 노출 자체를 원천 차단함
이메일 관련 토큰
- 초대 · 비밀번호 재설정 · 이메일 확인 토큰은 단회 사용 · 해싱 저장 · URL에 식별자를 포함하지 않음 - 만료: 비밀번호 재설정 30분 / 이메일 확인 24시간 / 초대 7일. 재설정 요청 및 각 이메일 재전송은 속도 제한(rate limit)을 적용함
통신 · 경계 · 입력
- 백엔드(back)를 비공개로 설정하여 브라우저에서의 직접 호출을 차단 (프론트엔드(front)를 통해서만 접근 가능)
- 통신은 HTTPS, 저장(Aurora / S3)은 암호화, 비밀 정보는 Secrets Manager를 통해 관리하며 코드에 직접 작성하지 않음
- CSRF: 프론트엔드의 Server Action 내장 기능(Origin/Host 대조) + SameSite Cookie로 방지. 백엔드는 비공개이므로 독자적인 토큰 검증은 갖지 않음 (중복을 피하기 위한 합리적 판단) - XSS: 입출력 이스케이프(escaping) + 모든 페이지에 CSP 등의 보안 헤더 적용 - 보장 수준은 OWASP ASVS L2를 목표로 함 (L1은 필수 달성). 위협은 STRIDE 모델로 식별하며, 출시 전에 **OWASP ZAP (DAST)**를 사용하여 직접 호출 차단 · 헤더 · Cookie 속성 · 인가 경계를 검사함
MFA · 외부 ID 연동 · 새 디바이스 알림 · 로그인 유지(remember me) 기능은 개인 단계에서는 의도적으로 포함하지 않음 (업무 단계에서 재검토). "하지 않을 것을 결정하는 것" 또한 품질 판단의 일부입니다.
품질을 “시스템”으로 만들기 ― SDD의 내용
여기서부터가 본론입니다. 개인의 역량에 의존하는 노력이 아니라, 시스템(mechanism)으로 품질을 고정하고 있습니다. 그 사고방식을 소개합니다 (절차의 세부 사항은 생략합니다).
1개 기능을 만드는 흐름
즉흥적으로 작성하는 것을 방지하기 위해, 1개 기능의 흐름을 단계별로 고정하고 있습니다.
- 파악: 사양(Specification)과 전제 조건 읽기 -
설계: 사양 작성 (요구사항 → 전체 설계 → 기본 설계 → 상세 설계) -
검증: 후술할 3가지 게이트(Gate)를 반드시 통과 -
승인:要確認:(확인 필요) 항목이 0개이고, 모든 FR/NFR이 사양에 참조되어 있는지 확인 후 구현으로 진행 -
플랜 제시·승인: 구현 단위·순서·영향 범위·테스트 항목을 제시하여 합의 -
구현: 상세 설계에 따라 최소 슬라이스(Slice) 단위로 만들고 다듬기 -
테스트: 사양의 테스트 항목을 그대로 옮겨 실행 -
구현 후 검증: 사양 전체 + 기존 코드와 대조하여, 차이가 있다면 양쪽을 일치시킴 -
증적: 동작 확인 결과(커맨드·로그·차이점)를 남김
「정전(Canon)」을 하나 둔다
개발 규칙을 작성한 하나의 파일을 **유일한 정전(Single Source of Truth)**으로 삼고 있습니다. 코딩 규약·사양 작성법·테스트 방침·검사 기준까지, 사람도 AI도 이곳을 보면 동일한 품질 판단을 내릴 수 있습니다. AI에 대한 지시는 매번 그때그때의 프롬프트가 아니라, 정전으로 일원화되어 있습니다.
spec-kit의 constitution과 유사한 발상이지만, 단순히 원칙 모음에 그치지 않고 검증 게이트나 출하 검사, 기술 규약까지 포함하는 「운용의 정전」으로 삼고 있다는 점이 다릅니다.
추측을 금지한다
사양에서 결정되지 않은 사항은 AI가 추측으로 채우게 하지 않습니다. 모호한 부분은 要確認:으로 남겨두고, 해소한 뒤에 구현으로 진행합니다. "아마 이럴 것이다"라는 내용을 코드에 섞지 않는 것이 품질의 가장 중요한 토대입니다.
판단이 갈리는 문제는 AI에게 결정하게 하지 않는다
값이 미확정된 것일 뿐인 사항은 규약이나 기본값으로 채웁니다. 하지만 설계 방침이 갈리는 선택(분할 방식, HTTP 상태 코드, 명명 규칙 등)은 AI가 마음대로 확정하지 않습니다. 선택지와 근거를 덧붙여 나에게 문의하고, 합의한 뒤에 사양에 반영합니다. 판단의 질은 사람이 쥐고 있다는 규칙입니다.
사양을 만들면 반드시 3가지 검증을 통과한다
사양서·설계서가 완성된 시점에서 기계적으로 3가지 게이트를 통과시킵니다.
- Web 검증: 현행 베스트 프랙티스(OWASP, 각 공식 문서 등)와 대조하여 누락을 제거함 -
- 재현성 검증: 문맥(Context)을 모르는 별도의 AI에게 동일한 사양으로부터 독립적으로 구현하게 해봄. 출력이 갈리는 지점은 "사양이 모호하여 품질이 안정되지 않는 지점"이므로, 그 부분을 규약이나
要確認:으로 확정함 - - 정합성 검증: 변경 사항이 사양 전체에 파급되어 모순을 일으키지 않는지(번호 매기기·상호 링크·상류/하류 대응)를 확인함
이 점이 일반적인 SDD와 결정적으로 다른 점입니다. 재현성 검증을 「자동화를 위해서」가 아니라 「모호함이라는 품질 리스크를 찾아내기 위해서」 사용하고 있습니다. AI가 두 번 작성했을 때 결과가 어긋난다면, 그것은 사양의 결함입니다.
요구사항을 끝까지 추적한다
모든 기능 요구사항(FR)과 비기능 요구사항(NFR)에 번호를 부여하여, 요구사항 → 설계 → 코드 → 테스트 → 검사가 하나로 이어지도록 하고 있습니다. "어떤 요구사항과도 연결되지 않는 코드"나 "어떤 사양에도 나타나지 않는 요구사항"을 grep을 통한 역추적(Reverse Lookup)으로 기계적으로 검출하여 제로(0)로 만듭니다. 누락을 정신력이 아닌 커맨드로 해결합니다.
문서 작성법까지 규약화한다
"1파일 1테마", "수식어 금지", "1항목당 40~60자", "판단에는 반드시 근거를 1행 덧붙임", "모호한 표현은 수치·고유명사·경로로 구체화" 등 문서 형식 자체를 규약으로 정했습니다. 누가 작성하더라도 동일한 구성과 동일한 입도의 사양서가 되도록 하기 위함입니다.
디자인도 "사양"으로 만들었다
디자인 출신으로서 외관에도 타협하지 않았습니다. 그리고 **디자인도 사양 주도(Specification-driven)**입니다.

가장 특징적인 것은 Figma를 사용하지 않는다는 점입니다.
- 디자인의 "정답(Truth)"을 **Storybook(실행 가능한 디자인 사양)**에 두었습니다. Figma와 코드에 정답이 이중화되어 동기화 비용이 발생하는 것을 피하기 위해서입니다. -
- 그 Storybook을 AI와 대화하며 구축했습니다. "여백을 더 줘", "모서리를 세워줘"라고 AI와 주고받으며 컴포넌트를 직접 코드로 다듬어 나갑니다. -
- 프론트엔드는 showcase를 보고 재구현하며, 디자인 결과물을 직접 import하지 않습니다. 이를 통해 디자인 승인 게이트를 유지합니다.
컴포넌트는 역할에 따라 계층을 나누고 있습니다.
| 계층 | 역할 | 예시 |
|---|---|---|
| ui | shadcn 생성물 (원칙적으로 그대로 유지) | Button, Dialog, Select |
| ... |
- shadcn/ui를 직접 사용하지 않고 **base로 래핑 (wrap)**함으로써, 수정 사항을 base에 가두어 재스킨 (re-skin)을 용이하게 함 (각 사 버전은 토큰 교체로 파생 가능)
- 디자인 토큰은 OKLCH로 정의 (명도·채도 조절이 용이). shadcn의 neutral 기본값을 계승하며, 덮어쓰기는 차분(diff)만 적용
디자인 사상은 "플레인 (plain), refined".
그레이스케일 (grayscale) 기조로 브랜드 컬러를 갖지 않음 (토대인 중립성을 유지하여, 각 사 버전에서 색을 입힐 여지를 남김)
코너 라운드 제로 (--radius: 0). 직각으로 긴장감을 줌
순백색이 아닌 매우 옅은 회색 #fafafa, 딱딱한 검은색이 아닌 부드러운 회색 (눈부심과 거부감을 피함)
status (성공·경고·위험)에만 유채색을 넣고, 색상에만 의존하지 않고 아이콘과 문구를 병기함 (색각 다양성 고려)
액세서빌리티 (accessibility)는 타협하지 않으며, 대비는 WCAG AA (실측 약 15:1), 모든 조작 부품에 포커스 링 (focus ring) 부여
비주얼 회귀 테스트 (Playwright 스냅샷)로 외관의 퇴행도 검출
"코드가 외관의 정답이다"라는 코드 퍼스트 (code-first) 디자인 시스템을 AI와 함께 만든다 ― 디자인과 엔지니어링을 모두 수행하는 사람으로서, 이 부분은 상당히 마음에 드는 지점입니다.
품질을 뒷받침하는 테스트와 CI
품질은 "말뿐"이면 의미가 없으므로, 기계로 지킵니다.
- 5계층 테스트: 단위 테스트 (pytest / vitest) / 통합 테스트 (실제 DB를 testcontainers로) / API 테스트 (httpx) / E2E 테스트 (playwright) / 인프라 테스트 (terraform validate·plan·apply→destroy→apply)
- 테스트는 사양 (specification)으로부터 도출 (구현을 따라가지 않음). 사양의 테스트 항목을 옮겨 적으며, 정상계뿐만 아니라 이상계(error case)·경계값도 작성
- 외부 의존성은 fake / in-memory로 교체하지만, DB 드라이버 자체는 모킹 (mock)하지 않음 (실제 DB로 통합을 확인)
- CI에서 포맷팅·타입·테스트를 매번 강제. 통과하지 못하면 머지 (merge)하지 않음
- 안전장치로서 **gitleaks (비밀 정보 유입 탐지)**와 **pip-audit / pnpm audit / Dependabot (의존성 취약점 스캔)**을 필수화
- 타입을 최대한 활용: TypeScript는 모호한 타입으로 회피하지 않고, Python은 타입 힌트(type hint) 필수, Pydantic은 제약 조건과 description을 반드시 부여 (Swagger 설명으로도 활용됨)
- 의존성의 재현성:
uv --frozen/pnpm --frozen-lockfile로 잠금(lock)하여, 누구의 환경에서도, CI에서도, 프로덕션 이미지에서도 동일한 것이 설치되도록 함
"동작함"으로 끝내지 않는다 ― 출하 전 검품과 그 증적
이 부분이 제가 가장 "품질을 위한 SDD"라고 생각하는 부분입니다.
보통 CI가 초록색이고 terraform apply가 성공하면 "완성되었다"고 생각하시죠. 하지만, **그것을 빠져나가는 런타임 버그 (runtime bug)**를 저는 실제로 몇 번이나 겪었습니다.
- 세션 기반이 클러스터 구성이라, 여러 키를 가로지르는 조작이 프로덕션에서만 깨짐
- 비밀 정보의 KMS 복호화 권한이, 기동하고 나서야 부족하다는 것을 알게 됨
- 비동기 워커의 메일 전송 권한이, 실제로 잡(job)을 실행할 때까지 드러나지 않음
- 로그인 단말기 이름이, 요청 헤더(request header) 전송 누락으로 인해 "알 수 없는 단말기"가 됨
이것들은 전부 CI에서도, apply 성공에서도 잡히지 않습니다. 실제로 기동하여 끝에서 끝까지 움직여봐야 비로소 나타납니다. "desired=0 (태스크 0개)로 배포하면, 기동되지 않기 때문에 런타임 버그가 전부 숨겨진다"는 함정에도 빠졌었습니다.
그래서, 출하 전 검품 (acceptance inspection)을 독립된 설계로서 가지고 있습니다.
- 주안점은 "CI 초록색·apply 성공"이 아니라, "실제로 가동되며, 끝에서 끝까지 동작함"을 증명하는 데 둠
- **맥락을 모르는 검사자 (다른 AI든 사람이든)**를 위해, 절차·기대값·증적 형식까지 자기 완결적으로 정의하며, 그 문서만 보고도 기계적으로 합불을 판정할 수 있게 함
검품은 계층별로 나누어 모두 통과시킵니다.
| 종류 | 대상 | 주요 목적 |
|---|---|---|
| A 배포 검품 | CD · 기동 · 초기 admin | 배포 후 기동하여 첫 로그인까지 통과 |
| ... |
실제 검품 결과
설계만으로는 의미가 없으므로, 프로덕션 등급 (production-grade)의 dev 환경을 대상으로 종합 검품을 실시하고, 그 결과를 증적 (evidence)으로 남겼습니다. 종합 판정은 합격. 내용을 발췌하면, 품질이 그대로 수치로 나타나 있습니다.
A 기동: front / back / worker 3개 서비스가 동일 이미지로 가동 (이미지 해시 일치 확인). 공개 URL의 헬스 체크 (health check)가 0.18초 만에 200 응답. 첫 배포 시 초기 관리자가 자동으로 시드 (seed) 되며, 재배포 시 중복 생성 및 중복 전송 없음 (멱등성, Idempotency) -
B 외부 의존: Aurora 통신 및 CRUD가 2xx로 성립. Valkey의 복수 키 조작 에러는 0건. SQS의 체류 및 DLQ(Dead Letter Queue) 모두 0. 메일은 최근 24시간 내 20통 이상의 송신 확인. DB secret의 암호 키와 execution 역할 (role)의 복호 키가 일치 (비밀 주입 = KMS 복호가 기능함) -
C 인수: 모든 기능 요구사항 (FR, Functional Requirements)의 주요 동선을 E2E (Playwright)로 통과시키고, back 액세스 로그의 상태 코드 집계로 검증. 로그인 204 / 잘못된 PW 401 / 메모 CRUD (201 · 200 · 204) / 존재하지 않음 404 / 잘못된 현재 PW로 변경 시 422 … 와 같이 기대 상태 코드와 모두 일치. 미로그인 상태에서 보호된 페이지는 /login
AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기