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를 확인해보세요.

댓글 없음:

댓글 쓰기