AI 시대의 SDD - 설계 문서가 중요한 이유

Go Yuumi·2026년 5월 21일

개요

AI 기반 개발을 하면서 가장 크게 느낀 문제는 “얼마나 빨리 구현하느냐”가 아니라, “내가 의도한 방식대로 구현되었는가” 였습니다. 구현을 맡기다 보면 다음 문제가 반복적으로 발생합니다.

  • 비즈니스 규칙과 다르게 구현됨
  • 컨텍스트가 길어질수록 추론 품질이 떨어짐
  • 이전 대화 내용을 잊어버림

요구사항을 정확하게 구현하기 위해서는 "좋은 프롬포트"보다 "좋은 컨텍스트"를 제공하는 것이 중요합니다.
명확한 요구사항, 일관된 도메인 용어, 시스템 흐름, 제약 조건과 같은 명세 기반 정보를 제공해야 합니다.

SDD를 도입한 이유

팀 내에서 SDD(Spec-Driven Development) 를 도입했습니다.

SDD는 구현 전에 스펙과 설계를 먼저 정의하고, 그 명세를 기준으로 개발과 검증을 진행하는 방식입니다.

왜 SDD인가

SDD를 적용하면 다음이 가능해집니다.

  • 용어가 통일됩니다. 기획자, 개발자, QA가 같은 단어로 같은 개념을 이야기합니다.
  • 시스템 흐름이 시각화됩니다. 구현 전에 설계 오류를 먼저 발견할 수 있습니다.
  • 실패 케이스가 미리 드러납니다. 엣지케이스를 코드 짜다가 발견하지 않아도 됩니다.

AI 활용 관점에서도 명세 기반 컨텍스트를 제공하기 때문에 코드 품질이 안정되고 추론 정확도가 올라갑니다.

워크플로우

기능 개발 시 다음 흐름을 따릅니다.

요구사항 → 시각화 → 명세 → 계획 → 구현 → 검증
단계산출물목적
요구사항PRD무엇을, 왜 만드는지 정의
시각화다이어그램 (C4, ERD, Sequence 등)시스템 구조와 흐름 정의
명세Feature Spec, API Spec구체적인 동작 계약 정의
계획Task 분해구현 단위로 쪼개기
구현코드TDD 기반 개발
검증테스트, AC명세 기준으로 검증

각 단계는 AI 슬래시 커맨드로 스킬화해서 사용하고 있습니다.

  • /sdd-req
  • /sdd-spec
  • /sdd-plan

1부. 요구사항 명세

PRD (Product Requirements Document)

기능을 개발할 때 "무엇을, 왜 만들어야 하는지"를 명확히 정의하는 핵심 기획 문서입니다.

PRD가 없으면 사람마다 기능을 다르게 해석하고, 구현 중간에 스코프가 바뀌고, 완료 기준이 모호해집니다.

구성 요소

섹션내용
Overview배경, 목표, KPI
User Stories사용자 관점 기능
Functional Requirements기능 요구사항
Non-functional Requirements성능, 보안, 가용성
Out of Scope이번 범위 제외
Open Questions미확정 사항

User Story 초안 자동 생성
"Out of Scope" 섹션이 특히 강력 — AI가 경계 케이스를 잘 잡아냄
Open Questions 목록 생성 → 회의 전 체크리스트로 활용

프롬프트 예시:
"다음 요구사항을 바탕으로 PRD 초안을 작성해줘.
특히 이번 범위에서 제외해야 할 항목(Out of Scope)과
아직 결정되지 않은 사항(Open Questions)을 반드시 포함해줘"

User Stories — 누가 무엇을 왜 원하는가

So that이 없으면 스펙이 아니라 기능 목록일 뿐입니다.
So that을 쓰다 보면 "이 기능이 정말 필요한가"를 자연스럽게 검증하게 됩니다.

IDAs aI want toSo that
US-01개인 플랜 유저팀 플랜으로 업그레이드팀원을 초대할 수 있다
US-02팀 플랜 유저개인 플랜으로 다운그레이드비용을 줄일 수 있다
US-03구독 유저결제 카드를 변경만료된 카드를 교체할 수 있다
US-04구독 유저플랜 변경 이력을 조회언제 변경했는지 확인할 수 있다

