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시간 |
데이터 접근 패턴
- 이벤트별 조회: eventId 기반 빠른 조회 (B-Tree 인덱스)
- 사용자별 조회: userId 기반 다중 이벤트 조회
- 시계열 조회: timestamp 범위 검색 (BRIN 인덱스)
- 채널별 조회: 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() |
수정 시간 |
인덱스
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)
제약 조건
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() |
수정 시간 |
인덱스
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)
제약 조건
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() |
수정 시간 |
인덱스
PRIMARY KEY (id)
INDEX idx_timeline_event_timestamp (event_id, timestamp ASC)
INDEX idx_timeline_timestamp (timestamp ASC)
시계열 최적화: BRIN 인덱스 사용 권장 (대량 시계열 데이터)
CREATE INDEX idx_timeline_brin_timestamp ON timeline_data USING BRIN (timestamp);
제약 조건
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 패턴
- 조회: 캐시 먼저 확인 → 없으면 DB 조회 → 캐시 저장
- 갱신: DB 업데이트 → 캐시 무효화 (Kafka Consumer)
- 만료: TTL 만료 시 자동 삭제
캐시 무효화 조건
- Kafka 이벤트 수신 시: 관련 캐시 삭제
- 배치 스케줄러: 5분마다 전체 갱신
- 수동 갱신 요청:
refresh=true 파라미터
3. 데이터베이스 설정
3.1 PostgreSQL 설정
Connection Pool
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
시계열 최적화
-- 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 설정
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 이벤트 대시보드 조회
-- Cache Miss 시
SELECT * FROM event_stats WHERE event_id = ?;
SELECT * FROM channel_stats WHERE event_id = ?;
4.2 사용자별 이벤트 조회
SELECT * FROM event_stats WHERE user_id = ? ORDER BY created_at DESC;
4.3 타임라인 조회
SELECT * FROM timeline_data
WHERE event_id = ?
AND timestamp BETWEEN ? AND ?
ORDER BY timestamp ASC;
4.4 채널별 성과 조회
SELECT * FROM channel_stats
WHERE event_id = ?
AND channel_name IN (?, ?, ...)
ORDER BY participants DESC;
5. 데이터 동기화
5.1 Kafka Consumer를 통한 실시간 업데이트
EventCreatedEvent 처리
@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 처리
@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 처리
@KafkaListener(topics = "distribution-completed")
public void handleDistributionCompleted(String message) {
// 1. channel_stats 생성 또는 업데이트
channelStatsRepository.save(channelStats);
// 2. 캐시 무효화
redisTemplate.delete("analytics:channel:" + eventId + ":" + channelName);
}
5.2 배치 스케줄러를 통한 주기적 갱신
@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 인덱스 전략
- B-Tree 인덱스: eventId, userId 등 정확한 매칭
- BRIN 인덱스: timestamp 시계열 데이터
- 복합 인덱스: (event_id, channel_name) 등 자주 함께 조회
6.2 쿼리 최적화
- N+1 문제 방지: Fetch Join 사용 (필요 시)
- Projection: 필요한 컬럼만 조회
- 캐싱: Redis를 통한 조회 성능 향상
6.3 파티셔닝
- timeline_data: 월별 파티셔닝으로 대용량 시계열 데이터 관리
- 자동 파티션 생성: pg_cron을 통한 자동화
7. 데이터 보안
7.1 접근 제어
-- 읽기 전용 사용자
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 백업 전략
# 일일 백업 (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 복구 전략
# 데이터베이스 복구
pg_restore -U postgres -d analytics_db -v /backup/analytics_20250129.dump
8.3 Redis 백업
# RDB 스냅샷 (매일 자정)
redis-cli --rdb /backup/redis_$(date +%Y%m%d).rdb
# AOF 로그 (실시간)
redis-cli CONFIG SET appendonly yes
9. 모니터링
9.1 데이터베이스 모니터링
-- 테이블 크기 확인
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 모니터링
# 메모리 사용량
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 클래스 매핑
@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 클래스 매핑
@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 클래스 매핑
@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