Skip to content

구독 서비스 연동 방안

NextMarket에서 구독 상품 결제 시 각 SaaS 서비스(aiagent 등)에 구독 활성화를 전달하는 아키텍처


1. 개요

1.1 현재 상황

┌─────────────────────────────────────────────────────────────┐
│                      NextMarket                              │
│                   (결제 플랫폼 - StepPay 연동)                │
├─────────────────────────────────────────────────────────────┤
│  상품 A: aiagent 월간 구독                                    │
│  상품 B: 다른 SaaS 서비스 구독                                │
│  상품 C: 또 다른 서비스 구독                                  │
└─────────────────────────────────────────────────────────────┘

         │ 결제 완료 후 각 서비스에 구독 활성화 필요

┌─────────────┐  ┌─────────────┐  ┌─────────────┐
│   aiagent   │  │  Service B  │  │  Service C  │
│     API     │  │     API     │  │     API     │
└─────────────┘  └─────────────┘  └─────────────┘

1.2 핵심 요구사항

  1. 상품별 서비스 구분: 어떤 상품이 어떤 서비스와 연동되는지 관리
  2. 구독 상태 동기화: 활성화/일시정지/해지 등 상태 변경 시 서비스에 전파
  3. 확장성: 새로운 서비스 추가 시 코드 변경 최소화

2. 아키텍처 설계

2.1 전체 흐름

┌──────────────────────────────────────────────────────────────────────┐
│                           NextMarket API                              │
├──────────────────────────────────────────────────────────────────────┤
│                                                                       │
│  [1] StepPay Webhook 수신                                            │
│        │                                                              │
│        ▼                                                              │
│  [2] Webhook Handler (subscription.paid, subscription.cancelled 등)  │
│        │                                                              │
│        ▼                                                              │
│  [3] Service Registry에서 상품에 연결된 서비스 조회                    │
│        │                                                              │
│        ▼                                                              │
│  [4] 각 서비스 Adapter를 통해 구독 상태 전파                          │
│        │                                                              │
│        ├──────────────┬──────────────┬──────────────┐                │
│        ▼              ▼              ▼              ▼                │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐            │
│  │ aiagent  │  │ Service  │  │ Service  │  │ Generic  │            │
│  │ Adapter  │  │ B Adapter│  │ C Adapter│  │ Webhook  │            │
│  └──────────┘  └──────────┘  └──────────┘  └──────────┘            │
│        │              │              │              │                │
└────────┼──────────────┼──────────────┼──────────────┼────────────────┘
         │              │              │              │
         ▼              ▼              ▼              ▼
   aiagent-api    service-b-api  service-c-api   외부 서비스

2.2 서비스 레지스트리 패턴

상품과 서비스의 매핑을 DB에서 관리하여 유연성 확보:

┌─────────────────────┐      ┌─────────────────────────────┐
│      PRODUCTS       │      │     SERVICE_REGISTRY        │
├─────────────────────┤      ├─────────────────────────────┤
│ ID: 1001            │      │ SERVICE_CODE: aiagent       │
│ NAME: AI Agent 월간 │──┐   │ SERVICE_NAME: AI Agent      │
│ CODE: aiagent_m01   │  │   │ BASE_URL: https://aiagent..│
└─────────────────────┘  │   │ WEBHOOK_PATH: /webhooks/sub│
                         │   └─────────────────────────────┘


              ┌─────────────────────────────┐
              │  PRODUCT_SERVICE_MAPPING    │
              ├─────────────────────────────┤
              │ PRODUCT_ID: 1001            │
              │ SERVICE_CODE: aiagent       │
              │ SYNC_CONFIG: {...}          │
              └─────────────────────────────┘

3. 데이터베이스 스키마

3.1 SERVICE_REGISTRY (서비스 레지스트리)

