레이블이 스펙작성인 게시물을 표시합니다. 모든 게시물 표시
레이블이 스펙작성인 게시물을 표시합니다. 모든 게시물 표시

2025년 11월 23일 일요일

AI가 이해하는 명세 작성법

"AI한테 명세를 어떻게 써줘야 제대로 코드가 나올까요?"

최근 가장 많이 받는 질문입니다. ChatGPT, Claude, Copilot... 다들 쓰고는 있는데, 원하는 결과가 안 나온다는 거죠.

비밀을 알려드릴게요: AI는 컴파일러처럼 생각하세요.

컴파일러에게 문법 틀린 코드를 주면? 에러가 나죠. AI도 마찬가지입니다. 명세가 모호하면 결과도 모호합니다.

사람을 위한 명세 vs AI를 위한 명세

10년 전과 지금의 차이를 보겠습니다.

10년 전: 사람이 읽는 명세

로그인 기능
- 이메일과 비밀번호로 로그인
- 성공시 대시보드로 이동
- 실패시 에러 표시

개발자가 알아서 나머지를 채워넣을 거라 기대했죠.

지금: AI가 읽는 명세

Function: authenticateUser
Method: POST /api/v1/auth/login

Input:
  email:
    type: string
    format: email
    required: true
    validation: RFC 5322
    example: "[email protected]"
  
  password:
    type: string
    required: true
    minLength: 8
    maxLength: 128
    validation: 
      - 최소 1개 대문자
      - 최소 1개 숫자
      - 최소 1개 특수문자

Output:
  success:
    status: 200
    body:
      token: string (JWT)
      expiresIn: number (seconds)
      refreshToken: string
  
  failure:
    status: 401
    body:
      error: "Invalid credentials"
      code: "AUTH_FAILED"

Business Logic:
  1. 이메일 존재 확인
  2. 비밀번호 bcrypt 비교
  3. 로그인 시도 기록
  4. 5회 실패시 계정 잠금
  5. JWT 토큰 발급 (1시간)
  6. 리프레시 토큰 발급 (30일)

Error Cases:
  - 이메일 없음: 404
  - 비밀번호 틀림: 401
  - 계정 잠김: 423
  - 서버 에러: 500

차이가 보이시나요? AI는 암묵적 지식이 없습니다. 모든 걸 명시해야 합니다.

AI 친화적 명세의 5가지 원칙

1. 구조화 (Structured)

AI는 구조를 좋아합니다. 자유 형식보다는 정형화된 포맷을 사용하세요.

❌ 나쁜 예

사용자가 로그인하면 토큰을 주고 대시보드로 보내고 
실패하면 에러 메시지를 보여주는데 5번 틀리면 잠그고...

✅ 좋은 예

Steps:
  1. 입력 검증
  2. DB 조회
  3. 비밀번호 확인
  4. 토큰 생성
  5. 응답 반환

Error Handling:
  - Case: 이메일 없음
    Action: 404 반환
  - Case: 비밀번호 틀림
    Action: 401 반환, 시도 횟수 증가

2. 구체화 (Specific)

"적절히", "필요시", "일반적으로" 같은 모호한 표현은 금물입니다.

❌ 모호한 명세

- 적절한 검증 수행
- 필요시 캐싱
- 일반적인 에러 처리

✅ 구체적 명세

- 이메일: RFC 5322 형식 검증
- 캐싱: Redis, TTL 3600초
- 에러: 400(검증), 401(인증), 500(서버)

3. 예시 포함 (Examples)

AI는 예시를 통해 패턴을 학습합니다.

Examples:
  Valid Input:
    email: "[email protected]"
    password: "SecurePass123!"
  
  Expected Output:
    {
      "token": "eyJhbGciOiJIUzI1NiIs...",
      "expiresIn": 3600,
      "refreshToken": "f47d3b2a-9c8e..."
    }
  
  Invalid Input:
    email: "not-an-email"
    password: "123"
  
  Expected Error:
    {
      "errors": [
        "Invalid email format",
        "Password too short"
      ]
    }

