# Analytics Service 데이터베이스 설계서

## 데이터설계 요약

### 설계 개요
- **서비스명**: Analytics Service
- **데이터베이스**: PostgreSQL 16.x (시계열 최적화)
- **캐시 DB**: Redis 7.x (분석 결과 캐싱)
- **아키텍처 패턴**: Layered Architecture
- **데이터 특성**: 시계열 분석 데이터, 실시간 집계 데이터

### 테이블 구성
| 테이블명 | 설명 | Entity 매핑 | 특징 |
|---------|------|-------------|------|
| event_stats | 이벤트별 통계 | EventStats | 집계 데이터, userId 인덱스 |
| channel_stats | 채널별 성과 | ChannelStats | 외부 API 연동 데이터 |
| timeline_data | 시계열 분석 | TimelineData | 시간 순서 데이터, 시계열 인덱스 |

### Redis 캐시 구조
| 키 패턴 | 설명 | TTL |
|--------|------|-----|
| `analytics:dashboard:{eventId}` | 대시보드 데이터 | 1시간 |
| `analytics:channel:{eventId}:{channelName}` | 채널별 분석 | 1시간 |
| `analytics:roi:{eventId}` | ROI 분석 | 1시간 |
| `analytics:timeline:{eventId}:{granularity}` | 타임라인 데이터 | 1시간 |
| `analytics:user:{userId}` | 사용자 통합 분석 | 1시간 |
| `analytics:processed:{messageId}` | 멱등성 처리 (Set) | 24시간 |

### 데이터 접근 패턴
1. **이벤트별 조회**: eventId 기반 빠른 조회 (B-Tree 인덱스)
2. **사용자별 조회**: userId 기반 다중 이벤트 조회
3. **시계열 조회**: timestamp 범위 검색 (BRIN 인덱스)
4. **채널별 조회**: eventId + channelName 복합 인덱스

---

## 1. 테이블 상세 설계

### 1.1 event_stats (이벤트 통계)

#### 테이블 설명
- **목적**: 이벤트별 통계 집계 데이터 관리
- **데이터 특성**: 실시간 업데이트, Kafka Consumer를 통한 증분 업데이트
- **조회 패턴**: eventId 단건 조회, userId 기반 목록 조회

#### 컬럼 정의
| 컬럼명 | 데이터 타입 | Null | 기본값 | 설명 |
|--------|------------|------|-------|------|
| id | BIGSERIAL | NOT NULL | AUTO | 기본 키 |
| event_id | VARCHAR(36) | NOT NULL | - | 이벤트 ID (UUID) |
| event_title | VARCHAR(255) | NOT NULL | - | 이벤트 제목 |
| user_id | VARCHAR(36) | NOT NULL | - | 사용자 ID (UUID) |
| total_participants | INTEGER | NOT NULL | 0 | 총 참여자 수 |
| total_views | INTEGER | NOT NULL | 0 | 총 조회 수 |
| estimated_roi | DECIMAL(10,2) | NOT NULL | 0.00 | 예상 ROI (%) |
| target_roi | DECIMAL(10,2) | NOT NULL | 0.00 | 목표 ROI (%) |
| sales_growth_rate | DECIMAL(10,2) | NOT NULL | 0.00 | 매출 성장률 (%) |
| total_investment | DECIMAL(15,2) | NOT NULL | 0.00 | 총 투자 금액 (원) |
| expected_revenue | DECIMAL(15,2) | NOT NULL | 0.00 | 예상 수익 (원) |
| status | VARCHAR(20) | NOT NULL | 'ACTIVE' | 이벤트 상태 |
| created_at | TIMESTAMP | NOT NULL | NOW() | 생성 시간 |
| updated_at | TIMESTAMP | NOT NULL | NOW() | 수정 시간 |

#### 인덱스
```sql
PRIMARY KEY (id)
UNIQUE INDEX uk_event_stats_event_id (event_id)
INDEX idx_event_stats_user_id (user_id)
INDEX idx_event_stats_status (status)
INDEX idx_event_stats_created_at (created_at DESC)
```

