✨ 주요 기능 - Azure 기반 물리아키텍처 설계 (개발환경/운영환경) - 7개 마이크로서비스 물리 구조 설계 - 네트워크 아키텍처 다이어그램 작성 (Mermaid) - 환경별 비교 분석 및 마스터 인덱스 문서 📁 생성 파일 - design/backend/physical/physical-architecture.md (마스터) - design/backend/physical/physical-architecture-dev.md (개발환경) - design/backend/physical/physical-architecture-prod.md (운영환경) - design/backend/physical/*.mmd (4개 Mermaid 다이어그램) 🎯 핵심 성과 - 비용 최적화: 개발환경 월 $143, 운영환경 월 $2,860 - 확장성: 개발환경 100명 → 운영환경 10,000명 (100배) - 가용성: 개발환경 95% → 운영환경 99.9% - 보안: 다층 보안 아키텍처 (L1~L4) 🛠️ 기술 스택 - Azure Kubernetes Service (AKS) - Azure Database for PostgreSQL Flexible - Azure Cache for Redis Premium - Azure Service Bus Premium - Application Gateway + WAF 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
393 lines
11 KiB
Markdown
393 lines
11 KiB
Markdown
# 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
|