kt-event-marketing/develop/dev/content-service-modification-plan.md
cherry2250 5e9e1759ce Content Service Phase 2: Port 인터페이스 구현 및 Gateway 통합
Phase 2 작업으로 Clean Architecture의 의존성 역전 원칙을 적용하여
Service 계층이 Port 인터페이스에만 의존하도록 구조를 개선했습니다.

주요 변경사항:
1. Redis DTO 생성 (Phase 1)
   - RedisAIEventData: AI 이벤트 데이터 DTO
   - RedisImageData: 이미지 데이터 DTO
   - RedisJobData: Job 데이터 DTO

2. Port 인터페이스 생성
   - ImageWriter: 이미지 저장 Port
   - ImageReader: 이미지 조회 Port
   - JobWriter: Job 저장 Port
   - JobReader: Job 조회 Port

3. Gateway 구현
   - RedisGateway: 4개 Port 인터페이스 구현 (Production용)
   - MockRedisGateway: 4개 Port 인터페이스 구현 (Local/Test용)
   - JobGateway: 2개 Port 인터페이스 구현 + @Primary 추가 (Phase 3 삭제 예정)

4. 하위 호환성 유지
   - Port 인터페이스에 레거시 메서드 추가 (save, findById)
   - Service 계층 코드 변경 없이 점진적 마이그레이션
   - "Phase 3에서 삭제 예정" 주석 표시

검증 완료:
- 컴파일 성공
- 서비스 정상 시작 (포트 8084)
- API 정상 작동 확인

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 10:14:54 +09:00

20 KiB
Raw Blame History

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 엔티티 사용

// 현재 구현 (잘못됨)
@Entity
public class Content { ... }

@Entity
public class GeneratedImage { ... }

@Entity
public class Job { ... }

문제 3: JPA Repository 사용

// 현재 구현 (잘못됨)
public interface ContentRepository extends JpaRepository<Content, Long> { ... }
public interface GeneratedImageRepository extends JpaRepository<GeneratedImage, Long> { ... }
public interface JobRepository extends JpaRepository<Job, String> { ... }

문제 4: application-local.yml 설정

# 현재 구현 (잘못됨)
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 (새로 생성)

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 (새로 생성)

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 (새로 생성 - 읽기 전용)

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<String, Object> additionalData;  // AI가 생성한 추가 데이터
}

2.2.2 Redis Gateway 확장

파일: content-service/src/main/java/com/kt/event/content/infra/gateway/RedisGateway.java

추가 메서드:

// 이미지 CRUD
void saveImage(RedisImageData imageData, long ttlSeconds);
Optional<RedisImageData> getImage(Long eventDraftId, ImageStyle style, Platform platform);
List<RedisImageData> getImagesByEventId(Long eventDraftId);
void deleteImage(Long eventDraftId, ImageStyle style, Platform platform);

// Job 상태 관리
void saveJob(RedisJobData jobData, long ttlSeconds);
Optional<RedisJobData> 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<Map<String, Object>> 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 수정

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<RedisImageData> images, long ttlSeconds);
}

2) ContentReader.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<RedisImageData> getImage(Long eventDraftId, String style, String platform);

    // 이벤트의 모든 이미지 조회 (Redis)
    List<RedisImageData> getImagesByEventId(Long eventDraftId);
}

3) JobWriter.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 수정

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<RedisJobData> 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

@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<ImageInfo> getImagesByEventId(Long eventDraftId) {
        List<RedisImageData> 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 수정 후

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 수정

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 예시:

{
  "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:

@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<RedisImageData> 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:

@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 기반 (기존 방식 유지)
    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