#### 제약 조건
```sql
CHECK (total_participants >= 0)
CHECK (total_views >= 0)
CHECK (estimated_roi >= 0)
CHECK (target_roi >= 0)
CHECK (total_investment >= 0)
CHECK (expected_revenue >= 0)
CHECK (status IN ('ACTIVE', 'ENDED', 'ARCHIVED'))
```

---

### 1.2 channel_stats (채널 통계)

#### 테이블 설명
- **목적**: 채널별 성과 데이터 관리
- **데이터 특성**: 외부 API 연동 데이터, Circuit Breaker 패턴 적용
- **조회 패턴**: eventId 기반 목록 조회, eventId + channelName 단건 조회

#### 컬럼 정의
| 컬럼명 | 데이터 타입 | Null | 기본값 | 설명 |
|--------|------------|------|-------|------|
| id | BIGSERIAL | NOT NULL | AUTO | 기본 키 |
| event_id | VARCHAR(36) | NOT NULL | - | 이벤트 ID (UUID) |
| channel_name | VARCHAR(50) | NOT NULL | - | 채널명 (WooriTV, GenieTV 등) |
| channel_type | VARCHAR(20) | NOT NULL | - | 채널 타입 (TV, SNS, VOICE) |
| impressions | INTEGER | NOT NULL | 0 | 노출 수 |
| views | INTEGER | NOT NULL | 0 | 조회 수 |
| clicks | INTEGER | NOT NULL | 0 | 클릭 수 |
| participants | INTEGER | NOT NULL | 0 | 참여자 수 |
| conversions | INTEGER | NOT NULL | 0 | 전환 수 |
| distribution_cost | DECIMAL(15,2) | NOT NULL | 0.00 | 배포 비용 (원) |
| likes | INTEGER | NOT NULL | 0 | 좋아요 수 (SNS) |
| comments | INTEGER | NOT NULL | 0 | 댓글 수 (SNS) |
| shares | INTEGER | NOT NULL | 0 | 공유 수 (SNS) |
| total_calls | INTEGER | NOT NULL | 0 | 총 통화 수 (VOICE) |
| completed_calls | INTEGER | NOT NULL | 0 | 완료 통화 수 (VOICE) |
| average_duration | INTEGER | NOT NULL | 0 | 평균 통화 시간 (초, VOICE) |
| created_at | TIMESTAMP | NOT NULL | NOW() | 생성 시간 |
| updated_at | TIMESTAMP | NOT NULL | NOW() | 수정 시간 |

#### 인덱스
```sql
PRIMARY KEY (id)
UNIQUE INDEX uk_channel_stats_event_channel (event_id, channel_name)
INDEX idx_channel_stats_event_id (event_id)
INDEX idx_channel_stats_channel_type (channel_type)
INDEX idx_channel_stats_participants (participants DESC)
```

#### 제약 조건
```sql
CHECK (impressions >= 0)
CHECK (views >= 0)
CHECK (clicks >= 0)
CHECK (participants >= 0)
CHECK (conversions >= 0)
CHECK (distribution_cost >= 0)
CHECK (total_calls >= 0)
CHECK (completed_calls >= 0 AND completed_calls <= total_calls)
CHECK (average_duration >= 0)
CHECK (channel_type IN ('TV', 'SNS', 'VOICE'))
```

---

### 1.3 timeline_data (시계열 데이터)

#### 테이블 설명
- **목적**: 시간별 참여 추이 데이터 관리
- **데이터 특성**: 시계열 데이터, 누적 참여자 수 포함
- **조회 패턴**: eventId + timestamp 범위 조회 (시간 순서)

