# Participation Service 데이터 설계서 ## 📋 데이터 설계 요약 ### 목적 - 이벤트 참여자 관리 및 당첨자 추첨 시스템 지원 - 중복 참여 방지 및 매장 방문 보너스 관리 - 공정한 추첨 이력 관리 ### 설계 범위 - **서비스명**: participation-service - **아키텍처 패턴**: Layered Architecture - **데이터베이스**: PostgreSQL (관계형 데이터) - **캐시**: Redis (참여 세션 정보, 추첨 결과 임시 저장) ### Entity 목록 1. **Participant**: 이벤트 참여자 정보 2. **DrawLog**: 당첨자 추첨 이력 --- ## 1. 데이터베이스 구조 ### 1.1 데이터베이스 정보 - **Database Name**: `participation_db` - **Schema**: public (기본 스키마) - **Character Set**: UTF8 - **Collation**: ko_KR.UTF-8 ### 1.2 테이블 목록 | 테이블명 | 설명 | 주요 특징 | |---------|------|----------| | participants | 이벤트 참여자 | 중복 참여 방지, 보너스 응모권 관리 | | draw_logs | 당첨자 추첨 이력 | 재추첨 방지, 추첨 이력 관리 | --- ## 2. 테이블 상세 설계 ### 2.1 participants (참여자) #### 테이블 설명 - 이벤트 참여자 정보 및 당첨 상태 관리 - 동일 이벤트에 동일 전화번호로 중복 참여 방지 - 매장 방문 시 보너스 응모권 부여 #### 컬럼 정의 | 컬럼명 | 데이터 타입 | NULL | 기본값 | 설명 | |--------|------------|------|--------|------| | id | BIGSERIAL | NOT NULL | AUTO | 내부 식별자 (PK) | | participant_id | VARCHAR(50) | NOT NULL | - | 참여자 고유 ID (형식: EVT{eventId}-{YYYYMMDD}-{SEQ}) | | event_id | VARCHAR(50) | NOT NULL | - | 이벤트 ID (외부 참조) | | name | VARCHAR(100) | NOT NULL | - | 참여자 이름 | | phone_number | VARCHAR(20) | NOT NULL | - | 전화번호 (중복 참여 검증용) | | email | VARCHAR(100) | NULL | - | 이메일 주소 | | channel | VARCHAR(50) | NOT NULL | - | 참여 채널 (WEB, MOBILE, INSTORE) | | store_visited | BOOLEAN | NOT NULL | false | 매장 방문 여부 | | bonus_entries | INTEGER | NOT NULL | 1 | 보너스 응모권 개수 (매장 방문 시 +2) | | agree_marketing | BOOLEAN | NOT NULL | false | 마케팅 수신 동의 | | agree_privacy | BOOLEAN | NOT NULL | true | 개인정보 수집 동의 (필수) | | is_winner | BOOLEAN | NOT NULL | false | 당첨 여부 | | winner_rank | INTEGER | NULL | - | 당첨 순위 (1등, 2등 등) | | won_at | TIMESTAMP | NULL | - | 당첨 일시 | | created_at | TIMESTAMP | NOT NULL | NOW() | 참여 일시 | | updated_at | TIMESTAMP | NOT NULL | NOW() | 수정 일시 | #### 제약 조건 **Primary Key** ```sql PRIMARY KEY (id) ``` **Unique Constraints** ```sql CONSTRAINT uk_participant_id UNIQUE (participant_id) CONSTRAINT uk_event_phone UNIQUE (event_id, phone_number) ``` **Check Constraints** ```sql CONSTRAINT chk_bonus_entries CHECK (bonus_entries >= 1 AND bonus_entries <= 3) CONSTRAINT chk_channel CHECK (channel IN ('WEB', 'MOBILE', 'INSTORE')) CONSTRAINT chk_winner_rank CHECK (winner_rank IS NULL OR winner_rank > 0) ``` #### 인덱스 ```sql -- 이벤트별 참여자 조회 (최신순) CREATE INDEX idx_participants_event_created ON participants(event_id, created_at DESC); -- 이벤트별 당첨자 조회 CREATE INDEX idx_participants_event_winner ON participants(event_id, is_winner, winner_rank); -- 매장 방문자 필터링 CREATE INDEX idx_participants_event_store ON participants(event_id, store_visited); -- 전화번호 중복 검증 (복합 유니크 인덱스로 커버) -- uk_event_phone 인덱스 활용 ``` --- ### 2.2 draw_logs (당첨자 추첨 이력) #### 테이블 설명 - 당첨자 추첨 이력 기록 - 재추첨 방지 및 감사 추적 - 추첨 알고리즘 및 설정 정보 저장 #### 컬럼 정의 | 컬럼명 | 데이터 타입 | NULL | 기본값 | 설명 | |--------|------------|------|--------|------| | id | BIGSERIAL | NOT NULL | AUTO | 내부 식별자 (PK) | | event_id | VARCHAR(50) | NOT NULL | - | 이벤트 ID (외부 참조) | | total_participants | INTEGER | NOT NULL | - | 총 참여자 수 | | winner_count | INTEGER | NOT NULL | - | 당첨자 수 | | apply_store_visit_bonus | BOOLEAN | NOT NULL | false | 매장 방문 보너스 적용 여부 | | algorithm | VARCHAR(50) | NOT NULL | 'RANDOM' | 추첨 알고리즘 (RANDOM, WEIGHTED) | | drawn_at | TIMESTAMP | NOT NULL | NOW() | 추첨 실행 일시 | | drawn_by | VARCHAR(100) | NOT NULL | 'SYSTEM' | 추첨 실행자 | | created_at | TIMESTAMP | NOT NULL | NOW() | 생성 일시 | | updated_at | TIMESTAMP | NOT NULL | NOW() | 수정 일시 | #### 제약 조건 **Primary Key** ```sql PRIMARY KEY (id) ``` **Unique Constraints** ```sql CONSTRAINT uk_draw_event UNIQUE (event_id) ``` **Check Constraints** ```sql CONSTRAINT chk_winner_count CHECK (winner_count > 0) CONSTRAINT chk_total_participants CHECK (total_participants >= winner_count) CONSTRAINT chk_algorithm CHECK (algorithm IN ('RANDOM', 'WEIGHTED')) ``` #### 인덱스 ```sql -- 이벤트별 추첨 이력 조회 CREATE INDEX idx_draw_logs_event ON draw_logs(event_id); -- 추첨 일시별 조회 CREATE INDEX idx_draw_logs_drawn_at ON draw_logs(drawn_at DESC); ``` --- ## 3. 캐시 설계 (Redis) ### 3.1 캐시 키 구조 #### 참여 세션 정보 - **Key**: `participation:session:{eventId}:{phoneNumber}` - **Type**: String (JSON) - **TTL**: 10분 - **용도**: 중복 참여 방지, 빠른 검증 - **데이터**: ```json { "participantId": "EVT123-20251029-001", "eventId": "EVT123", "phoneNumber": "010-1234-5678", "participatedAt": "2025-10-29T10:00:00" } ``` #### 추첨 결과 임시 저장 - **Key**: `participation:draw:{eventId}` - **Type**: String (JSON) - **TTL**: 1시간 - **용도**: 추첨 결과 임시 캐싱, 빠른 조회 - **데이터**: ```json { "eventId": "EVT123", "winners": [ { "participantId": "EVT123-20251029-001", "rank": 1, "name": "홍길동", "phoneNumber": "010-****-5678" } ], "drawnAt": "2025-10-29T15:00:00" } ``` #### 이벤트별 참여자 카운트 - **Key**: `participation:count:{eventId}` - **Type**: String (숫자) - **TTL**: 5분 - **용도**: 빠른 참여자 수 조회 - **데이터**: "123" (참여자 수) ### 3.2 캐시 전략 #### 캐시 갱신 정책 - **Write-Through**: 참여 등록 시 DB 저장 후 캐시 갱신 - **Cache-Aside**: 조회 시 캐시 미스 시 DB 조회 후 캐시 저장 #### 캐시 무효화 - **이벤트 종료 시**: `participation:*:{eventId}` 패턴 삭제 - **추첨 완료 시**: `participation:count:{eventId}` 삭제 (재조회 유도) --- ## 4. 데이터 무결성 설계 ### 4.1 중복 참여 방지 #### 1차 검증: Redis 캐시 ``` 1. 참여 요청 수신 2. Redis 키 확인: participation:session:{eventId}:{phoneNumber} 3. 키 존재 시 → DuplicateParticipationException 발생 4. 키 미존재 시 → 2차 검증 진행 ``` #### 2차 검증: PostgreSQL 유니크 제약 ``` 1. DB 삽입 시도 2. uk_event_phone 제약 위반 시 → DuplicateParticipationException 발생 3. 정상 삽입 시 → Redis 캐시 생성 (TTL: 10분) ``` ### 4.2 재추첨 방지 #### 추첨 이력 검증 ```sql -- 추첨 전 검증 쿼리 SELECT COUNT(*) FROM draw_logs WHERE event_id = ?; -- 결과 > 0 → AlreadyDrawnException 발생 ``` #### 유니크 제약 ```sql -- uk_draw_event: 이벤트당 1회만 추첨 가능 CONSTRAINT uk_draw_event UNIQUE (event_id) ``` ### 4.3 당첨자 수 검증 #### 최소 참여자 수 검증 ```sql -- 추첨 전 참여자 수 확인 SELECT COUNT(*) FROM participants WHERE event_id = ? AND is_winner = false; -- 참여자 수 < 당첨자 수 → InsufficientParticipantsException 발생 ``` --- ## 5. 성능 최적화 ### 5.1 인덱스 전략 #### 쿼리 패턴별 인덱스 1. **참여자 목록 조회** (페이징, 최신순) - 인덱스: `idx_participants_event_created` - 커버: `(event_id, created_at DESC)` 2. **당첨자 목록 조회** (순위 오름차순) - 인덱스: `idx_participants_event_winner` - 커버: `(event_id, is_winner, winner_rank)` 3. **매장 방문자 필터링** - 인덱스: `idx_participants_event_store` - 커버: `(event_id, store_visited)` ### 5.2 캐시 활용 #### 읽기 성능 최적화 - **참여자 수 조회**: Redis 캐시 우선 (TTL: 5분) - **추첨 결과 조회**: Redis 캐시 (TTL: 1시간) - **중복 참여 검증**: Redis 캐시 (TTL: 10분) #### 캐시 히트율 목표 - **중복 참여 검증**: 95% 이상 - **추첨 결과 조회**: 90% 이상 - **참여자 수 조회**: 85% 이상 --- ## 6. 보안 고려사항 ### 6.1 개인정보 보호 #### 전화번호 마스킹 - **저장**: 원본 저장 (중복 검증용) - **조회**: 마스킹 처리 (010-****-5678) - **로그**: 마스킹 처리 (감사 추적용) #### 이메일 마스킹 - **저장**: 원본 저장 - **조회**: 마스킹 처리 (hong***@example.com) ### 6.2 데이터 암호화 #### 저장 시 암호화 (향후 적용 권장) - **민감 정보**: 전화번호, 이메일 - **암호화 알고리즘**: AES-256 - **키 관리**: AWS KMS 또는 HashiCorp Vault --- ## 7. 백업 및 복구 ### 7.1 백업 정책 - **Full Backup**: 매일 02:00 (KST) - **Incremental Backup**: 6시간마다 - **보관 기간**: 30일 ### 7.2 복구 목표 - **RPO (Recovery Point Objective)**: 6시간 이내 - **RTO (Recovery Time Objective)**: 1시간 이내 --- ## 8. 모니터링 지표 ### 8.1 성능 지표 - **참여 등록 응답 시간**: 평균 < 200ms - **당첨자 조회 응답 시간**: 평균 < 100ms - **캐시 히트율**: > 85% ### 8.2 비즈니스 지표 - **총 참여자 수**: 이벤트별 실시간 집계 - **매장 방문자 비율**: 보너스 응모권 적용률 - **중복 참여 시도 횟수**: 비정상 접근 탐지 --- ## 9. 데이터 마이그레이션 전략 ### 9.1 초기 데이터 로드 - **참조 데이터**: 없음 (참여자 데이터는 실시간 생성) - **테스트 데이터**: 샘플 참여자 100명, 추첨 이력 10건 ### 9.2 데이터 정합성 검증 ```sql -- 중복 참여자 확인 SELECT event_id, phone_number, COUNT(*) FROM participants GROUP BY event_id, phone_number HAVING COUNT(*) > 1; -- 당첨자 순위 중복 확인 SELECT event_id, winner_rank, COUNT(*) FROM participants WHERE is_winner = true GROUP BY event_id, winner_rank HAVING COUNT(*) > 1; -- 추첨 이력 정합성 확인 SELECT d.event_id, d.winner_count, COUNT(p.id) as actual_winners FROM draw_logs d LEFT JOIN participants p ON d.event_id = p.event_id AND p.is_winner = true GROUP BY d.event_id, d.winner_count HAVING d.winner_count != COUNT(p.id); ``` --- ## 10. 참조 및 의존성 ### 10.1 외부 서비스 참조 - **event-id**: Event Service에서 생성한 이벤트 ID 참조 (캐시 기반) - **user-id**: User Service의 사용자 ID 참조 없음 (비회원 참여 가능) ### 10.2 이벤트 발행 - **Topic**: `participant-registered` - **Event**: `ParticipantRegisteredEvent` - **Consumer**: Analytics Service --- **설계자**: Backend Developer (최수연 "아키텍처") **설계일**: 2025-10-29 **문서 버전**: v1.0