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

## 데이터설계 요약

### 설계 개요
- **서비스**: Content Service (이미지 생성 및 콘텐츠 관리)
- **아키텍처 패턴**: Clean Architecture
- **데이터베이스**: PostgreSQL (주 저장소), Redis (캐시 및 Job 상태 관리)
- **설계 원칙**: 데이터독립성원칙 준수, Entity 클래스와 1:1 매핑

### 주요 엔티티
1. **Content**: 이벤트별 콘텐츠 집합 정보
2. **GeneratedImage**: AI 생성 이미지 메타데이터 및 CDN URL
3. **Job**: 비동기 이미지 생성 작업 상태 추적

### 캐시 설계 (Redis)
1. **RedisJobData**: Job 상태 추적 (TTL: 1시간)
2. **RedisImageData**: 이미지 캐싱 (TTL: 7일)
3. **RedisAIEventData**: AI 추천 데이터 캐싱 (TTL: 1시간)

### 주요 특징
- **이미지 메타데이터 최적화**: CDN URL만 저장, 실제 이미지는 Azure Blob Storage
- **비동기 처리**: Kafka 기반 Job 처리, Redis로 상태 관리
- **캐싱 전략**: eventId 기반 캐시 키, TTL 설정으로 자동 만료
- **외부 연동**: Stable Diffusion/DALL-E API, Azure Blob Storage CDN

---

## 1. PostgreSQL 데이터베이스 설계

### 1.1 테이블: content (콘텐츠 집합)

**목적**: 이벤트별 생성된 콘텐츠 집합 정보 관리

**Entity 매핑**: `com.kt.event.content.biz.domain.Content`

| 컬럼명 | 데이터 타입 | NULL | 기본값 | 설명 |
|--------|------------|------|--------|------|
| id | BIGSERIAL | NOT NULL | AUTO | 콘텐츠 ID (PK) |
| event_id | VARCHAR(100) | NOT NULL | - | 이벤트 초안 ID |
| event_title | VARCHAR(200) | NOT NULL | - | 이벤트 제목 |
| event_description | TEXT | NULL | - | 이벤트 설명 |
| created_at | TIMESTAMP | NOT NULL | NOW() | 생성 시각 |
| updated_at | TIMESTAMP | NOT NULL | NOW() | 수정 시각 |

**제약 조건**:
- PRIMARY KEY: id
- UNIQUE INDEX: event_id (이벤트당 하나의 콘텐츠 집합)
- INDEX: created_at (시간 기반 조회)

**비즈니스 규칙**:
- 하나의 이벤트는 하나의 콘텐츠 집합만 보유
- 이미지는 별도 테이블에서 1:N 관계로 관리

---

### 1.2 테이블: generated_image (생성된 이미지)

**목적**: AI 생성 이미지 메타데이터 및 CDN URL 관리

**Entity 매핑**: `com.kt.event.content.biz.domain.GeneratedImage`

| 컬럼명 | 데이터 타입 | NULL | 기본값 | 설명 |
|--------|------------|------|--------|------|
| id | BIGSERIAL | NOT NULL | AUTO | 이미지 ID (PK) |
| event_id | VARCHAR(100) | NOT NULL | - | 이벤트 초안 ID |
| style | VARCHAR(20) | NOT NULL | - | 이미지 스타일 (FANCY, SIMPLE, TRENDY) |
| platform | VARCHAR(30) | NOT NULL | - | 플랫폼 (INSTAGRAM, FACEBOOK, KAKAO, BLOG) |
| cdn_url | VARCHAR(500) | NOT NULL | - | CDN 이미지 URL (Azure Blob) |
| prompt | TEXT | NOT NULL | - | 생성에 사용된 프롬프트 |
| selected | BOOLEAN | NOT NULL | false | 사용자 선택 여부 |
| width | INT | NOT NULL | - | 이미지 너비 (픽셀) |
| height | INT | NOT NULL | - | 이미지 높이 (픽셀) |
| file_size | BIGINT | NULL | - | 파일 크기 (bytes) |
| content_type | VARCHAR(50) | NOT NULL | 'image/png' | MIME 타입 |
| created_at | TIMESTAMP | NOT NULL | NOW() | 생성 시각 |
| updated_at | TIMESTAMP | NOT NULL | NOW() | 수정 시각 |