#### 컬럼 정의
| 컬럼명 | 데이터 타입 | Null | 기본값 | 설명 |
|--------|------------|------|-------|------|
| id | BIGSERIAL | NOT NULL | AUTO | 기본 키 |
| event_id | VARCHAR(36) | NOT NULL | - | 이벤트 ID (UUID) |
| timestamp | TIMESTAMP | NOT NULL | - | 기록 시간 |
| participants | INTEGER | NOT NULL | 0 | 해당 시간 참여자 수 |
| views | INTEGER | NOT NULL | 0 | 해당 시간 조회 수 |
| engagement | INTEGER | NOT NULL | 0 | 참여도 (상호작용 수) |
| conversions | INTEGER | NOT NULL | 0 | 해당 시간 전환 수 |
| cumulative_participants | INTEGER | NOT NULL | 0 | 누적 참여자 수 |
| created_at | TIMESTAMP | NOT NULL | NOW() | 생성 시간 |
| updated_at | TIMESTAMP | NOT NULL | NOW() | 수정 시간 |

#### 인덱스
```sql
PRIMARY KEY (id)
INDEX idx_timeline_event_timestamp (event_id, timestamp ASC)
INDEX idx_timeline_timestamp (timestamp ASC)
```

**시계열 최적화**: BRIN 인덱스 사용 권장 (대량 시계열 데이터)
```sql
CREATE INDEX idx_timeline_brin_timestamp ON timeline_data USING BRIN (timestamp);
```

#### 제약 조건
```sql
CHECK (participants >= 0)
CHECK (views >= 0)
CHECK (engagement >= 0)
CHECK (conversions >= 0)
CHECK (cumulative_participants >= 0)
```

---

## 2. Redis 캐시 설계

### 2.1 캐시 구조

#### 대시보드 캐시
```
Key: analytics:dashboard:{eventId}
Type: String (JSON)
TTL: 3600초 (1시간)
Value: {
  "eventId": "uuid",
  "eventTitle": "이벤트 제목",
  "summary": { ... },
  "channelPerformance": [ ... ],
  "roi": { ... }
}
```

#### 채널 분석 캐시
```
Key: analytics:channel:{eventId}:{channelName}
Type: String (JSON)
TTL: 3600초
Value: {
  "channelName": "WooriTV",
  "metrics": { ... },
  "performance": { ... }
}
```

#### ROI 분석 캐시
```
Key: analytics:roi:{eventId}
Type: String (JSON)
TTL: 3600초
Value: {
  "currentRoi": 15.5,
  "targetRoi": 20.0,
  "investment": { ... }
}
```

#### 타임라인 캐시
```
Key: analytics:timeline:{eventId}:{granularity}
Type: String (JSON)
TTL: 3600초
Value: {
  "dataPoints": [ ... ],
  "trend": { ... }
}
```

#### 사용자 통합 분석 캐시
```
Key: analytics:user:{userId}
Type: String (JSON)
TTL: 3600초
Value: {
  "totalEvents": 5,
  "summary": { ... },
  "events": [ ... ]
}
```

#### 멱등성 처리 캐시
```
Key: analytics:processed:{messageId}
Type: Set
TTL: 86400초 (24시간)
Value: "1" (존재 여부만 확인)
```

### 2.2 캐시 전략

#### Cache-Aside 패턴
1. **조회**: 캐시 먼저 확인 → 없으면 DB 조회 → 캐시 저장
2. **갱신**: DB 업데이트 → 캐시 무효화 (Kafka Consumer)
3. **만료**: TTL 만료 시 자동 삭제

#### 캐시 무효화 조건
- **Kafka 이벤트 수신 시**: 관련 캐시 삭제
- **배치 스케줄러**: 5분마다 전체 갱신
- **수동 갱신 요청**: `refresh=true` 파라미터

---

## 3. 데이터베이스 설정

### 3.1 PostgreSQL 설정

#### Connection Pool
```properties
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.idle-timeout=600000
spring.datasource.hikari.max-lifetime=1800000
```

#### 시계열 최적화
```sql
-- timeline_data 테이블 파티셔닝 (월별)
CREATE TABLE timeline_data (
  ...
) PARTITION BY RANGE (timestamp);

CREATE TABLE timeline_data_2025_01 PARTITION OF timeline_data
  FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');

-- 자동 파티션 생성 (pg_cron 활용)
```

### 3.2 Redis 설정

