B2B 파트너 연동 PRD
Background
sports-application은 현재 B2C 사용자(모바일 앱)와 내부 운영자(web /portal, /admin)만을 액터로 갖는다. goods.Product와 ticketing.Event는 ownerId(실제 컬럼은 owner_id) 필드로 소유자 개념을 갖고 있으나, 이 값은 OwnershipGuardImpl이 UserPrincipal.id로 해석하는 User ID 네임스페이스를 그대로 사용한다 — 즉 현재 “소유자”는 로그인한 User 그 자체이며, User와 구분되는 “협력사”라는 별도 신원 타입이 존재하지 않는다. 등록 API(GoodsSellerApiController, EventHostApiController)는 각각 @PreAuthorize("hasRole('GOODS_SELLER')"), @PreAuthorize("hasRole('EVENT_HOST')")로 보호되어 있어, 이 role을 가진 User만 등록할 수 있다.
요구사항은 “B2B 사이드 외부 협력사가 상품 등록·관람 티켓 등록 유즈케이스를 하루 1,000건 이상 지속 발생시킨다”는 부하 시나리오를 요구하며, 이를 검증하려면 먼저 B2B 협력사가 실제로 상품·티켓을 등록할 수 있는 API·인증·권한 체계가 있어야 한다. 이 과제는 신규 Partner 도메인을 개발하되, 상품·티켓 등록의 비즈니스 로직 자체는 기존 goods·ticketing 도메인 서비스(정확히는 기존 CreateMyProductUseCase·CreateMyEventUseCase)를 그대로 경유해 호출한다 — Partner 도메인이 등록 로직을 중복 구현하지 않는다.
이 과제는 [도메인 경계 재설계](../도메인 경계 재설계/PRD.md)에서 확정하는 컨텍스트 맵(코어 도메인이 지원 도메인에게 유즈케이스를 제공하는 방향)을 전제로 한다.
Problem Definition
- 외부 협력사가 상품·티켓을 등록하려면 현재 내부 운영자 전용 웹 포털(
web/app/portal)을 통해서만 가능하다. 협력사 전용 API·인증 수단이 없다. - owner_id 네임스페이스 충돌:
Product·Event의owner_id는 User ID를 가리킨다. Partner를 이 네임스페이스에 어떻게 편입할지(User로 가장하는 계정을 둘지, owner-type 구분 컬럼을 추가할지)가 결정돼 있지 않으면 기존 소유권 검증 로직(OwnershipGuardImpl,requireOwnedBy)이 깨진다. - role 게이트 충돌: 등록 API는
hasRole('GOODS_SELLER'/'EVENT_HOST')로 보호돼 있다. Partner 요청이 이 role 검사를 통과할 경로가 없으면 API Key 인증만으로는 등록이 불가능하다. - 유즈케이스 시그니처 비대칭:
CreateMyProductUseCase는 소유자를OwnershipGuard.authUserId()로 SecurityContext에서 직접 해석하는 반면,CreateMyEventUseCase는CreateMyEventCommand.ownerUserId를 Command 인자로 받는다. “기존 유즈케이스를 그대로 호출한다”는 것이 구체적으로 무엇을 의미하는지(코드 변경 없이 인증 계층에서만 해결할지, Command 생성 지점을 손댈지)가 정의돼 있지 않다. - 협력사의 등록 활동을 감사할 로그 체계가 없다 — 단
mcp도메인에는 이미McpAuditLog(엔티티) +McpAuditLogAsyncRecorder(비동기 기록기) +McpAuditLogDomainService형태의 감사 로그 인프라 패턴이 존재한다. Partner 감사 로그가 이 패턴을 참조할지,mcp테이블을 공유할지 결정이 필요하다. - 이 상태로는 요구사항의 “하루 1,000건 이상 지속 발생” B2B 트래픽을 만들 대상 API 자체가 없다.
Goals / Non-Goals
Goals
Partner(외부 협력사) 엔티티와 API Key 기반 인증을 도입한다.- owner_id 네임스페이스 결정: Partner를 User의 특수 유형으로 편입한다 — Partner 가입 시 로그인 불가능한 “연동 전용 User 계정”을 함께 생성하고, 이 User 계정에 기존
ROLE_GOODS_SELLER/ROLE_EVENT_HOST를 부여한다. 이렇게 하면 별도 owner-type 구분 컬럼을 추가하지 않고 기존owner_id(User ID) 네임스페이스·기존 role 게이트·기존OwnershipGuardImpl을 그대로 재사용할 수 있다. - Partner API Key 인증 필터가 요청을 인증하면, 위에서 만든 연동 전용 User 계정을
SecurityContext의 Authentication으로 채운다. 이후CreateMyProductUseCase(SecurityContext에서 소유자 해석)·CreateMyEventUseCase(Controller가 SecurityContext에서 얻은 id를 Command에 채워 전달)는 코드 변경 없이 그대로 호출된다 — “경유”의 실제 접점은 인증 계층이다. - Partner API Key의 발급·재발급·폐기 라이프사이클을 제공한다.
- Partner별 소유 리소스 접근 권한을 검증한다(타 Partner 리소스 접근 시 거부) — 기존
OwnershipGuardImpl을 그대로 재사용한다. - Partner의 등록·수정 활동에 대한 감사 로그를 남긴다 —
mcp의McpAuditLog패턴(비동기 기록기)을 참조하되, 도메인 분리 원칙에 따라mcp테이블을 공유하지 않고 별도PartnerAuditLog로 구현한다.
Non-Goals
- 정산·수수료 체계 — 본 과제는 등록 유즈케이스에 한정하며, 판매 대금 정산은 범위 밖이다. 향후 별도 과제로 검토한다.
- Partner 전용 관리 UI(가입 신청·승인 화면, API Key 발급 화면, 감사 로그 조회 화면) — 본 과제는 API 전용 연동이며 UI는 제공하지 않는다. API Key 발급·재발급·폐기는 운영자가 기존
web/portal(또는 백엔드 관리 API)을 통해 수행하되, Partner 전용 신규 화면은 만들지 않는다. 필요해지면 별도 과제로 분리한다. - 실시간 협력사 매출 대시보드 — 별도 과제로 검토한다.
User Scenarios
페르소나: 외부 협력사 담당자(체육용품 벤더, 경기·공연 주최사).
- 해피 패스 — 상품 등록: 발급받은 API Key로 인증 헤더를 실어 상품 등록 API를 호출한다 → 인증 필터가 Partner의 연동 전용 User 계정으로 SecurityContext를 채운다 →
CreateMyProductUseCase가 코드 변경 없이 그대로 호출되어 상품이owner_id=Partner 연동 User소속으로 생성된다. - 해피 패스 — 티켓 등록: API Key로 인증해 이벤트 등록 API를 호출한다 →
CreateMyEventUseCase가 코드 변경 없이 그대로 호출되어 이벤트·좌석이 생성된다. - 예외 — 잘못된 요청: 무효한 카테고리 값을 담아 상품 등록을 요청하면 400을 받는다.
- 예외 — 권한 없음: Partner A가 Partner B 소유 상품을 수정하려 하면 404를 받는다(기존
OwnershipGuardImpl.requireOwned가 소유자 불일치 시ResourceNotFoundException을 던져 리소스 존재 자체를 은닉하는 기존 동작을 그대로 재사용 — 코드 변경 없음). - 예외 — 인증 만료: 폐기된 API Key로 요청하면 401을 받고, 재발급 절차 안내를 받는다.
- 예외 — API Key 재발급: 기존 키가 유출 의심될 때 재발급을 요청하면 이전 키는 즉시 무효화되고 신규 키가 발급된다.
- 빈 상태: 신규 가입한 Partner가 등록 이력 조회 API를 호출하면 빈 목록을 받는다.
Benchmarking
| 제품명 | 카테고리 | 참조 패턴 | URL |
|---|---|---|---|
| 쿠팡 마켓플레이스 Open API | 이커머스 오픈마켓 셀러 API | 판매자가 API Key 발급 후 상품을 직접 등록하며, 카테고리·배송지 등 필수 정보를 API 계약으로 강제한다. 판매자(Partner)가 플랫폼 코어 상품 로직을 그대로 사용하는 구조를 참조 | Coupang Open API — 상품 API |
| 카페24 Open API | 쇼핑몰 빌더 파트너 API | 외부 파트너사가 API Key/OAuth로 인증해 쇼핑몰 데이터(상품 등)에 접근하는 표준 파트너 연동 구조를 참조 | 카페24 Open API 안내 |
Functional Requirements
| ID | 요구사항 | 우선순위 |
|---|---|---|
| FR-1 | Partner 엔티티(id, name, apiKeyHash, status(ACTIVE/SUSPENDED), 생성일, 연동 User 계정 id)를 도입한다 | P0 |
| FR-2 | Partner 가입 시 로그인 불가능한 연동 전용 User 계정을 함께 생성하고, 기존 ROLE_GOODS_SELLER·ROLE_EVENT_HOST를 부여한다 | P0 |
| FR-3 | API Key 기반 Partner 인증 필터를 구현한다 — 인증 성공 시 연동 전용 User 계정으로 SecurityContext를 채우고, 유효하지 않거나 폐기된 키는 401을 반환한다 | P0 |
| FR-4 | Partner의 상품 등록 요청은 기존 CreateMyProductUseCase를 코드 변경 없이 그대로 호출한다(인증 계층에서만 소유자가 해석됨) | P0 |
| FR-5 | Partner의 이벤트(관람 티켓) 등록 요청은 기존 CreateMyEventUseCase를 코드 변경 없이 그대로 호출한다(Controller가 SecurityContext에서 얻은 id를 Command에 채운다) | P0 |
| FR-6 | API Key 발급·재발급·폐기 라이프사이클을 제공한다 — 재발급 시 이전 키는 즉시 무효화된다 | P0 |
| FR-7 | Partner는 자기 소유 리소스만 조회·수정할 수 있다(기존 OwnershipGuardImpl.requireOwned 재사용, 코드 변경 없음) — 타 Partner 리소스 접근 시 404를 반환한다(소유자 불일치를 ResourceNotFoundException으로 처리해 리소스 존재를 은닉하는 기존 동작) | P1 |
| FR-8 | Partner의 등록·수정 활동에 대한 감사 로그(PartnerAuditLog: 요청 시각, 요청자, 대상 리소스, 결과 상태)를 mcp의 비동기 기록기 패턴을 참조해 남긴다 | P1 |
| FR-9 | SUSPENDED 상태의 Partner는 등록·수정 요청이 거부된다 | P2 |
Non-Functional Requirements
- 상품·이벤트 등록 API의 P95 응답 시간은 300ms 이내다(동기 등록, DB 쓰기 1건 기준).
- 하루 1,000건 이상의 등록 요청을 지속 처리해도 에러율 1% 미만을 유지한다([상시 트래픽 시뮬레이터](../상시 트래픽 시뮬레이터/PRD.md) B2B 시나리오로 검증).
- API Key 폐기 후 1분 이내 신규 요청이 차단된다.
Operations
- Partner별 등록 성공·실패율을 [옵저버빌리티 스택 도입](../옵저버빌리티 스택 도입/PRD.md) 대시보드에 노출한다.
- Partner 등록 실패율이 급증하면 [지능형 장애 알림](../지능형 장애 알림/PRD.md) 채널로 통지한다(알림 소스 태그:
partner). - 감사 로그 조회는 API 전용으로 제공하며(Non-Goals에 따라 전용 화면은 없음), 필요 시 운영자가 기존
web/portal에서 조회한다(포털 화면 확장 여부는 Open Questions).
Success Metrics
- Partner 등록 API 에러율 1% 미만.
- 모든 등록 요청의 감사 로그 커버리지 100% — 측정 방법: 일정 기간 동안의 등록 API 성공·실패 응답 건수(Access 로그 집계)와
PartnerAuditLog적재 건수를 대조해 누락 0건을 확인한다. - 하루 1,000건 이상 지속 처리 검증 완료([상시 트래픽 시뮬레이터](../상시 트래픽 시뮬레이터/PRD.md) 결과로 확인).
Milestones
- M1: Partner 엔티티·연동 User 계정·API Key 인증(FR-1~FR-3, 선행).
- M2: 상품 등록 경유 연동(FR-4).
- M3: 티켓 등록 경유 연동(FR-5).
- M4: API Key 라이프사이클·권한 검증·감사 로그·SUSPENDED 거부(FR-6~FR-9).
의존: [도메인 경계 재설계](../도메인 경계 재설계/PRD.md)의 컨텍스트 맵(코어 도메인 → 지원 도메인 제공 인터페이스 확정)이 선행돼야 M2·M3의 “경유 호출” 설계가 확정된다. 본 과제 완료 후 [상시 트래픽 시뮬레이터](../상시 트래픽 시뮬레이터/PRD.md)의 B2B 시나리오가 이 API를 대상으로 동작한다.
Open Questions
- 정산·수수료는 범위 밖으로 확정했으나, 향후 별도 과제로 예정할지 결정이 필요하다.
- 운영자가 Partner 가입·API Key 발급을 수행할 최소한의 관리 경로(백엔드 관리 API만 제공할지, 기존
web/portal에 최소 화면을 추가할지)를 TDD 단계에서 결정한다 — Non-Goals에서 확정한 것은 “Partner 전용 신규 UI를 만들지 않는다”는 것이며, 운영자가 아예 수동 개입 없이 Partner를 등록할 수 없다는 뜻은 아니다.
Document History
| 날짜 | 변경 내용 |
|---|---|
| 2026-07-03 | 최초 작성 |
| 2026-07-03 | 재검수 1차 반영: owner_id 네임스페이스 충돌 해소(연동 전용 User 계정 방식으로 결정), role 게이트 통과 경로 명시, 유즈케이스 시그니처 비대칭 해소(코드 불변, 인증 계층에서만 해결), API Key 라이프사이클 FR 추가(FR-6), Non-Goals의 “Partner 전용 UI”를 확정으로 고정하고 모순되던 Open Questions 항목 제거, 감사 로그 커버리지 측정 방법 명시, FR-9(SUSPENDED 거부)를 M4에 배치, AS-IS에 mcp의 McpAuditLog 인프라 존재 반영 |
| 2026-07-03 | 재검수 2차 반영: FR-7·User Scenario 4의 상태코드를 403→404로 정정 — OwnershipGuardImpl.requireOwned가 소유자 불일치 시 ResourceNotFoundException(404)을 던지는 실제 동작에 맞춤(리소스 존재 은닉이라는 기존 보안 패턴 유지, 코드 변경 없음 전제는 그대로 유지) |