기능 요구사항 vs 비기능 요구사항

많은 팀이 기능 요구사항만 적고 비기능 요구사항을 빠뜨립니다.
비기능 요구사항은 구현 방식 전체에 영향을 주기 때문에 반드시 명시해야 합니다.

## 기능 요구사항 (Functional Requirements)
- 사용자는 플랜을 업그레이드/다운그레이드할 수 있다
- 업그레이드는 즉시 적용, 잔여 기간 일할 계산 청구
- 다운그레이드는 현재 구독 말일에 적용
항목요구사항측정 방법
성능결제 API 응답 3초 이내P99 응답시간 모니터링
보안카드 정보 직접 저장 금지 (PCI-DSS)Stripe 위임 처리
가용성결제 서비스 99.9% 업타임월간 다운타임 43분 이내
감사모든 플랜 변경 이벤트 1년 보관audit_log 테이블

Out of Scope — 가장 중요하지만 가장 많이 빠지는 섹션

"이번에 안 하는 것을 명시한다"

"그 기능도 되는 거 아니에요?"라는 질문이 개발 중간에 나오는 순간 스코프 크리프(scope creep)가 시작됩니다.
Out of Scope는 그 대화를 미리 끝내는 섹션입니다.

## Out of Scope
- ❌ 연간 구독 플랜 (추후 로드맵)
- ❌ 카드 외 결제 수단 (계좌이체, PayPal)
- ❌ 구독 해지 기능
- ❌ 플랜 변경 예약 취소
- ❌ 관리자의 타 유저 플랜 강제 변경

2부. 다이어그램

텍스트 문서(PRD, Feature Spec)만으로는 구조와 흐름을 전달하기 어렵습니다.
다이어그램은 그 간극을 메우는 시각 언어입니다.

왜 다이어그램을 그리는가

다음 문장을 읽어보겠습니다.

"결제 요청이 들어오면 API 서버가 검증 후 Stripe에 요청을 보내고,
결과를 받아 DB에 저장한 뒤 이메일 서비스를 호출한다"

읽고 나서도 머릿속에 그림이 잘 안 그려집니다.
컴포넌트 간 순서와 의존성이 모호하고, 팀원마다 다르게 이해할 수 있습니다.

다이어그램이 하는 일은 다섯 가지입니다.

  1. 복잡한 구조를 한눈에 — 텍스트 500자를 그림 1장으로
  2. 팀 공통 멘탈 모델 형성 — "우리가 만드는 게 이렇게 생겼다"
  3. 설계 오류 조기 발견 — 그리다 보면 순환 의존성, 병목이 보임
  4. 온보딩 비용 절감 — 새 팀원이 코드 읽기 전 구조 파악 가능
  5. 의사결정 근거 보존 — 왜 이렇게 설계했는지 기록

다이어그램의 표준 — UML

다이어그램에도 공통 언어가 있습니다. 바로 UML(Unified Modeling Language) 입니다.

분류목적대표 다이어그램
구조 다이어그램시스템이 어떻게 생겼는가 (정적)클래스, 컴포넌트, 배포
행위 다이어그램시스템이 어떻게 동작하는가 (동적)시퀀스, 유스케이스, 상태 전이

실무에서 UML을 대하는 자세
UML을 100% 정확하게 지키는 팀은 거의 없습니다.
중요한 건 팀 내 기호의 의미가 통일되는 것입니다.
표준을 알고 나서 현실에 맞게 쓰는 게 올바른 접근입니다.

시퀀스 다이어그램

언제 작성하는가

  • API 설계 전 흐름 검증
  • 결제, 인증처럼 실패 케이스가 많은 플로우
  • 마이크로서비스 간 통신이 복잡한 경우

작성 시 고려해야 할 것들

  • 정상 흐름과 실패 흐름을 alt 블록으로 분리해서 명시합니다.
  • 비동기 처리는 점선 화살표로 동기와 구분합니다.
  • 모든 참여자(Lifeline)를 왼쪽에서 오른쪽으로 호출 순서대로 배치합니다.
  • 응답이 없는 단방향 이벤트(Fire and Forget)도 명시합니다.
