diff --git a/.specify/feature.json b/.specify/feature.json index 17fa4468..ddeb6e6e 100644 --- a/.specify/feature.json +++ b/.specify/feature.json @@ -1,3 +1,3 @@ { - "feature_directory": "specs/001-feed-features" + "feature_directory": "specs/002-follow-domain" } diff --git a/specs/002-follow-domain/checklists/requirements.md b/specs/002-follow-domain/checklists/requirements.md new file mode 100644 index 00000000..31c1ddb2 --- /dev/null +++ b/specs/002-follow-domain/checklists/requirements.md @@ -0,0 +1,34 @@ +# Specification Quality Checklist: 팔로우(Follow) 기능 + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-05-17 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) — 비관적 락/Retry/JPA 등 구현 용어 본문 미포함, 비즈니스 어휘로 기술 +- [x] Focused on user value and business needs — 우선순위(P1~P3)로 사용자 가치 정렬 +- [x] Written for non-technical stakeholders — 한국어 비기술자 친화 서술 +- [x] All mandatory sections completed — User Scenarios / Requirements / Success Criteria 모두 작성 + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain — 1개 마커 해소 (Q1: 재시도 소진 시 "잠시 후 다시 시도" 일반 안내, 자원 경합 원인 비노출) +- [x] Requirements are testable and unambiguous — 모든 FR이 관찰 가능한 결과로 기술 +- [x] Success criteria are measurable — SC 7건 모두 측정 가능 +- [x] Success criteria are technology-agnostic — 응답시간/RPS 등 기술 임계치 배제 +- [x] All acceptance scenarios are defined — 5개 User Story 모두 Given-When-Then 보유 +- [x] Edge cases are identified — Edge Cases 섹션 8건 +- [x] Scope is clearly bounded — 차단/비공개 계정, 알림 도메인, 사용자 라이프사이클 등 명시적 범위 외 처리 +- [x] Dependencies and assumptions identified — Assumptions 섹션 8건 + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria — FR-001~FR-016가 User Story 시나리오와 매핑 +- [x] User scenarios cover primary flows — 토글/조회/알림/동시성 모두 커버 +- [x] Feature meets measurable outcomes defined in Success Criteria — SC가 P1~P3 시나리오와 정렬 +- [x] No implementation details leak into specification — 구현 디테일은 Assumptions에서 외부화 + +## Notes + +- 2026-05-17 1차 검증: 마커 해소, 모든 항목 통과. `/speckit-clarify`(선택) 또는 `/speckit-plan`으로 진행 가능. diff --git a/specs/002-follow-domain/spec.md b/specs/002-follow-domain/spec.md new file mode 100644 index 00000000..f8e61dc8 --- /dev/null +++ b/specs/002-follow-domain/spec.md @@ -0,0 +1,177 @@ +# Feature Specification: 팔로우(Follow) 기능 + +**Feature Branch**: `002-follow-domain` + +**Created**: 2026-05-17 + +**Status**: Reviewed (clarifications resolved 2026-05-17) + +**Input**: User description: "follow 관련 기능" + +> 본 문서는 신규 기능 정의가 아닌, 이미 운영 중인 THIP 서비스의 "팔로우" 도메인을 사용자 관점에서 역설계해 정형화한 PRD다. 구현 상세(스택·아키텍처·재시도 메커니즘 등)는 의도적으로 배제하고, 사용자가 무엇을(WHAT) 왜(WHY) 할 수 있어야 하는지에 집중한다. + +## User Scenarios & Testing *(mandatory)* + +THIP은 같은 책을 읽는 사람들이 감상을 공유하는 독서 커뮤니티이며, **팔로우**는 사용자가 관심 있는 다른 사용자의 활동을 자신의 피드 흐름에 포함시키기 위해 사용하는 *비대칭 관계*다. 아래 사용자 스토리는 우선순위(P1~P3)로 정렬되어 있으며, 각각 독립적으로 검증·배포 가능한 슬라이스다. + +### User Story 1 - 다른 사용자를 팔로우/언팔로우하기 (Priority: P1) + +사용자는 다른 사용자의 프로필 또는 피드에서 팔로우/언팔로우 상태를 토글할 수 있다. 동작은 즉시 반영되며 동일 대상에 대한 빠른 반복 토글에서도 카운트가 정합해야 한다. + +**Why this priority**: 팔로우 관계가 없으면 피드 도메인의 "팔로잉 우선 노출 모드"가 의미를 잃는다. 본 시나리오는 커뮤니티 형성의 1차 트리거이며, 모든 후속 시나리오의 전제 조건이다. + +**Independent Test**: 두 사용자 A, B가 존재할 때 A가 B를 팔로우/언팔로우 상태로 토글했을 때 (1) A의 팔로잉 목록, (2) B의 팔로워 목록, (3) A→B 팔로잉 여부 확인, (4) B의 팔로워 수가 모두 일관된 결과를 보인다. + +**Acceptance Scenarios**: + +1. **Given** A가 B를 팔로우하지 않은 상태에서, **When** A가 B에 대한 팔로우 요청을 보내면, **Then** A→B 팔로잉 관계가 생성되고 B의 팔로워 수가 1 증가한다. +2. **Given** A가 B를 이미 팔로우하고 있을 때, **When** A가 B에 대한 언팔로우 요청을 보내면, **Then** A→B 팔로잉 관계가 해제되고 B의 팔로워 수가 1 감소한다. +3. **Given** A가 B를 이미 팔로우하고 있을 때, **When** A가 B에 대해 다시 팔로우 요청을 보내면, **Then** 작업은 "이미 팔로우 중" 오류로 거부되고 카운트는 변하지 않는다. +4. **Given** A가 B를 팔로우하지 않을 때, **When** A가 B에 대한 언팔로우 요청을 보내면, **Then** 작업은 "이미 언팔로우 상태" 오류로 거부되고 카운트는 변하지 않는다. +5. **Given** 사용자 A는 자기 자신에 대한 팔로우 또는 언팔로우 요청을 보낸다, **When** 어느 경우든, **Then** 작업이 거부된다. +6. **Given** 다수의 사용자가 동시에 B를 팔로우/언팔로우 한다, **When** 모든 요청이 처리된 뒤, **Then** B의 최종 팔로워 수는 실제 활성 팔로잉 행 수와 정확히 일치한다(중복 누계·누락 없음). + +--- + +### User Story 2 - 팔로우 관계 둘러보기 (Priority: P1) + +사용자는 자신의 팔로잉 목록과 임의 사용자의 팔로워 목록을 조회하고, 특정 사용자에 대한 자신의 팔로잉 여부를 즉시 확인할 수 있다. + +**Why this priority**: 토글 행동만 가능하고 결과 가시화가 없으면 사용자는 자신의 관계 상태를 신뢰할 수 없다. 가시화는 토글과 함께 출시되어야 의미가 있다. + +**Independent Test**: A가 사용자 B, C를 팔로우한 상태에서 (1) A의 팔로잉 목록에 B와 C가, (2) B와 C의 팔로워 목록에 A가, (3) A→B is-following 확인이 true로 반환된다. + +**Acceptance Scenarios**: + +1. **Given** A가 다수의 사용자를 팔로우하고 있을 때, **When** A가 자신의 팔로잉 목록을 요청하면, **Then** 활성 팔로잉 대상의 프로필 요약 목록이 반환된다. +2. **Given** B가 다수의 팔로워를 가진 상태에서, **When** 임의 사용자가 B의 팔로워 목록을 요청하면, **Then** B를 팔로우 중인 사용자들의 프로필 요약 목록이 반환된다. +3. **Given** A가 B를 팔로우 중이거나 아닌 상태에서, **When** A가 B에 대한 팔로잉 여부 확인을 요청하면, **Then** 시스템은 현재 관계 상태를 단일 값(true/false)으로 반환한다. + +--- + +### User Story 3 - 내가 팔로우한 사람들의 최근 활동 확인 (Priority: P2) + +사용자는 자신이 팔로우한 사용자들의 최근 피드만을 빠르게 확인해 "지금 무슨 이야기를 하는지" 알 수 있다. + +**Why this priority**: 팔로우의 가치는 결국 피드 노출로 이어진다. 별도 진입 경로(예: 홈 상단 위젯, 팔로잉 전용 탭)에서 즉시 확인 가능해야 발견 효율이 의미를 가진다. + +**Independent Test**: A가 사용자 B와 C를 팔로우한 상태에서 B와 C가 새 피드를 작성한 뒤, A가 "내 팔로잉의 최근 피드" 화면을 열면 B와 C의 피드가 최근순으로 함께 보인다(공개 피드 한정). + +**Acceptance Scenarios**: + +1. **Given** A가 다수의 사용자를 팔로우 중이고 그들 중 일부가 최근 공개 피드를 작성했을 때, **When** A가 "팔로잉 최근 활동" 화면을 요청하면, **Then** 활성 팔로잉 대상의 최근 공개 피드 묶음이 반환된다. +2. **Given** A가 팔로우 중인 사용자가 비공개 피드를 작성했을 때, **When** A가 동일 화면을 요청하면, **Then** 해당 비공개 피드는 결과에서 제외된다(공개 정책은 피드 PRD를 따름). +3. **Given** A가 누구도 팔로우하지 않을 때, **When** A가 동일 화면을 요청하면, **Then** 빈 결과 또는 안내가 일관되게 반환된다. + +--- + +### User Story 4 - 팔로우 시 상대에게 알림이 전달되는 경험 (Priority: P2) + +사용자가 팔로우될 때 상대 사용자는 그 사실을 알 수 있어야 한다(시스템 알림 메커니즘을 통해). 본 PRD는 알림 도메인의 상세를 정의하지 않으며, "팔로우 행위가 알림 발화 트리거가 된다"는 사실만 보증한다. + +**Why this priority**: 팔로우의 사회적 효과(상호 인지·맞팔)는 알림 발화 여부에 크게 좌우된다. 알림은 별도 도메인이지만, 트리거 발화 누락은 본 도메인 책임이다. + +**Independent Test**: B가 사용자 A에 의해 팔로우될 때 알림 시스템에 발화 이벤트가 정확히 1회 도달한다(언팔로우 시 알림 미발화). + +**Acceptance Scenarios**: + +1. **Given** A가 B를 처음 팔로우할 때, **When** 팔로우 요청이 성공하면, **Then** 알림 도메인에 "팔로우됨" 트리거가 정확히 1회 발화된다. +2. **Given** A가 이전에 B를 팔로우/언팔로우한 이력이 있고 다시 팔로우할 때, **When** 새 팔로우 요청이 성공하면, **Then** 알림 트리거가 다시 1회 발화된다. +3. **Given** A가 B에게 언팔로우 요청을 할 때, **When** 요청이 성공하면, **Then** 알림 트리거는 발화되지 않는다. + +--- + +### User Story 5 - 동시 토글 및 일시적 장애 상황에서의 일관된 사용자 경험 (Priority: P3) + +사용자가 빠르게 같은 대상을 반복 토글하거나, 시스템 일시적 부하로 1차 처리가 실패한 경우에도 사용자는 "최종 상태가 무엇인지" 명확히 알 수 있어야 한다. + +**Why this priority**: 평상시에는 P1/P2로 충분하지만, 트래픽 피크 또는 사용자가 빠르게 탭하는 상황에서 카운트 깨짐·중복 알림은 신뢰 손상으로 직결된다. + +**Independent Test**: 동일 사용자가 짧은 시간 안에 같은 대상에 대해 N번 토글했을 때 최종 관계 상태가 마지막 사용자 의도와 일치하며, 팔로워 수가 정합하다. + +**Acceptance Scenarios**: + +1. **Given** A가 짧은 시간 안에 B에 대해 팔로우 → 언팔로우 → 팔로우를 빠르게 요청했을 때, **When** 모든 요청이 처리된 후, **Then** 최종 관계 상태는 "팔로우", 팔로워 수는 1번의 팔로우만 반영된다(중복 증가 0건). +2. **Given** 시스템이 일시적 자원 경합 상태로 1차 처리에 실패하더라도, **When** 사용자에게 응답이 돌아왔을 때, **Then** 응답에는 "성공" 또는 "사용자가 다시 시도해야 함을 명확히 안내하는 오류" 중 하나만 포함되며, "성공으로 보이지만 실제로는 처리 안 됨" 상태는 발생하지 않는다. +3. **Given** 동시 토글로 인해 카운트 갱신이 직렬화될 때, **When** 모든 요청이 처리된 뒤, **Then** B의 팔로워 수는 활성 팔로잉 관계 수와 정확히 일치한다. + +**재시도 한계 초과 시 사용자 경험 (확정)**: + +시스템은 일시적 경합을 흡수하기 위해 내부 자동 재시도를 수행한다. 모든 자동 재시도가 소진되어도 처리에 실패한 경우, 사용자에게는 **"잠시 후 다시 시도해주세요" 형태의 일반 안내**만 노출된다. 자원 경합 같은 시스템 내부 원인은 사용자에게 노출하지 않는다. 운영자와 개발자는 내부 오류 코드·로그를 통해 원인을 식별한다. + +--- + +### Edge Cases + +- **자기 자신 팔로우 시도**: 시스템은 작업을 거부한다. +- **이미 팔로우 중인 대상에 대한 중복 팔로우**: 작업은 거부되며 카운트는 변하지 않는다. +- **존재하지 않는 팔로우 관계에 대한 언팔로우**: 작업은 거부되며 카운트는 변하지 않는다. +- **동시 팔로우 토글**: 동일 (요청자, 대상) 쌍에 대해 중복 관계 행이 절대 생성되지 않는다(데이터베이스 차원의 유일성 보장 포함). +- **카운트 무결성**: 어떤 동시성·재시도 시나리오에서도 팔로워 수가 실제 활성 관계 수와 일치한다. +- **재시도 후 최종 실패**: 시스템이 내부 재시도 한계를 모두 소진해도 처리에 실패하면 사용자에게는 "잠시 후 다시 시도해주세요" 형태의 일반 안내가 반환되며 자원 경합 원인은 비노출된다. 부분 상태(관계는 생성됐는데 카운트는 안 올라간 상태 등)는 절대 남지 않는다. +- **자체 알림 발화 방지**: 자기 자신이 자신에게 알림을 보내지 않는다(자기 팔로우가 거부되므로 자연 충족). +- **삭제·탈퇴 사용자**: 어느 한쪽이 탈퇴한 사용자라면 팔로잉/팔로워 목록에 그가 노출되지 않거나 명확히 안내된다(상세 정책은 사용자 라이프사이클 도메인을 따름). + +## Requirements *(mandatory)* + +### Functional Requirements + +#### 팔로우 토글 + +- **FR-001**: 사용자는 임의의 다른 사용자를 대상으로 팔로우 또는 언팔로우 상태를 토글할 수 있어야 한다. +- **FR-002**: 시스템은 사용자 본인을 대상으로 한 팔로우/언팔로우 요청을 거부해야 한다. +- **FR-003**: 시스템은 이미 팔로우 중인 사용자에 대한 중복 팔로우 요청을 거부하고, 팔로우 관계가 없는 사용자에 대한 언팔로우 요청을 거부해야 한다. +- **FR-004**: 시스템은 동일 (요청자, 대상) 쌍에 대해 중복된 활성 팔로잉 관계가 절대 존재하지 않도록 보장해야 한다(데이터 차원의 유일성). + +#### 카운트와 동시성 + +- **FR-005**: 팔로우/언팔로우의 결과로 대상 사용자의 팔로워 수가 정확히 1씩 증가/감소해야 한다. +- **FR-006**: 동시 다중 요청, 빠른 반복 토글 시에도 대상 사용자의 팔로워 수는 활성 팔로잉 관계 수와 항상 일치해야 한다(누락·중복 증가 0). +- **FR-007**: 어떤 경우에도 팔로워 수가 음수가 되어서는 안 된다. +- **FR-008**: 일시적 자원 경합으로 1차 처리에 실패한 경우 시스템은 자동 재시도로 흡수를 시도한다. 모든 자동 재시도가 실패한 경우 사용자에게는 자원 경합 원인을 비노출한 채 "잠시 후 다시 시도해주세요" 형태의 일반 안내만 반환한다. 부분 상태(관계 생성 ↔ 카운트 미반영)는 발생하지 않아야 한다. 내부 원인 식별을 위한 시스템 차원의 오류 코드·로그는 별도로 유지한다. + +#### 조회 + +- **FR-009**: 사용자는 임의 사용자의 팔로워 목록을 조회할 수 있어야 한다. +- **FR-010**: 사용자는 자신의 팔로잉 목록을 조회할 수 있어야 한다. +- **FR-011**: 사용자는 특정 대상 사용자에 대한 자신의 팔로잉 여부(true/false)를 단일 호출로 확인할 수 있어야 한다. +- **FR-012**: 사용자는 자신이 팔로우한 사용자들의 최근 공개 피드 묶음을 단일 진입점에서 확인할 수 있어야 한다(공개 정책은 피드 PRD를 따름). + +#### 알림 연동 + +- **FR-013**: 팔로우 요청이 성공한 경우, 시스템은 알림 도메인에 "팔로우됨" 트리거를 정확히 1회 발화해야 한다. +- **FR-014**: 언팔로우 요청이 성공한 경우, 시스템은 알림 트리거를 발화하지 않아야 한다. +- **FR-015**: 알림 트리거의 메시지 내용·전달 방식·수신자 설정 우선순위는 본 PRD의 범위가 아니며 알림 도메인 PRD를 따른다. + +#### 사용자 라이프사이클 연동 + +- **FR-016**: 탈퇴·삭제된 사용자는 다른 사용자의 팔로잉/팔로워 목록 결과에서 노출되지 않거나, 노출되더라도 라이프사이클 상태를 명확히 안내해야 한다(상세 정책은 사용자 라이프사이클 도메인 PRD를 따름). + +### Key Entities + +- **Following (팔로잉 관계)**: "사용자 A가 사용자 B를 팔로우 중"이라는 *방향 있는* 관계. (요청자, 대상) 쌍에 대해 활성 관계가 최대 1건 존재한다. +- **User (사용자)**: 팔로잉 관계의 주체이자 대상. 본 PRD는 사용자가 보유한 *팔로워 수* 속성만을 의미 있게 다룬다(상세 사용자 정의는 본 PRD 범위 외). +- **Follow Notification Trigger (팔로우 알림 트리거)**: 팔로우 성공의 사후 효과로 발화되는 추상 이벤트. 본 PRD는 발화 조건만 정의하며, 알림 자체는 알림 도메인 PRD가 다룬다. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: 사용자가 임의 대상의 팔로우 버튼을 눌렀을 때, 사용자가 "지연 없이 반영됐다"고 인식하는 시간 내에 변경된 관계 상태(true/false)와 즉시 일치하는 화면 갱신을 본다. +- **SC-002**: 동일 사용자가 같은 대상에 대해 짧은 시간 안에 토글을 N회 반복했을 때 최종 카운트 차이는 마지막 사용자 의도와 정확히 일치한다(중복 증가·누락 0건). +- **SC-003**: 임의 사용자의 팔로워 수와 "그를 팔로우 중인 사용자 목록의 크기"가 항상 일치한다(불일치 사건 0건). +- **SC-004**: 동시 N건의 팔로우/언팔로우 요청 부하 상황에서 모든 요청은 (성공 / 명확한 오류) 중 하나로 종결되며 "성공 응답을 받았는데 실제로는 처리되지 않은" 상태는 발생하지 않는다. +- **SC-005**: 팔로우 성공 1건당 알림 도메인에 도달하는 "팔로우됨" 트리거는 정확히 1건이다(누락·중복 0건). +- **SC-006**: 사용자가 "내 팔로잉의 최근 피드" 화면을 열었을 때, 자신이 팔로우 중인 사용자들의 공개 피드만 결과에 포함되어 있음을 신뢰할 수 있다(비공개 피드 노출 0건). +- **SC-007**: 시스템이 일시적 경합으로 1차 실패했을 때, 자동 재시도로 흡수되어 사용자에게는 정상 응답으로 보이는 비율이 일정 수준 이상(운영 기준치)이다. 자동 재시도가 모두 실패한 경우에도 사용자에게는 항상 정확한 결과(성공 또는 명시적 오류)가 전달된다. + +## Assumptions + +- **이미 운영 중인 기능의 역설계**: 본 PRD는 신규 기능 정의가 아니라 기존 구현을 사용자 관점으로 정형화한 산출물이다. 요구사항은 "구현이 보장해야 한다(혹은 보장하고 있어야 한다)"의 형태로 읽힌다. +- **인증 전제**: 모든 팔로우 시나리오는 인증된 사용자를 전제로 한다. 인증/인가의 상세 동작은 별도 PRD가 다룬다. +- **사용자 라이프사이클 외부 의존**: 탈퇴·삭제·차단된 사용자에 대한 노출 정책은 별도 사용자 라이프사이클/계정 정책 도메인 PRD가 정의한다. 본 PRD는 그 결과만을 인정한다. +- **차단(Block)·비공개 계정(Private Account)은 범위 외**: 현재 THIP에는 명시적인 차단·비공개 계정 기능이 없다. 향후 도입 시 본 PRD는 영향을 받는다(별도 개정 트리거). +- **알림(Notification)은 별도 도메인**: 본 PRD는 "팔로우 성공이 알림 트리거를 발화한다"는 사실만 보증한다. 트리거의 메시지·푸시 채널·수신자 설정은 알림 도메인 PRD가 정의한다. +- **피드(Feed)와의 연동**: 본 PRD가 다루는 "팔로잉 최근 피드" 화면은 피드 도메인의 공개 정책을 그대로 따른다. 본 PRD는 노출 규칙을 중복 정의하지 않는다. +- **카운트 정합성**: 본 PRD는 *사용자가 관찰 가능한 일관성*만 정의한다. 일관성을 달성하기 위한 구체 메커니즘(락 전략, 재시도 정책, 인덱스/제약)은 구현 영역이며 본 PRD의 범위가 아니다. +- **목록 페이지네이션**: 팔로워/팔로잉 목록의 페이지네이션 정책(커서 사용 여부, 페이지 크기)은 본 PRD가 단정하지 않는다. 다른 목록 도메인(피드 등)과의 일관성을 따른다.