# Content Service 아키텍처 수정 계획안 ## 문서 정보 - **작성일**: 2025-10-24 - **작성자**: Backend Developer - **대상 서비스**: Content Service - **수정 사유**: 논리 아키텍처 설계 준수 (Redis 단독 저장소) --- ## 1. 현황 분석 ### 1.1 논리 아키텍처 요구사항 **Content Service 핵심 책임** (논리 아키텍처 문서 기준): - 3가지 스타일 SNS 이미지 자동 생성 - 플랫폼별 이미지 최적화 - 이미지 편집 기능 **데이터 저장 요구사항**: ``` 데이터 저장: - Redis: 이미지 생성 결과 (CDN URL, TTL 7일) - CDN: 생성된 이미지 파일 ``` **데이터 읽기 요구사항**: ``` 데이터 읽기: - Redis에서 AI Service가 저장한 이벤트 데이터 읽기 ``` **캐시 구조** (논리 아키텍처 4.2절): ``` | 서비스 | 캐시 키 패턴 | 데이터 타입 | TTL | 예상 크기 | |--------|-------------|-----------|-----|----------| | Content | content:image:{이벤트ID}:{스타일} | String | 7일 | 0.2KB (URL) | | AI | ai:event:{이벤트ID} | Hash | 24시간 | 10KB | | AI/Content | job:{jobId} | Hash | 1시간 | 1KB | ``` ### 1.2 현재 구현 문제점 **문제 1: RDB 사용** - ❌ H2 In-Memory Database 사용 (Local) - ❌ PostgreSQL 설정 (Production) - ❌ Spring Data JPA 의존성 및 설정 **문제 2: JPA 엔티티 사용** ```java // 현재 구현 (잘못됨) @Entity public class Content { ... } @Entity public class GeneratedImage { ... } @Entity public class Job { ... } ``` **문제 3: JPA Repository 사용** ```java // 현재 구현 (잘못됨) public interface ContentRepository extends JpaRepository { ... } public interface GeneratedImageRepository extends JpaRepository { ... } public interface JobRepository extends JpaRepository { ... } ``` **문제 4: application-local.yml 설정** ```yaml # 현재 구현 (잘못됨) spring: datasource: url: jdbc:h2:mem:contentdb username: sa password: driver-class-name: org.h2.Driver jpa: database-platform: org.hibernate.dialect.H2Dialect hibernate: ddl-auto: create-drop ``` ### 1.3 올바른 아키텍처 ``` [Client] ↓ [API Gateway] ↓ [Content Service] ├─→ [Redis] ← AI 이벤트 데이터 읽기 │ └─ content:image:{eventId}:{style} (이미지 URL 저장, TTL 7일) │ └─ job:{jobId} (Job 상태, TTL 1시간) │ └─→ [External Image API] (Stable Diffusion/DALL-E) └─→ [Azure CDN] (이미지 파일 업로드) ``` **핵심 원칙**: 1. **Content Service는 Redis에만 데이터 저장** 2. **RDB (H2/PostgreSQL) 사용 안 함** 3. **JPA 사용 안 함** 4. **Redis는 캐시가 아닌 주 저장소로 사용** --- ## 2. 수정 계획 ### 2.1 삭제 대상 #### 2.1.1 Entity 파일 (3개) ``` content-service/src/main/java/com/kt/event/content/biz/domain/ ├─ Content.java ← 삭제 ├─ GeneratedImage.java ← 삭제 └─ Job.java ← 삭제 ``` #### 2.1.2 Repository 파일 (3개) ``` content-service/src/main/java/com/kt/event/content/biz/usecase/out/ ├─ ContentRepository.java ← 삭제 (또는 이름만 남기고 인터페이스 변경) ├─ GeneratedImageRepository.java ← 삭제 └─ JobRepository.java ← 삭제 ``` #### 2.1.3 JPA Adapter 파일 (있다면) ``` content-service/src/main/java/com/kt/event/content/infra/adapter/ └─ *JpaAdapter.java ← 모두 삭제 ``` #### 2.1.4 설정 파일 수정 - `application-local.yml`: H2, JPA 설정 제거 - `application.yml`: PostgreSQL 설정 제거 - `build.gradle`: JPA, H2, PostgreSQL 의존성 제거 ### 2.2 생성/수정 대상 #### 2.2.1 Redis 데이터 모델 (DTO) **파일 위치**: `content-service/src/main/java/com/kt/event/content/biz/dto/` **1) RedisImageData.java** (새로 생성) ```java package com.kt.event.content.biz.dto; import com.kt.event.content.biz.domain.ImageStyle; import com.kt.event.content.biz.domain.Platform; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.time.LocalDateTime; /** * Redis에 저장되는 이미지 데이터 구조 * Key: content:image:{eventDraftId}:{style}:{platform} * Type: String (JSON) * TTL: 7일 */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public class RedisImageData { private Long id; // 이미지 고유 ID private Long eventDraftId; // 이벤트 초안 ID private ImageStyle style; // 이미지 스타일 (FANCY, SIMPLE, TRENDY) private Platform platform; // 플랫폼 (INSTAGRAM, KAKAO, NAVER) private String cdnUrl; // CDN 이미지 URL private String prompt; // 이미지 생성 프롬프트 private Boolean selected; // 선택 여부 private LocalDateTime createdAt; private LocalDateTime updatedAt; } ``` **2) RedisJobData.java** (새로 생성) ```java package com.kt.event.content.biz.dto; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.time.LocalDateTime; /** * Redis에 저장되는 Job 상태 정보 * Key: job:{jobId} * Type: Hash * TTL: 1시간 */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public class RedisJobData { private String id; // Job ID (예: job-mock-7ada8bd3) private Long eventDraftId; // 이벤트 초안 ID private String jobType; // Job 타입 (image-generation, image-regeneration) private String status; // 상태 (PENDING, IN_PROGRESS, COMPLETED, FAILED) private Integer progress; // 진행률 (0-100) private String resultMessage; // 결과 메시지 private String errorMessage; // 에러 메시지 private LocalDateTime createdAt; private LocalDateTime updatedAt; } ``` **3) RedisAIEventData.java** (새로 생성 - 읽기 전용) ```java package com.kt.event.content.biz.dto; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.util.Map; /** * AI Service가 Redis에 저장한 이벤트 데이터 (읽기 전용) * Key: ai:event:{eventDraftId} * Type: Hash * TTL: 24시간 */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public class RedisAIEventData { private Long eventDraftId; private String eventTitle; private String eventDescription; private String targetAudience; private String eventObjective; private Map additionalData; // AI가 생성한 추가 데이터 } ``` #### 2.2.2 Redis Gateway 확장 **파일**: `content-service/src/main/java/com/kt/event/content/infra/gateway/RedisGateway.java` **추가 메서드**: ```java // 이미지 CRUD void saveImage(RedisImageData imageData, long ttlSeconds); Optional getImage(Long eventDraftId, ImageStyle style, Platform platform); List getImagesByEventId(Long eventDraftId); void deleteImage(Long eventDraftId, ImageStyle style, Platform platform); // Job 상태 관리 void saveJob(RedisJobData jobData, long ttlSeconds); Optional getJob(String jobId); void updateJobStatus(String jobId, String status, Integer progress); void updateJobResult(String jobId, String resultMessage); void updateJobError(String jobId, String errorMessage); // AI 이벤트 데이터 읽기 (이미 구현됨 - getAIRecommendation) // Optional> getAIRecommendation(Long eventDraftId); ``` #### 2.2.3 MockRedisGateway 확장 **파일**: `content-service/src/main/java/com/kt/event/content/biz/service/mock/MockRedisGateway.java` **추가 메서드**: - 위의 RedisGateway와 동일한 메서드들을 In-Memory Map으로 구현 - Local/Test 환경에서 Redis 없이 테스트 가능 #### 2.2.4 Port Interface 수정 **파일**: `content-service/src/main/java/com/kt/event/content/biz/usecase/out/` **1) ContentWriter.java 수정** ```java package com.kt.event.content.biz.usecase.out; import com.kt.event.content.biz.dto.RedisImageData; import java.util.List; /** * Content 저장 Port (Redis 기반) */ public interface ContentWriter { // 이미지 저장 (Redis) void saveImage(RedisImageData imageData, long ttlSeconds); // 이미지 삭제 (Redis) void deleteImage(Long eventDraftId, String style, String platform); // 여러 이미지 저장 (Redis) void saveImages(Long eventDraftId, List images, long ttlSeconds); } ``` **2) ContentReader.java 수정** ```java package com.kt.event.content.biz.usecase.out; import com.kt.event.content.biz.dto.RedisImageData; import java.util.List; import java.util.Optional; /** * Content 조회 Port (Redis 기반) */ public interface ContentReader { // 특정 이미지 조회 (Redis) Optional getImage(Long eventDraftId, String style, String platform); // 이벤트의 모든 이미지 조회 (Redis) List getImagesByEventId(Long eventDraftId); } ``` **3) JobWriter.java 수정** ```java package com.kt.event.content.biz.usecase.out; import com.kt.event.content.biz.dto.RedisJobData; /** * Job 상태 저장 Port (Redis 기반) */ public interface JobWriter { // Job 생성 (Redis) void saveJob(RedisJobData jobData, long ttlSeconds); // Job 상태 업데이트 (Redis) void updateJobStatus(String jobId, String status, Integer progress); // Job 결과 업데이트 (Redis) void updateJobResult(String jobId, String resultMessage); // Job 에러 업데이트 (Redis) void updateJobError(String jobId, String errorMessage); } ``` **4) JobReader.java 수정** ```java package com.kt.event.content.biz.usecase.out; import com.kt.event.content.biz.dto.RedisJobData; import java.util.Optional; /** * Job 상태 조회 Port (Redis 기반) */ public interface JobReader { // Job 조회 (Redis) Optional getJob(String jobId); } ``` #### 2.2.5 Service Layer 수정 **파일**: `content-service/src/main/java/com/kt/event/content/biz/service/` **주요 변경사항**: 1. JPA Repository 의존성 제거 2. RedisGateway 사용으로 변경 3. 도메인 Entity → DTO 변환 로직 추가 **예시: ContentServiceImpl.java** ```java @Service @RequiredArgsConstructor public class ContentServiceImpl implements ContentService { // ❌ 삭제: private final ContentRepository contentRepository; // ✅ 추가: private final RedisGateway redisGateway; private final ContentWriter contentWriter; // Redis 기반 private final ContentReader contentReader; // Redis 기반 @Override public List getImagesByEventId(Long eventDraftId) { List redisData = contentReader.getImagesByEventId(eventDraftId); return redisData.stream() .map(this::toImageInfo) .collect(Collectors.toList()); } private ImageInfo toImageInfo(RedisImageData data) { return ImageInfo.builder() .id(data.getId()) .eventDraftId(data.getEventDraftId()) .style(data.getStyle()) .platform(data.getPlatform()) .cdnUrl(data.getCdnUrl()) .prompt(data.getPrompt()) .selected(data.getSelected()) .createdAt(data.getCreatedAt()) .updatedAt(data.getUpdatedAt()) .build(); } } ``` #### 2.2.6 설정 파일 수정 **1) application-local.yml 수정 후** ```yaml spring: # ❌ 삭제: datasource, h2, jpa 설정 data: redis: repositories: enabled: false host: localhost port: 6379 autoconfigure: exclude: - org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration - org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration server: port: 8084 logging: level: com.kt.event: DEBUG ``` **2) build.gradle 수정** ```gradle dependencies { // ❌ 삭제 // implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // runtimeOnly 'com.h2database:h2' // runtimeOnly 'org.postgresql:postgresql' // ✅ 유지 implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'io.lettuce:lettuce-core' // 기타 의존성 유지 implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' } ``` --- ## 3. Redis Key 구조 설계 ### 3.1 이미지 데이터 **Key Pattern**: `content:image:{eventDraftId}:{style}:{platform}` **예시**: ``` content:image:1:FANCY:INSTAGRAM content:image:1:SIMPLE:KAKAO ``` **Data Type**: String (JSON) **Value 예시**: ```json { "id": 1, "eventDraftId": 1, "style": "FANCY", "platform": "INSTAGRAM", "cdnUrl": "https://cdn.azure.com/images/1/fancy_instagram_7ada8bd3.png", "prompt": "Mock prompt for FANCY style on INSTAGRAM platform", "selected": true, "createdAt": "2025-10-23T21:52:57.524759", "updatedAt": "2025-10-23T21:52:57.524759" } ``` **TTL**: 7일 (604800초) ### 3.2 Job 상태 **Key Pattern**: `job:{jobId}` **예시**: ``` job:job-mock-7ada8bd3 job:job-regen-df2bb3a3 ``` **Data Type**: Hash **Fields**: ``` id: "job-mock-7ada8bd3" eventDraftId: "1" jobType: "image-generation" status: "COMPLETED" progress: "100" resultMessage: "4개의 이미지가 성공적으로 생성되었습니다." errorMessage: null createdAt: "2025-10-23T21:52:57.511438" updatedAt: "2025-10-23T21:52:58.571923" ``` **TTL**: 1시간 (3600초) ### 3.3 AI 이벤트 데이터 (읽기 전용) **Key Pattern**: `ai:event:{eventDraftId}` **예시**: ``` ai:event:1 ``` **Data Type**: Hash **Fields** (AI Service가 저장): ``` eventDraftId: "1" eventTitle: "Mock 이벤트 제목 1" eventDescription: "Mock 이벤트 설명입니다." targetAudience: "20-30대 여성" eventObjective: "신규 고객 유치" ``` **TTL**: 24시간 (86400초) --- ## 4. 마이그레이션 전략 ### 4.1 단계별 마이그레이션 **Phase 1: Redis 구현 추가** (기존 JPA 유지) 1. RedisImageData, RedisJobData DTO 생성 2. RedisGateway에 이미지/Job CRUD 메서드 추가 3. MockRedisGateway 확장 4. 단위 테스트 작성 및 검증 **Phase 2: Service Layer 전환** 1. 새로운 Port Interface 생성 (Redis 기반) 2. Service에서 Redis Port 사용하도록 수정 3. 통합 테스트로 기능 검증 **Phase 3: JPA 제거** 1. Entity, Repository, Adapter 파일 삭제 2. JPA 설정 및 의존성 제거 3. 전체 테스트 재실행 **Phase 4: 문서화 및 배포** 1. API 테스트 결과서 업데이트 2. 수정 내역 commit & push 3. Production 배포 ### 4.2 롤백 전략 각 Phase마다 별도 branch 생성: ``` feature/content-redis-phase1 feature/content-redis-phase2 feature/content-redis-phase3 ``` 문제 발생 시 이전 Phase branch로 롤백 가능 --- ## 5. 테스트 계획 ### 5.1 단위 테스트 **RedisGatewayTest.java**: ```java @Test void saveAndGetImage_성공() { // Given RedisImageData imageData = RedisImageData.builder() .id(1L) .eventDraftId(1L) .style(ImageStyle.FANCY) .platform(Platform.INSTAGRAM) .cdnUrl("https://cdn.azure.com/test.png") .build(); // When redisGateway.saveImage(imageData, 604800); Optional result = redisGateway.getImage(1L, ImageStyle.FANCY, Platform.INSTAGRAM); // Then assertThat(result).isPresent(); assertThat(result.get().getCdnUrl()).isEqualTo("https://cdn.azure.com/test.png"); } ``` ### 5.2 통합 테스트 **ContentServiceIntegrationTest.java**: ```java @SpringBootTest @Testcontainers class ContentServiceIntegrationTest { @Container static GenericContainer redis = new GenericContainer<>("redis:7.2") .withExposedPorts(6379); @Test void 이미지_생성_및_조회_전체_플로우() { // 1. AI 이벤트 데이터 Redis 저장 (Mock) // 2. 이미지 생성 Job 요청 // 3. Job 상태 폴링 // 4. 이미지 조회 // 5. 검증 } } ``` ### 5.3 API 테스트 기존 test-backend.md의 7개 API 테스트 재실행: 1. POST /content/images/generate 2. GET /content/images/jobs/{jobId} 3. GET /content/events/{eventDraftId} 4. GET /content/events/{eventDraftId}/images 5. GET /content/images/{imageId} 6. POST /content/images/{imageId}/regenerate 7. DELETE /content/images/{imageId} **예상 결과**: 모든 API 정상 동작 (Redis 기반) --- ## 6. 성능 및 용량 산정 ### 6.1 Redis 메모리 사용량 **이미지 데이터**: - 1개 이미지: 약 0.5KB (JSON) - 1개 이벤트당 이미지: 최대 9개 (3 style × 3 platform) - 1개 이벤트당 용량: 4.5KB **Job 데이터**: - 1개 Job: 약 1KB (Hash) - 동시 처리 Job: 최대 50개 - Job 총 용량: 50KB **예상 총 메모리**: - 동시 이벤트 50개 × 4.5KB = 225KB - Job 50KB - 버퍼 (20%): 55KB - **총 메모리**: 약 330KB (여유 충분) ### 6.2 TTL 전략 | 데이터 타입 | TTL | 이유 | |------------|-----|------| | 이미지 URL | 7일 (604800초) | 이벤트 기간 동안 재사용 | | Job 상태 | 1시간 (3600초) | 완료 후 빠른 정리 | | AI 이벤트 데이터 | 24시간 (86400초) | AI Service 관리 | --- ## 7. 체크리스트 ### 7.1 구현 체크리스트 - [ ] RedisImageData DTO 생성 - [ ] RedisJobData DTO 생성 - [ ] RedisAIEventData DTO 생성 - [ ] RedisGateway 이미지 CRUD 메서드 추가 - [ ] RedisGateway Job 상태 관리 메서드 추가 - [ ] MockRedisGateway 확장 - [ ] Port Interface 수정 (ContentWriter, ContentReader, JobWriter, JobReader) - [ ] Service Layer JPA → Redis 전환 - [ ] JPA Entity 파일 삭제 - [ ] JPA Repository 파일 삭제 - [ ] application-local.yml H2/JPA 설정 제거 - [ ] build.gradle JPA/H2/PostgreSQL 의존성 제거 - [ ] 단위 테스트 작성 - [ ] 통합 테스트 작성 - [ ] API 테스트 재실행 (7개 엔드포인트) ### 7.2 검증 체크리스트 - [ ] Redis 연결 정상 동작 확인 - [ ] 이미지 저장/조회 정상 동작 - [ ] Job 상태 업데이트 정상 동작 - [ ] TTL 자동 만료 확인 - [ ] 모든 API 테스트 통과 (100%) - [ ] 서버 기동 시 에러 없음 - [ ] JPA 관련 로그 완전히 사라짐 ### 7.3 문서화 체크리스트 - [ ] 수정 계획안 작성 완료 (이 문서) - [ ] API 테스트 결과서 업데이트 - [ ] Redis Key 구조 문서화 - [ ] 개발 가이드 업데이트 --- ## 8. 예상 이슈 및 대응 방안 ### 8.1 Redis 장애 시 대응 **문제**: Redis 서버 다운 시 서비스 중단 **대응 방안**: - **Local/Test**: MockRedisGateway로 대체 (자동) - **Production**: Redis Sentinel을 통한 자동 Failover - **Circuit Breaker**: Redis 실패 시 임시 In-Memory 캐시 사용 ### 8.2 TTL 만료 후 데이터 복구 **문제**: 이미지 URL이 TTL 만료로 삭제됨 **대응 방안**: - **Event Service가 최종 승인 시**: Redis → Event DB 영구 저장 (논리 아키텍처 설계) - **TTL 연장 API**: 필요 시 TTL 연장 가능한 API 제공 - **이미지 재생성 API**: 이미 구현되어 있음 (POST /content/images/{id}/regenerate) ### 8.3 ID 생성 전략 **문제**: RDB auto-increment 없이 ID 생성 필요 **대응 방안**: - **이미지 ID**: Redis INCR 명령으로 순차 ID 생성 ``` INCR content:image:id:counter ``` - **Job ID**: UUID 기반 (기존 방식 유지) ```java String jobId = "job-mock-" + UUID.randomUUID().toString().substring(0, 8); ``` --- ## 9. 결론 ### 9.1 수정 필요성 Content Service는 논리 아키텍처 설계에 따라 **Redis를 주 저장소로 사용**해야 하며, RDB (H2/PostgreSQL)는 사용하지 않아야 합니다. 현재 구현은 설계와 불일치하므로 전면 수정이 필요합니다. ### 9.2 기대 효과 **아키텍처 준수**: - ✅ 논리 아키텍처 설계 100% 준수 - ✅ Redis 단독 저장소 전략 - ✅ 불필요한 RDB 의존성 제거 **성능 개선**: - ✅ 메모리 기반 Redis로 응답 속도 향상 - ✅ TTL 자동 만료로 메모리 관리 최적화 **운영 간소화**: - ✅ Content Service DB 운영 불필요 - ✅ 백업/복구 절차 간소화 ### 9.3 다음 단계 1. **승인 요청**: 이 수정 계획안 검토 및 승인 2. **Phase 1 착수**: Redis 구현 추가 (기존 코드 유지) 3. **단계별 진행**: Phase 1 → 2 → 3 순차 진행 4. **테스트 및 배포**: 각 Phase마다 검증 후 다음 단계 진행 --- **문서 버전**: 1.0 **최종 수정일**: 2025-10-24 **작성자**: Backend Developer