```properties
spring.redis.host=${REDIS_HOST:localhost}
spring.redis.port=${REDIS_PORT:6379}
spring.redis.password=${REDIS_PASSWORD:}
spring.redis.database=${REDIS_DATABASE:0}
spring.redis.timeout=3000
spring.redis.lettuce.pool.max-active=20
spring.redis.lettuce.pool.max-idle=10
spring.redis.lettuce.pool.min-idle=5
```

---

## 4. 데이터 접근 패턴

### 4.1 이벤트 대시보드 조회
```sql
-- Cache Miss 시
SELECT * FROM event_stats WHERE event_id = ?;
SELECT * FROM channel_stats WHERE event_id = ?;
```

### 4.2 사용자별 이벤트 조회
```sql
SELECT * FROM event_stats WHERE user_id = ? ORDER BY created_at DESC;
```

### 4.3 타임라인 조회
```sql
SELECT * FROM timeline_data
WHERE event_id = ?
  AND timestamp BETWEEN ? AND ?
ORDER BY timestamp ASC;
```

### 4.4 채널별 성과 조회
```sql
SELECT * FROM channel_stats
WHERE event_id = ?
  AND channel_name IN (?, ?, ...)
ORDER BY participants DESC;
```

---

## 5. 데이터 동기화

### 5.1 Kafka Consumer를 통한 실시간 업데이트

#### EventCreatedEvent 처리
```java
@KafkaListener(topics = "event-created")
public void handleEventCreated(String message) {
    // 1. event_stats 생성
    EventStats stats = new EventStats(...);
    eventStatsRepository.save(stats);

    // 2. 캐시 무효화
    redisTemplate.delete("analytics:dashboard:" + eventId);
}
```

#### ParticipantRegisteredEvent 처리
```java
@KafkaListener(topics = "participant-registered")
public void handleParticipantRegistered(String message) {
    // 1. 멱등성 체크 (Redis Set)
    if (redisTemplate.opsForSet().isMember("analytics:processed", messageId)) {
        return;
    }

    // 2. event_stats 업데이트 (참여자 증가)
    eventStats.incrementParticipants();

    // 3. timeline_data 업데이트
    updateTimelineData(eventId);

    // 4. 캐시 무효화
    redisTemplate.delete("analytics:*:" + eventId);

    // 5. 멱등성 기록
    redisTemplate.opsForSet().add("analytics:processed", messageId);
    redisTemplate.expire("analytics:processed:" + messageId, 24, TimeUnit.HOURS);
}
```

#### DistributionCompletedEvent 처리
```java
@KafkaListener(topics = "distribution-completed")
public void handleDistributionCompleted(String message) {
    // 1. channel_stats 생성 또는 업데이트
    channelStatsRepository.save(channelStats);

    // 2. 캐시 무효화
    redisTemplate.delete("analytics:channel:" + eventId + ":" + channelName);
}
```

### 5.2 배치 스케줄러를 통한 주기적 갱신

```java
@Scheduled(cron = "0 */5 * * * *") // 5분마다
public void refreshAnalyticsDashboard() {
    List<EventStats> activeEvents = eventStatsRepository.findByStatus("ACTIVE");

    for (EventStats event : activeEvents) {
        // 1. 캐시 갱신
        AnalyticsDashboardResponse response = analyticsService.getDashboardData(
            event.getEventId(), null, null, true
        );

        // 2. 외부 API 호출 (Circuit Breaker)
        externalChannelService.updateChannelStatsFromExternalAPIs(
            event.getEventId(),
            channelStatsRepository.findByEventId(event.getEventId())
        );
    }
}
```

---

## 6. 성능 최적화

### 6.1 인덱스 전략
1. **B-Tree 인덱스**: eventId, userId 등 정확한 매칭
2. **BRIN 인덱스**: timestamp 시계열 데이터
3. **복합 인덱스**: (event_id, channel_name) 등 자주 함께 조회

### 6.2 쿼리 최적화
- **N+1 문제 방지**: Fetch Join 사용 (필요 시)
- **Projection**: 필요한 컬럼만 조회
- **캐싱**: Redis를 통한 조회 성능 향상

### 6.3 파티셔닝
- **timeline_data**: 월별 파티셔닝으로 대용량 시계열 데이터 관리
- **자동 파티션 생성**: pg_cron을 통한 자동화