**제약 조건**:
- PRIMARY KEY: id
- INDEX: (event_id, style, platform) - 필터링 조회 최적화
- INDEX: event_id - 이벤트별 이미지 조회
- INDEX: created_at - 시간 기반 조회
- CHECK: style IN ('FANCY', 'SIMPLE', 'TRENDY')
- CHECK: platform IN ('INSTAGRAM', 'FACEBOOK', 'KAKAO', 'BLOG')
- CHECK: width > 0 AND height > 0

**비즈니스 규칙**:
- 이미지 실체는 Azure Blob Storage에 저장, DB는 메타데이터만 보유
- 동일한 (event_id, style, platform) 조합으로 여러 이미지 생성 가능 (재생성)
- selected = true인 이미지가 최종 선택 이미지

**플랫폼별 기본 해상도**:
- INSTAGRAM: 1080x1080
- FACEBOOK: 1200x628
- KAKAO: 800x800
- BLOG: 800x600

---

### 1.3 테이블: job (비동기 작업 추적)

**목적**: 이미지 생성 비동기 작업 상태 추적

**Entity 매핑**: `com.kt.event.content.biz.domain.Job`

| 컬럼명 | 데이터 타입 | NULL | 기본값 | 설명 |
|--------|------------|------|--------|------|
| id | VARCHAR(100) | NOT NULL | - | Job ID (PK) |
| event_id | VARCHAR(100) | NOT NULL | - | 이벤트 초안 ID |
| job_type | VARCHAR(50) | NOT NULL | - | 작업 타입 (IMAGE_GENERATION, IMAGE_REGENERATION) |
| status | VARCHAR(20) | NOT NULL | 'PENDING' | 작업 상태 (PENDING, PROCESSING, COMPLETED, FAILED) |
| progress | INT | NOT NULL | 0 | 진행률 (0-100) |
| result_message | TEXT | NULL | - | 완료 메시지 |
| error_message | TEXT | NULL | - | 에러 메시지 |
| created_at | TIMESTAMP | NOT NULL | NOW() | 생성 시각 |
| updated_at | TIMESTAMP | NOT NULL | NOW() | 수정 시각 |
| completed_at | TIMESTAMP | NULL | - | 완료 시각 |

**제약 조건**:
- PRIMARY KEY: id
- INDEX: event_id - 이벤트별 작업 조회
- INDEX: (status, created_at) - 상태별 작업 조회
- CHECK: status IN ('PENDING', 'PROCESSING', 'COMPLETED', 'FAILED')
- CHECK: job_type IN ('IMAGE_GENERATION', 'IMAGE_REGENERATION')
- CHECK: progress >= 0 AND progress <= 100

**비즈니스 규칙**:
- Job ID는 "job-img-{uuid}" 형식 (외부에서 생성)
- 상태 전이: PENDING → PROCESSING → COMPLETED/FAILED
- COMPLETED/FAILED 상태에서 completed_at 자동 설정
- Redis에도 동일한 Job 정보 저장 (TTL 1시간, 폴링 조회 최적화)

---

## 2. Redis 캐시 설계

### 2.1 RedisJobData (Job 상태 캐싱)

**목적**: 비동기 작업 상태 폴링 조회 성능 최적화

**DTO 매핑**: `com.kt.event.content.biz.dto.RedisJobData`

**Redis 키 패턴**: `job:{jobId}`

**TTL**: 1시간 (3600초)

**데이터 구조** (Hash):
```
job:job-img-abc123 = {
  "id": "job-img-abc123",
  "eventId": "evt-draft-12345",
  "jobType": "IMAGE_GENERATION",
  "status": "PROCESSING",
  "progress": 50,
  "resultMessage": null,
  "errorMessage": null,
  "createdAt": "2025-10-29T10:00:00Z",
  "updatedAt": "2025-10-29T10:00:05Z"
}
```