sql
CREATE TABLE SERVICE_REGISTRY (
    SERVICE_CODE        VARCHAR(50) PRIMARY KEY COMMENT '서비스 코드',
    SERVICE_NAME        VARCHAR(100) NOT NULL COMMENT '서비스명',
    BASE_URL            VARCHAR(500) NOT NULL COMMENT '서비스 API URL',
    WEBHOOK_PATH        VARCHAR(200) DEFAULT '/webhooks/subscription' COMMENT '웹훅 경로',
    API_KEY             VARCHAR(255) NULL COMMENT 'API 인증키',
    AUTH_TYPE           ENUM('BEARER', 'API_KEY', 'BASIC', 'NONE') DEFAULT 'BEARER',
    IS_ACTIVE           BOOLEAN DEFAULT TRUE COMMENT '활성화 여부',
    RETRY_COUNT         INT DEFAULT 3 COMMENT '재시도 횟수',
    TIMEOUT_MS          INT DEFAULT 5000 COMMENT '타임아웃 (ms)',
    CREATE_TS           DATETIME DEFAULT CURRENT_TIMESTAMP,
    MODIFY_TS           DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    INDEX IDX_ACTIVE (IS_ACTIVE)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

예시 데이터:

SERVICE_CODESERVICE_NAMEBASE_URLAPI_KEY
aiagentAI Agent 서비스https://api.aiagent.comsk_xxx
analytics분석 서비스https://api.analytics.comak_yyy

3.2 PRODUCT_SERVICE_MAPPING (상품-서비스 매핑)

sql
CREATE TABLE PRODUCT_SERVICE_MAPPING (
    PRODUCT_ID          BIGINT NOT NULL COMMENT 'StepPay 상품 ID',
    SERVICE_CODE        VARCHAR(50) NOT NULL COMMENT '서비스 코드',
    SYNC_CONFIG         JSON NULL COMMENT '동기화 설정 (서비스별 추가 파라미터)',
    IS_ACTIVE           BOOLEAN DEFAULT TRUE,
    CREATE_TS           DATETIME DEFAULT CURRENT_TIMESTAMP,

    PRIMARY KEY (PRODUCT_ID, SERVICE_CODE),
    FOREIGN KEY (PRODUCT_ID) REFERENCES PRODUCTS(ID) ON DELETE CASCADE,
    FOREIGN KEY (SERVICE_CODE) REFERENCES SERVICE_REGISTRY(SERVICE_CODE) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

SYNC_CONFIG 예시:

json
{
  "planType": "SUBSCRIBE:M01",
  "features": ["chat", "analytics"],
  "maxUsers": 5
}

3.3 SERVICE_SYNC_LOG (동기화 로그)

sql
CREATE TABLE SERVICE_SYNC_LOG (
    ID                  BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    SUBSCRIPTION_ID     BIGINT NOT NULL COMMENT '구독 ID',
    SERVICE_CODE        VARCHAR(50) NOT NULL COMMENT '서비스 코드',
    EVENT_TYPE          VARCHAR(50) NOT NULL COMMENT '이벤트 타입',
    REQUEST_PAYLOAD     JSON NULL COMMENT '요청 데이터',
    RESPONSE_PAYLOAD    JSON NULL COMMENT '응답 데이터',
    STATUS              ENUM('PENDING', 'SUCCESS', 'FAILED', 'RETRY') DEFAULT 'PENDING',
    ERROR_MESSAGE       TEXT NULL,
    RETRY_COUNT         INT DEFAULT 0,
    CREATE_TS           DATETIME DEFAULT CURRENT_TIMESTAMP,
    PROCESSED_TS        DATETIME NULL,

    INDEX IDX_SUBSCRIPTION (SUBSCRIPTION_ID),
    INDEX IDX_SERVICE (SERVICE_CODE),
    INDEX IDX_STATUS (STATUS),
    INDEX IDX_CREATE_TS (CREATE_TS)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

4. 이벤트 타입

4.1 구독 관련 이벤트

이벤트설명서비스 액션
subscription.activated구독 활성화 (첫 결제 완료)서비스 접근 권한 부여
subscription.renewed구독 갱신 (정기 결제 완료)만료일 연장
subscription.paused구독 일시정지서비스 일시 중단
subscription.resumed구독 재개서비스 접근 복구
subscription.cancelled구독 해지서비스 접근 권한 제거
subscription.expired구독 만료서비스 접근 권한 제거

4.2 웹훅 페이로드 구조

typescript
interface SubscriptionWebhookPayload {
  eventType: string;              // 이벤트 타입
  timestamp: string;              // ISO 8601 형식
  subscription: {
    id: number;                   // NextMarket 구독 ID
    code: string;                 // 구독 코드
    status: string;               // 구독 상태
    startDate: string;            // 시작일
    endDate: string;              // 종료일 (해지 시)
    nextPaymentDate?: string;     // 다음 결제일
  };
  customer: {
    id: number;                   // NextMarket 고객 ID
    code: string;                 // 고객 코드
    email: string;
    name?: string;
    phone?: string;
  };
  product: {
    id: number;                   // 상품 ID
    code: string;                 // 상품 코드
    name: string;                 // 상품명
  };
  syncConfig?: object;            // PRODUCT_SERVICE_MAPPING.SYNC_CONFIG
}

5. 구현 방안

5.1 서비스 연동 모듈 구조

src/modules/service-sync/
├── service-sync.module.ts
├── service-sync.service.ts       # 메인 동기화 로직
├── service-registry.service.ts   # 서비스 레지스트리 관리
├── adapters/
│   ├── base.adapter.ts           # 기본 어댑터 인터페이스
│   ├── aiagent.adapter.ts        # aiagent 전용 어댑터
│   ├── generic-webhook.adapter.ts # 범용 웹훅 어댑터
│   └── index.ts
├── dto/
│   └── webhook-payload.dto.ts
└── interfaces/
    └── service-adapter.interface.ts

5.2 어댑터 인터페이스

typescript
// interfaces/service-adapter.interface.ts
export interface ServiceAdapter {
  serviceCode: string;

  // 구독 활성화
  activateSubscription(payload: SubscriptionWebhookPayload): Promise<AdapterResult>;

  // 구독 갱신
  renewSubscription(payload: SubscriptionWebhookPayload): Promise<AdapterResult>;

  // 구독 일시정지
  pauseSubscription(payload: SubscriptionWebhookPayload): Promise<AdapterResult>;

  // 구독 재개
  resumeSubscription(payload: SubscriptionWebhookPayload): Promise<AdapterResult>;

  // 구독 해지
  cancelSubscription(payload: SubscriptionWebhookPayload): Promise<AdapterResult>;
}

export interface AdapterResult {
  success: boolean;
  serviceResponse?: any;
  error?: string;
}

5.3 aiagent 어댑터 구현 예시

typescript
// adapters/aiagent.adapter.ts
@Injectable()
export class AiagentAdapter implements ServiceAdapter {
  serviceCode = 'aiagent';

  constructor(private readonly httpService: HttpService) {}

  async activateSubscription(payload: SubscriptionWebhookPayload): Promise<AdapterResult> {
    // aiagent API 호출: 구독 승인
    const response = await this.httpService.post(
      `${payload.serviceConfig.baseUrl}/v1/subscribe/approval`,
      {
        customerId: payload.customer.id,
        customerEmail: payload.customer.email,
        subType: payload.syncConfig?.planType || 'SUBSCRIBE:M01',
        subStart: payload.subscription.startDate,
        subFinish: payload.subscription.endDate,
      }
    ).toPromise();

    return {
      success: response.data.success,
      serviceResponse: response.data,
    };
  }

  async cancelSubscription(payload: SubscriptionWebhookPayload): Promise<AdapterResult> {
    // aiagent API 호출: 구독 만료 처리
    const response = await this.httpService.post(
      `${payload.serviceConfig.baseUrl}/v1/subscribe/expire`,
      {
        customerId: payload.customer.id,
      }
    ).toPromise();

    return {
      success: response.data.success,
      serviceResponse: response.data,
    };
  }

  // ... 나머지 메서드 구현
}

5.4 범용 웹훅 어댑터

새로운 서비스 추가 시 코드 변경 없이 DB 설정만으로 연동:

typescript
// adapters/generic-webhook.adapter.ts
@Injectable()
export class GenericWebhookAdapter implements ServiceAdapter {
  serviceCode = 'generic';

  async activateSubscription(
    payload: SubscriptionWebhookPayload,
    serviceConfig: ServiceRegistry
  ): Promise<AdapterResult> {
    const webhookUrl = `${serviceConfig.baseUrl}${serviceConfig.webhookPath}`;

    const response = await this.httpService.post(webhookUrl, {
      eventType: 'subscription.activated',
      ...payload,
    }, {
      headers: this.buildAuthHeaders(serviceConfig),
      timeout: serviceConfig.timeoutMs,
    }).toPromise();

    return {
      success: response.status === 200,
      serviceResponse: response.data,
    };
  }

  private buildAuthHeaders(config: ServiceRegistry): Record<string, string> {
    switch (config.authType) {
      case 'BEARER':
        return { 'Authorization': `Bearer ${config.apiKey}` };
      case 'API_KEY':
        return { 'X-API-Key': config.apiKey };
      default:
        return {};
    }
  }
}

6. 처리 흐름

6.1 구독 활성화 흐름

StepPay Webhook (subscription.paid)


┌─────────────────────────────────────────────┐
│  WebhooksController.handleSteppayWebhook()  │
│  - 웹훅 검증 및 로깅                          │
└─────────────────────────────────────────────┘


┌─────────────────────────────────────────────┐
│  ServiceSyncService.handleSubscriptionEvent()│
│  1. 구독 정보 조회                            │
│  2. 상품에 연결된 서비스 목록 조회             │
│  3. 각 서비스별 어댑터 호출                    │
└─────────────────────────────────────────────┘

         ├─── aiagent 연결됨 ──────────────────┐
         │                                     ▼
         │                    ┌────────────────────────────┐
         │                    │  AiagentAdapter.activate() │
         │                    │  - /v1/subscribe/approval  │
         │                    └────────────────────────────┘

         ├─── service-b 연결됨 ────────────────┐
         │                                     ▼
         │                    ┌────────────────────────────┐
         │                    │  GenericWebhook.activate() │
         │                    │  - POST /webhooks/sub      │
         │                    └────────────────────────────┘


┌─────────────────────────────────────────────┐
│  SERVICE_SYNC_LOG에 결과 저장                │
│  - 성공/실패 여부                            │
│  - 재시도 필요 시 큐에 등록                   │
└─────────────────────────────────────────────┘

6.2 실패 재처리 전략

typescript
// 재시도 로직
async function retryFailedSync() {
  const failedLogs = await this.db.call('SP_GET_FAILED_SYNC_LOGS');

  for (const log of failedLogs) {
    if (log.retryCount >= log.maxRetry) {
      // 최대 재시도 초과 - 알림 발송
      await this.notifyAdmin(log);
      continue;
    }

    // 지수 백오프로 재시도
    const delay = Math.pow(2, log.retryCount) * 1000; // 1s, 2s, 4s, 8s...
    await this.sleep(delay);

    await this.processSync(log);
  }
}

7. aiagent 연동 상세

7.1 aiagent 현재 구독 API

aiagent-api의 기존 구독 관련 API:

엔드포인트메서드설명
/v1/subscribe/registPOST구독 등록 (신규)
/v1/subscribe/approvalPOST구독 승인 (활성화)
/v1/subscribe/updatePOST구독 수정
/v1/subscribe/listGET구독 목록 조회
/v1/subscribe/infoGET구독 상세 정보

7.2 aiagent 구독 상태값

SUB_STAT:WAIT    - 대기 (결제 전)
SUB_STAT:APROVAL - 승인 (활성)
SUB_STAT:EXPIRE  - 만료

7.3 NextMarket → aiagent 매핑

NextMarket 이벤트aiagent APIaiagent 상태
subscription.activated/v1/subscribe/approvalSUB_STAT:APROVAL
subscription.renewed/v1/subscribe/updateSUB_STAT:APROVAL (기간 연장)
subscription.cancelled(상태 업데이트)SUB_STAT:EXPIRE
subscription.expired(상태 업데이트)SUB_STAT:EXPIRE

7.4 aiagent용 SYNC_CONFIG 예시

json
{
  "planType": "SUBSCRIBE:M01",
  "createRagTenant": true,
  "agentId": "smartstore-message-agent",
  "features": {
    "chat": true,
    "analytics": true,
    "export": false
  }
}

8. 구현 우선순위

Phase 1: 기본 인프라 (1주차)

  • [ ] SERVICE_REGISTRY, PRODUCT_SERVICE_MAPPING 테이블 생성
  • [ ] service-sync 모듈 기본 구조 구현
  • [ ] 범용 웹훅 어댑터 구현

Phase 2: aiagent 연동 (2주차)

  • [ ] aiagent 전용 어댑터 구현
  • [ ] 구독 활성화/해지 테스트
  • [ ] 에러 핸들링 및 로깅

Phase 3: 안정화 (3주차)

  • [ ] 재시도 로직 구현
  • [ ] 모니터링 대시보드
  • [ ] 관리자 알림 시스템

9. 설정 예시

9.1 config.json 추가 항목

json
{
  "serviceSync": {
    "enabled": true,
    "retryMaxCount": 3,
    "retryDelayMs": 1000,
    "timeoutMs": 5000
  },
  "services": {
    "aiagent": {
      "baseUrl": "https://api.aiagent.example.com",
      "apiKey": "${AIAGENT_API_KEY}"
    }
  }
}

9.2 초기 데이터 INSERT

sql
-- 서비스 레지스트리 등록
INSERT INTO SERVICE_REGISTRY (SERVICE_CODE, SERVICE_NAME, BASE_URL, API_KEY, AUTH_TYPE) VALUES
('aiagent', 'AI Agent 서비스', 'https://api.aiagent.example.com', 'sk_xxx', 'BEARER');

-- 상품-서비스 매핑 (상품 ID는 실제 StepPay 상품 ID로 변경)
INSERT INTO PRODUCT_SERVICE_MAPPING (PRODUCT_ID, SERVICE_CODE, SYNC_CONFIG) VALUES
(1001, 'aiagent', '{"planType": "SUBSCRIBE:M01", "createRagTenant": true}');

10. 참고 자료


작성일: 2026년 1월