---

## 7. 데이터 보안

### 7.1 접근 제어
```sql
-- 읽기 전용 사용자
CREATE USER analytics_readonly WITH PASSWORD 'secure_password';
GRANT SELECT ON ALL TABLES IN SCHEMA public TO analytics_readonly;

-- 읽기/쓰기 사용자
CREATE USER analytics_readwrite WITH PASSWORD 'secure_password';
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO analytics_readwrite;
```

### 7.2 데이터 마스킹
- **개인 정보**: userId는 UUID로만 저장 (이름, 연락처 등 제외)
- **금액 정보**: 암호화 저장 권장 (필요 시)

---

## 8. 백업 및 복구

### 8.1 백업 전략
```bash
# 일일 백업 (pg_dump)
pg_dump -U postgres -F c -b -v -f /backup/analytics_$(date +%Y%m%d).dump analytics_db

# 주간 전체 백업 (pg_basebackup)
pg_basebackup -U postgres -D /backup/full -Ft -z -P
```

### 8.2 복구 전략
```bash
# 데이터베이스 복구
pg_restore -U postgres -d analytics_db -v /backup/analytics_20250129.dump
```

### 8.3 Redis 백업
```bash
# RDB 스냅샷 (매일 자정)
redis-cli --rdb /backup/redis_$(date +%Y%m%d).rdb

# AOF 로그 (실시간)
redis-cli CONFIG SET appendonly yes
```

---

## 9. 모니터링

### 9.1 데이터베이스 모니터링
```sql
-- 테이블 크기 확인
SELECT
  schemaname,
  tablename,
  pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;

-- 느린 쿼리 확인
SELECT
  query,
  calls,
  total_time,
  mean_time
FROM pg_stat_statements
ORDER BY mean_time DESC
LIMIT 10;
```

### 9.2 Redis 모니터링
```bash
# 메모리 사용량
redis-cli INFO memory

# 캐시 히트율
redis-cli INFO stats | grep keyspace

# 느린 명령어 확인
redis-cli SLOWLOG GET 10
```

---

## 10. 트러블슈팅

### 10.1 일반적인 문제 및 해결

#### 문제 1: 캐시 Miss 증가
- **원인**: TTL이 너무 짧거나, 잦은 캐시 무효화
- **해결**: TTL 조정, 캐시 무효화 로직 최적화

#### 문제 2: 시계열 쿼리 느림
- **원인**: BRIN 인덱스 미사용, 파티셔닝 미적용
- **해결**: BRIN 인덱스 생성, 월별 파티셔닝 적용

#### 문제 3: 외부 API 장애
- **원인**: Circuit Breaker 미동작, Timeout 설정 문제
- **해결**: Resilience4j 설정 확인, Timeout 조정

---

## 부록: Entity 매핑 확인

### EventStats 클래스 매핑
```java
@Entity
@Table(name = "event_stats")
public class EventStats extends BaseTimeEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "event_id", nullable = false, unique = true, length = 36)
    private String eventId;

    @Column(name = "event_title", nullable = false)
    private String eventTitle;

    @Column(name = "user_id", nullable = false, length = 36)
    private String userId;

    @Column(name = "total_participants", nullable = false)
    private Integer totalParticipants = 0;

    // ... 기타 필드
}
```

### ChannelStats 클래스 매핑
```java
@Entity
@Table(name = "channel_stats")
public class ChannelStats extends BaseTimeEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "event_id", nullable = false, length = 36)
    private String eventId;

    @Column(name = "channel_name", nullable = false, length = 50)
    private String channelName;

    // ... 기타 필드
}
```

### TimelineData 클래스 매핑
```java
@Entity
@Table(name = "timeline_data")
public class TimelineData extends BaseTimeEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "event_id", nullable = false, length = 36)
    private String eventId;

    @Column(name = "timestamp", nullable = false)
    private LocalDateTime timestamp;

    // ... 기타 필드
}
```

---

**문서 버전**: v1.0
**작성자**: Backend Architect (최수연 "아키텍처")
**작성일**: 2025-10-29