**사용 시나리오**:
1. 이미지 생성 요청 시 Job 생성 → Redis 저장
2. 클라이언트 폴링 조회 → Redis에서 빠르게 조회
3. Job 완료 후 1시간 뒤 자동 삭제
4. PostgreSQL의 job 테이블과 동기화 (영구 이력)

---

### 2.2 RedisImageData (이미지 캐싱)

**목적**: 동일 이벤트 재요청 시 즉시 반환

**DTO 매핑**: `com.kt.event.content.biz.dto.RedisImageData`

**Redis 키 패턴**: `image:{eventId}:{style}:{platform}`

**TTL**: 7일 (604800초)

**데이터 구조** (Hash):
```
image:evt-draft-12345:SIMPLE:INSTAGRAM = {
  "eventId": "evt-draft-12345",
  "style": "SIMPLE",
  "platform": "INSTAGRAM",
  "imageUrl": "https://cdn.kt-event.com/images/evt-draft-12345-simple.png",
  "prompt": "Clean and simple event poster with coffee theme",
  "createdAt": "2025-10-29T10:00:10Z"
}
```

**사용 시나리오**:
1. 이미지 생성 완료 → Redis 저장
2. 동일 이벤트 재요청 → Redis 캐시 확인 → 즉시 반환
3. 7일 후 자동 삭제 (오래된 캐시 정리)

---

### 2.3 RedisAIEventData (AI 추천 데이터 캐싱)

**목적**: AI Service 이벤트 데이터 캐싱

**DTO 매핑**: `com.kt.event.content.biz.dto.RedisAIEventData`

**Redis 키 패턴**: `ai:event:{eventId}`

**TTL**: 1시간 (3600초)

**데이터 구조** (Hash):
```
ai:event:evt-draft-12345 = {
  "eventId": "evt-draft-12345",
  "recommendedStyles": ["SIMPLE", "TRENDY"],
  "recommendedKeywords": ["coffee", "spring", "discount"],
  "cachedAt": "2025-10-29T10:00:00Z"
}
```

**사용 시나리오**:
1. AI Service에서 이벤트 분석 완료 → Redis 저장
2. Content Service에서 이미지 생성 시 AI 추천 데이터 참조
3. 1시간 후 자동 삭제

---

## 3. 인덱스 전략

### 3.1 성능 최적화 인덱스

**generated_image 테이블**:
```sql
-- 이벤트별 이미지 조회 (가장 빈번)
CREATE INDEX idx_generated_image_event_id ON generated_image(event_id);

-- 필터링 조회 (스타일, 플랫폼)
CREATE INDEX idx_generated_image_filter ON generated_image(event_id, style, platform);

-- 선택된 이미지 조회
CREATE INDEX idx_generated_image_selected ON generated_image(event_id, selected) WHERE selected = true;

-- 시간 기반 조회 (최근 생성 이미지)
CREATE INDEX idx_generated_image_created ON generated_image(created_at DESC);
```

**job 테이블**:
```sql
-- 이벤트별 작업 조회
CREATE INDEX idx_job_event_id ON job(event_id);

-- 상태별 작업 조회 (모니터링)
CREATE INDEX idx_job_status ON job(status, created_at DESC);
```

**content 테이블**:
```sql
-- 이벤트 ID 기반 조회 (UNIQUE)
CREATE UNIQUE INDEX idx_content_event_id ON content(event_id);
```

---

## 4. 데이터 정합성 규칙

### 4.1 데이터 일관성 보장

**PostgreSQL ↔ Redis 동기화**:
- **Write-Through**: Job 생성 시 PostgreSQL + Redis 동시 저장
- **Cache-Aside**: 이미지 조회 시 Redis 먼저 확인 → 없으면 PostgreSQL
- **TTL 기반 자동 만료**: Redis 데이터는 TTL로 자동 정리

### 4.2 트랜잭션 범위