sequenceDiagram
  participant C as Client
  participant A as API Server
  participant DB as PostgreSQL
  participant S as Stripe
  participant E as EmailService
 
  C->>A: POST /subscriptions/{id}/upgrade
  A->>DB: SELECT subscription
  DB-->>A: subscription data
 
  A->>S: 결제 요청 (proration)
 
  alt 결제 성공
    S-->>A: payment_intent (success)
    A->>DB: INSERT plan_change (applied)
    A->>DB: UPDATE subscription (plan_id)
    A--)E: 이메일 발송 이벤트 (비동기)
    A-->>C: 200 OK
  else 결제 실패
    S-->>A: payment_intent (failed)
    A-->>C: 402 PAYMENT_FAILED
  end

클래스 다이어그램

시스템을 구성하는 클래스, 속성, 메서드, 그리고 클래스 간의 관계를 표현합니다.

저는 두 가지 용도로 사용하고 있습니다.

용도목적
도메인 모델비즈니스 개념과 규칙을 표현
컴포넌트코드 구조와 의존성 방향 표현

도메인 모델 클래스 다이어그램

비즈니스 개념을 코드로 옮기기 전에 그립니다.
기술 구현과 무관하게 비즈니스 규칙을 표현하며, 메서드보다 속성과 관계에 집중합니다.

classDiagram
  direction TB
 
  class User {
    -id: UUID
    -email: String
    -name: String
    +getActiveSubscription() Subscription
  }
 
  class Subscription {
    -id: UUID
    -status: SubscriptionStatus
    -currentPeriodStart: Date
    -currentPeriodEnd: Date
    +isActive() Boolean
    +getRemainingDays() Int
    +canDowngradeTo(plan: Plan) Boolean
  }
 
  class Plan {
    -id: UUID
    -name: PlanType
    -priceKrw: Int
    -maxMembers: Int
    +isTeamPlan() Boolean
  }
 
  class PlanChange {
    -id: UUID
    -status: PlanChangeStatus
    -applyAt: Date
    -prorationAmount: Money
    +isUpgrade() Boolean
    +apply() void
    +cancel() void
  }
 
  class Payment {
    -id: UUID
    -amountKrw: Money
    -status: PaymentStatus
    -pgTransactionId: String
    +isSuccess() Boolean
    +retry() void
  }
 
  class SubscriptionStatus {
    <<enumeration>>
    ACTIVE
    PAUSED
    CANCELLED
  }
 
  class PlanChangeStatus {
    <<enumeration>>
    SCHEDULED
    APPLIED
    CANCELLED
  }
 
  User "1" --> "0..*" Subscription : has
  Subscription "0..*" --> "1" Plan : belongs to
  Subscription "1" --> "0..*" PlanChange : has
  Subscription "1" --> "0..*" Payment : has

도메인 모델 작성 팁

비즈니스 규칙을 메서드 이름으로 표현합니다.

// ❌ 기술적 CRUD만 있으면 도메인 의도가 안 보임
+ save()
+ delete()
+ update()
 
// ✅ 비즈니스 행위를 메서드로
+ canDowngradeTo(plan: Plan): Boolean
+ apply(): void
+ cancel(): void

컴포넌트 구조 클래스 다이어그램

도메인 모델이 무엇을 만드는지를 보여준다면, 어떻게 코드가 조직되는지를 보여줍니다.