4. 제약사항 명시 (Constraints)

기술적, 비즈니스적 제약을 명확히 하세요.

Constraints:
  Technical:
    - Node.js 18+
    - Express 4.x
    - PostgreSQL 14+
    - JWT (not sessions)
  
  Business:
    - 비밀번호 최소 8자
    - 토큰 유효기간 1시간
    - 동시 로그인 3개까지
    - GDPR 준수
  
  Performance:
    - 응답시간 < 200ms
    - 동시 요청 1000/s

5. 테스트 케이스 (Test Cases)

테스트 케이스를 명세에 포함하면 AI가 더 정확한 코드를 생성합니다.

Test Cases:
  - name: "정상 로그인"
    input: {email: "[email protected]", password: "Test123!"}
    expected: {status: 200, hasToken: true}
  
  - name: "잘못된 이메일"
    input: {email: "invalid", password: "Test123!"}
    expected: {status: 400, error: "Invalid email"}
  
  - name: "계정 잠금"
    setup: "5회 로그인 실패 후"
    input: {email: "[email protected]", password: "Test123!"}
    expected: {status: 423, error: "Account locked"}

실전: 결제 시스템 명세 작성

실제 프로젝트에서 사용한 명세입니다.

# 결제 처리 시스템 명세

## Overview
System: Payment Processing Service
Purpose: Stripe를 사용한 구독 결제 처리
Version: 1.0.0

## Architecture
```mermaid
graph LR
    Client --> API
    API --> Stripe
    API --> Database
    Stripe --> Webhook
    Webhook --> API
``` {data-source-line="218"}

## API Endpoints

### 1. Create Subscription
POST /api/v1/subscriptions

Request:
  headers:
    Authorization: Bearer {token}
  body:
    planId: string # "basic" | "pro" | "enterprise"
    paymentMethodId: string # Stripe payment method
    couponCode?: string

Response:
  200 OK:
    subscriptionId: string
    status: "active" | "trialing"
    currentPeriodEnd: ISO8601
    nextBillingDate: ISO8601
  
  400 Bad Request:
    error: "Invalid plan"
    code: "INVALID_PLAN"
  
  402 Payment Required:
    error: "Payment failed"
    code: "PAYMENT_FAILED"
    declineCode: string

Business Rules:
  1. 사용자당 1개 구독만 허용
  2. 업그레이드는 즉시 적용, 다운그레이드는 다음 결제일
  3. 3일 무료 체험 (카드 등록 필수)
  4. 실패시 3회 재시도 (1일, 3일, 5일)

Implementation Details:
  - Stripe Customer 생성/조회
  - Payment Method 첨부
  - Subscription 생성
  - DB 저장 (subscriptions 테이블)
  - 이메일 알림 발송

### 2. Cancel Subscription
DELETE /api/v1/subscriptions/{id}

Parameters:
  id: string (subscription ID)
  immediately?: boolean (default: false)

Response:
  200 OK:
    canceledAt: ISO8601
    cancelAtPeriodEnd: boolean

Business Rules:
  - 즉시 취소: 남은 기간 환불 없음
  - 기간 종료 취소: 남은 기간 사용 가능

### 3. Webhook Handler
POST /api/v1/webhooks/stripe

Security:
  - Stripe 서명 검증 필수
  - IP 화이트리스트 (옵션)

Events:
  customer.subscription.created:
    - DB 상태 업데이트
    - 환영 이메일 발송
  
  customer.subscription.updated:
    - 플랜 변경 처리
    - 알림 발송
  
  customer.subscription.deleted:
    - 접근 권한 제거
    - 취소 설문 이메일
  
  invoice.payment_failed:
    - 재시도 스케줄
    - 경고 이메일

## Database Schema