**이미지 생성 트랜잭션**:
```
BEGIN TRANSACTION
  1. Job 상태 업데이트 (PROCESSING)
  2. 외부 API 호출 (Stable Diffusion)
  3. CDN 업로드 (Azure Blob)
  4. generated_image INSERT
  5. Job 상태 업데이트 (COMPLETED)
  6. Redis 캐시 저장
COMMIT
```

**실패 시 롤백**:
- 외부 API 실패 → Job 상태 FAILED, error_message 저장
- CDN 업로드 실패 → 재시도 (3회), 최종 실패 시 FAILED
- Circuit Breaker OPEN → Fallback 템플릿 이미지 사용

---

## 5. 백업 및 보존 정책

### 5.1 백업 전략

**PostgreSQL**:
- **Full Backup**: 매일 오전 2시 (Cron Job)
- **Incremental Backup**: 6시간마다
- **보관 기간**: 30일

**Redis**:
- **RDB Snapshot**: 1시간마다
- **AOF (Append-Only File)**: 실시간 로깅
- **보관 기간**: 7일

### 5.2 데이터 보존 정책

**generated_image**:
- **보존 기간**: 90일
- **정리 방식**: created_at 기준 90일 초과 데이터 자동 삭제 (Batch Job)

**job**:
- **보존 기간**: 30일
- **정리 방식**: created_at 기준 30일 초과 데이터 자동 삭제

**content**:
- **보존 기간**: 영구 (이미지 삭제 시에만 CASCADE 삭제)

---

## 6. 확장성 고려사항

### 6.1 수평 확장

**Read Replica**:
- PostgreSQL Read Replica 구성 (조회 성능 향상)
- 쓰기: Master, 읽기: Replica

**Sharding 전략** (미래 대비):
- Shard Key: event_id (이벤트 ID 기반 분산)
- 예상 임계점: 1억 건 이미지 이상

### 6.2 캐시 전략

**Redis Cluster**:
- 3 Master + 3 Replica 구성
- 데이터 파티셔닝: event_id 기반 Hash Slot

**Cache Warming**:
- 자주 조회되는 이미지는 Redis에 영구 보관 (별도 TTL 없음)

---

## 7. 모니터링 지표

### 7.1 성능 지표

**PostgreSQL**:
- QPS (Queries Per Second): 이미지 조회 빈도
- Slow Query: 100ms 이상 쿼리 모니터링
- Connection Pool: 사용률 70% 이하 유지

**Redis**:
- Cache Hit Ratio: 90% 이상 목표
- Memory Usage: 80% 이하 유지
- Eviction Rate: 최소화

### 7.2 비즈니스 지표

**이미지 생성**:
- 성공률: 95% 이상
- 평균 생성 시간: 10초 이내
- Circuit Breaker OPEN 빈도: 월 5회 이하

**캐시 효율**:
- 재사용률: 동일 이벤트 재요청 비율
- TTL 만료율: 7일 이내 재조회 비율

---

## 8. 데이터독립성 검증

### 8.1 서비스 경계

**Content Service 소유 데이터**:
- content, generated_image, job 테이블 완전 소유
- 다른 서비스는 API를 통해서만 접근

**외부 의존성 최소화**:
- Event Service 데이터: event_id만 참조 (FK 없음)
- User Service 데이터: 참조하지 않음
- AI Service 데이터: Redis 캐시로만 참조

### 8.2 크로스 서비스 조인 금지

**허용되지 않는 패턴**:
```sql
-- ❌ 금지: Event Service DB와 조인
SELECT * FROM event_service.event e
JOIN content_service.generated_image i ON e.id = i.event_id;
```

**올바른 패턴**:
```java
// ✅ 허용: API 호출 또는 캐시 참조
String eventTitle = eventServiceClient.getEvent(eventId).getTitle();
```

---

## 9. 보안 고려사항

### 9.1 접근 제어

**PostgreSQL**:
- 계정: content_service_user (최소 권한)
- 권한: content, generated_image, job 테이블만 SELECT, INSERT, UPDATE, DELETE
- 스키마 변경: DBA 계정만 가능