classDiagram
  direction TB
 
  class SubscriptionController {
    +upgradePlan(req, res)
    +downgradePlan(req, res)
  }
 
  class PlanChangeService {
    -subscriptionRepo: ISubscriptionRepository
    -paymentGateway: IPaymentGateway
    +requestUpgrade(command: UpgradeCommand)
    +requestDowngrade(command: DowngradeCommand)
    +applyScheduled()
  }
 
  class ISubscriptionRepository {
    <<interface>>
    +findById(id: UUID) Subscription
    +save(sub: Subscription)
  }
 
  class IPaymentGateway {
    <<interface>>
    +charge(amount: Money) PaymentResult
    +refund(transactionId: String)
  }
 
  class SubscriptionRepository {
    -db: PostgresClient
    +findById(id: UUID) Subscription
    +save(sub: Subscription)
  }
 
  class StripeGateway {
    -client: StripeClient
    +charge(amount: Money) PaymentResult
    +refund(transactionId: String)
  }
 
  SubscriptionController ..> PlanChangeService : uses
  PlanChangeService ..> ISubscriptionRepository : uses
  PlanChangeService ..> IPaymentGateway : uses
  SubscriptionRepository ..|> ISubscriptionRepository : implements
  StripeGateway ..|> IPaymentGateway : implements

이 다이어그램에서 확인해야 할 것은 인터페이스를 통한 의존성 역전(DIP) 입니다.

주의: 클래스 다이어그램에서 자주 하는 실수

모든 클래스, 모든 메서드, 모든 필드를 그리지 않습니다.
"이 다이어그램이 보여주는 것은 OOO이다"라는 제목이 있어야 합니다.
설명하고 싶은 개념에 필요한 것만 그리는 것이 좋은 다이어그램입니다.

ERD

DB 설계와 도메인 모델을 정의할 때 작성합니다.
엔티티 간 관계(1:1, 1:N, N:M)와 제약 조건을 명시합니다.

ERD에서 자주 빠지는 것들

  • deleted_at 소프트 딜리트 컬럼 정책
  • 인덱스 설계 (ERD에 명시하면 리뷰 때 논의 가능)
  • NULL 허용 여부 (관계의 선택성과 직결)
erDiagram
  USER {
    uuid id PK
    string email
    string name
    timestamp created_at
  }
 
  SUBSCRIPTION {
    uuid id PK
    uuid user_id FK
    uuid plan_id FK
    string status "active|paused|cancelled"
    date current_period_start
    date current_period_end
  }
 
  PLAN {
    uuid id PK
    string name "personal|team"
    int price_krw
    int max_members "NULL이면 무제한"
  }
 
  PLAN_CHANGE {
    uuid id PK
    uuid subscription_id FK
    uuid from_plan_id FK
    uuid to_plan_id FK
    string status "scheduled|applied|cancelled"
    date apply_at
    int proration_amount
  }
 
  PAYMENT {
    uuid id PK
    uuid subscription_id FK
    int amount_krw
    string status "success|failed|refunded"
    string pg_transaction_id
    timestamp paid_at
  }
 
  USER ||--o{ SUBSCRIPTION : "has"
  SUBSCRIPTION }o--|| PLAN : "belongs to"
  SUBSCRIPTION ||--o{ PLAN_CHANGE : "has"
  SUBSCRIPTION ||--o{ PAYMENT : "has"

상태 다이어그램

주문, 결제, 구독처럼 상태가 바뀌는 도메인 객체를 설계할 때 작성합니다.
엣지케이스 발굴에 가장 효과적인 도구입니다.

불가능한 상태 전이를 시각적으로 식별하고, 그리다 보면 자연스럽게 "이 상태에서 저 상태로 가는 경우는 없나?"라는 질문이 나옵니다. 코드 짜다가 발견하는 것보다 훨씬 낫습니다.

stateDiagram-v2
  [*] --> pending : 결제 요청
 
  pending --> success : 결제 승인
  pending --> failed : 결제 거절
 
  failed --> pending : 재시도 (3회 이내)
  failed --> permanently_failed : 재시도 초과
 
  success --> refunded : 환불 요청
  permanently_failed --> [*]
  refunded --> [*]

마치며

구현 자동화 시대에 역설적이게도 설계 문서의 가치는 더욱 커지고 있습니다.
AI가 코드를 빠르게 생성할수록, "무엇을 만드는가"를 명확히 하는 것이 더 중요해집니다.
좋은 스펙은 좋은 AI 컨텍스트이고, 좋은 AI 컨텍스트는 좋은 코드를 만듭니다.
구현 전에 사고를 정리하는 과정은 느린 것처럼 보이지만, 결국 가장 빠른 길입니다.

profile
개발 서포터 탈출기

0개의 댓글