```sql
CREATE TABLE subscriptions (
  id UUID PRIMARY KEY,
  user_id UUID REFERENCES users(id),
  stripe_subscription_id VARCHAR(255) UNIQUE,
  stripe_customer_id VARCHAR(255),
  plan_id VARCHAR(50),
  status VARCHAR(50),
  current_period_start TIMESTAMP,
  current_period_end TIMESTAMP,
  cancel_at_period_end BOOLEAN DEFAULT false,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_user_subscription ON subscriptions(user_id);
CREATE INDEX idx_stripe_subscription ON subscriptions(stripe_subscription_id);
``` {data-source-line="321"}

## Error Handling

```typescript
class PaymentError extends Error {
  constructor(
    message: string,
    public code: string,
    public statusCode: number,
    public details?: any
  ) {
    super(message);
  }
}

// Usage
throw new PaymentError(
  'Payment method declined',
  'PAYMENT_DECLINED',
  402,
  { declineCode: 'insufficient_funds' }
);
``` {data-source-line="344"}

## Testing

### Unit Tests
```typescript
describe('SubscriptionService', () => {
  it('should create subscription for new user', async () => {
    // Given
    const userId = 'user123';
    const planId = 'pro';
    
    // When
    const subscription = await service.create(userId, planId);
    
    // Then
    expect(subscription.status).toBe('active');
    expect(subscription.planId).toBe('pro');
  });
  
  it('should prevent duplicate subscriptions', async () => {
    // Given: user with existing subscription
    // When: attempt to create another
    // Then: throw error
  });
});
``` {data-source-line="370"}

### Integration Tests
- Stripe Test Mode 사용
- Test 카드 번호 사용
- Webhook 시뮬레이션

## Monitoring

Metrics:
  - 결제 성공률
  - 평균 응답 시간
  - 재시도 횟수
  - 취소율

Alerts:
  - 성공률 < 95%
  - 응답시간 > 1초
  - 5xx 에러 > 1%

이런 명세를 AI에게 주면, 프로덕션 레벨의 코드가 나옵니다.

AI별 명세 작성 팁

ChatGPT

  • 대화형으로 점진적 개선
  • "이전 코드를 기반으로..." 활용
  • 컨텍스트 유지 중요

Claude

  • 한 번에 전체 명세 제공
  • 긴 문서도 잘 처리
  • 구조화된 출력 요청

GitHub Copilot

  • 주석으로 명세 작성
  • 함수 시그니처 먼저 작성
  • 테스트 먼저 작성하면 더 정확

명세 템플릿 모음

제가 실제 사용하는 템플릿들입니다:

API 엔드포인트

Endpoint: [METHOD] /path
Purpose: [한 줄 설명]
Authentication: [Required/Optional]
Request: [구조]
Response: [구조]
Errors: [에러 케이스]
Business Logic: [단계별]

데이터 모델

Model: [이름]
Table: [테이블명]
Fields:
  - name: type, constraints
Relations:
  - type: target
Indexes:
  - fields
Validations:
  - rules

비즈니스 로직

Function: [이름]
Input: [파라미터]
Output: [반환값]
Preconditions: [사전 조건]
Postconditions: [사후 조건]
Steps: [알고리즘]
Edge Cases: [예외 상황]

마무리: 명세는 투자다

"명세 쓸 시간에 코딩하는 게 빠르지 않나요?"

예전엔 그랬을지도 모릅니다. 하지만 AI 시대에는 다릅니다.

명세 작성 30분 = AI 코드 생성 3분 = 수동 코딩 3시간

명세를 잘 쓰면:

  • AI가 정확한 코드 생성
  • 팀원과 명확한 소통
  • 문서화 자동 완성
  • 테스트 케이스 명확

명세는 비용이 아니라 투자입니다.

특히 WBS와 결합하면 더 강력합니다. WBS의 각 태스크마다 명확한 명세를 작성하면, AI가 전체 프로젝트를 거의 자동으로 구현할 수 있습니다.

다음에 AI를 쓸 때는 "대충" 요청하지 마시고, 제대로 된 명세를 작성해보세요.

놀라운 결과를 보게 될 겁니다.


AI 시대의 프로젝트 관리와 명세 작성이 필요하신가요? Plexo를 확인해보세요.