**Redis**:
- 계정: content_service_redis (별도 패스워드)
- ACL: 특정 키 패턴만 접근 (`job:*`, `image:*`, `ai:event:*`)

### 9.2 데이터 암호화

**전송 암호화**:
- PostgreSQL: SSL/TLS 연결 강제
- Redis: TLS 연결 강제

**저장 암호화**:
- PostgreSQL: AES-256 암호화 (pgcrypto 확장)
- CDN URL은 공개 데이터 (암호화 불필요)

---

## 10. 클래스 설계와의 매핑 검증

### 10.1 Entity 클래스 매핑

| Entity 클래스 | PostgreSQL 테이블 | 필드 매핑 일치 |
|--------------|-------------------|---------------|
| Content | content | ✅ 완전 일치 |
| GeneratedImage | generated_image | ✅ 완전 일치 (width, height 추가) |
| Job | job | ✅ 완전 일치 |

### 10.2 DTO 매핑

| DTO 클래스 | Redis 키 패턴 | 필드 매핑 일치 |
|-----------|---------------|---------------|
| RedisJobData | job:{jobId} | ✅ 완전 일치 |
| RedisImageData | image:{eventId}:{style}:{platform} | ✅ 완전 일치 |
| RedisAIEventData | ai:event:{eventId} | ✅ 완전 일치 |

### 10.3 Enum 매핑

| Enum 클래스 | 데이터베이스 | 값 일치 |
|------------|-------------|---------|
| ImageStyle | VARCHAR CHECK | ✅ FANCY, SIMPLE, TRENDY |
| Platform | VARCHAR CHECK | ✅ INSTAGRAM, FACEBOOK, KAKAO, BLOG |
| JobStatus | VARCHAR CHECK | ✅ PENDING, PROCESSING, COMPLETED, FAILED |

---

## 11. 마이그레이션 전략

### 11.1 초기 배포

**데이터베이스 생성**:
```sql
CREATE DATABASE content_service_db;
CREATE SCHEMA content;
```

**테이블 생성 순서**:
1. content (부모 테이블)
2. generated_image (자식 테이블)
3. job (독립 테이블)

### 11.2 버전 관리

**도구**: Flyway (Spring Boot 통합)

**마이그레이션 파일 위치**: `src/main/resources/db/migration/`

**명명 규칙**: `V{version}__{description}.sql`

**예시**:
- V1__create_content_tables.sql
- V2__add_image_size_columns.sql
- V3__create_job_status_index.sql

---

## 12. 테스트 데이터

### 12.1 샘플 데이터

**Content**:
```sql
INSERT INTO content (event_id, event_title, event_description)
VALUES ('evt-draft-12345', '봄맞이 커피 할인 이벤트', '신메뉴 아메리카노 1+1 이벤트');
```

**GeneratedImage**:
```sql
INSERT INTO generated_image (event_id, style, platform, cdn_url, prompt, width, height)
VALUES
  ('evt-draft-12345', 'SIMPLE', 'INSTAGRAM',
   'https://cdn.kt-event.com/images/evt-draft-12345-simple.png',
   'Clean and simple coffee event poster', 1080, 1080),
  ('evt-draft-12345', 'FANCY', 'INSTAGRAM',
   'https://cdn.kt-event.com/images/evt-draft-12345-fancy.png',
   'Vibrant and colorful coffee event poster', 1080, 1080);
```

**Job**:
```sql
INSERT INTO job (id, event_id, job_type, status, progress)
VALUES ('job-img-abc123', 'evt-draft-12345', 'IMAGE_GENERATION', 'COMPLETED', 100);
```

---

## 13. 참조 문서

- **클래스 설계서**: design/backend/class/content-service.puml
- **API 명세서**: design/backend/api/content-service-api.yaml
- **통합 검증**: design/backend/class/integration-verification.md
- **데이터설계 가이드**: claude/data-design.md

---

**작성자**: Backend Developer (아키텍트)
**작성일**: 2025-10-29
**버전**: v1.0
