kt-event-marketing/design/backend/database/analytics-service.md
jhbkjh 3075a5d49f 물리아키텍처 설계 완료
 주요 기능
- 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>
2025-10-29 15:13:01 +09:00

17 KiB

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() 수정 시간

인덱스

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 패턴

  1. 조회: 캐시 먼저 확인 → 없으면 DB 조회 → 캐시 저장
  2. 갱신: DB 업데이트 → 캐시 무효화 (Kafka Consumer)
  3. 만료: 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 인덱스 전략

  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 접근 제어

-- 읽기 전용 사용자
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