kt-event-marketing/design/backend/class/content-service.puml
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

529 lines
18 KiB
Plaintext

@startuml
!theme mono
title Content Service - 클래스 다이어그램 (Clean Architecture)
' ============================================
' Domain Layer (엔티티 및 비즈니스 로직)
' ============================================
package "com.kt.event.content.biz.domain" <<Rectangle>> {
class Content {
- id: Long
- eventId: String
- eventTitle: String
- eventDescription: String
- images: List<GeneratedImage>
- createdAt: LocalDateTime
- updatedAt: LocalDateTime
+ addImage(image: GeneratedImage): void
+ getSelectedImages(): List<GeneratedImage>
+ getImagesByStyle(style: ImageStyle): List<GeneratedImage>
+ getImagesByPlatform(platform: Platform): List<GeneratedImage>
}
class GeneratedImage {
- id: Long
- eventId: String
- style: ImageStyle
- platform: Platform
- cdnUrl: String
- prompt: String
- selected: boolean
- createdAt: LocalDateTime
- updatedAt: LocalDateTime
+ select(): void
+ deselect(): void
}
class Job {
- id: String
- eventId: String
- jobType: String
- status: Status
- progress: int
- resultMessage: String
- errorMessage: String
- createdAt: LocalDateTime
- updatedAt: LocalDateTime
+ start(): void
+ updateProgress(progress: int): void
+ complete(resultMessage: String): void
+ fail(errorMessage: String): void
+ isProcessing(): boolean
+ isCompleted(): boolean
+ isFailed(): boolean
}
enum JobStatus {
PENDING
PROCESSING
COMPLETED
FAILED
}
enum ImageStyle {
FANCY
SIMPLE
TRENDY
+ getDescription(): String
}
enum Platform {
INSTAGRAM
FACEBOOK
KAKAO
BLOG
+ getWidth(): int
+ getHeight(): int
+ getAspectRatio(): String
}
}
' ============================================
' Application Layer (Use Cases)
' ============================================
package "com.kt.event.content.biz.usecase" <<Rectangle>> {
package "in" {
interface GenerateImagesUseCase {
+ execute(command: GenerateImagesCommand): JobInfo
}
interface GetJobStatusUseCase {
+ execute(jobId: String): JobInfo
}
interface GetEventContentUseCase {
+ execute(eventId: String): ContentInfo
}
interface GetImageListUseCase {
+ execute(eventId: String, style: ImageStyle, platform: Platform): List<ImageInfo>
}
interface GetImageDetailUseCase {
+ execute(imageId: Long): ImageInfo
}
interface RegenerateImageUseCase {
+ execute(command: RegenerateImageCommand): JobInfo
}
interface DeleteImageUseCase {
+ execute(imageId: Long): void
}
}
package "out" {
interface ContentReader {
+ findByEventDraftIdWithImages(eventId: String): Optional<Content>
+ findImageById(imageId: Long): Optional<GeneratedImage>
+ findImagesByEventDraftId(eventId: String): List<GeneratedImage>
}
interface ContentWriter {
+ save(content: Content): Content
+ saveImage(image: GeneratedImage): GeneratedImage
+ getImageById(imageId: Long): GeneratedImage
+ deleteImageById(imageId: Long): void
}
interface ImageReader {
+ getImage(eventId: String, style: ImageStyle, platform: Platform): Optional<RedisImageData>
+ getImagesByEventId(eventId: String): List<RedisImageData>
}
interface ImageWriter {
+ saveImage(image: GeneratedImage): GeneratedImage
+ getImageById(imageId: Long): GeneratedImage
+ deleteImageById(imageId: Long): void
}
interface JobReader {
+ getJob(jobId: String): Optional<RedisJobData>
}
interface JobWriter {
+ saveJob(jobData: RedisJobData, ttlSeconds: long): void
+ updateJobStatus(jobId: String, status: String, progress: Integer): void
+ updateJobResult(jobId: String, resultMessage: String): void
+ updateJobError(jobId: String, errorMessage: String): void
}
interface RedisAIDataReader {
+ getAIRecommendation(eventId: String): Optional<Map<String, Object>>
}
interface RedisImageWriter {
+ cacheImages(eventId: String, images: List<GeneratedImage>, ttlSeconds: long): void
}
interface CDNUploader {
+ upload(imageData: byte[], fileName: String): String
}
interface ImageGeneratorCaller {
+ generateImage(prompt: String, width: int, height: int): String
}
}
}
' ============================================
' Application Service Layer
' ============================================
package "com.kt.event.content.biz.service" <<Rectangle>> {
class StableDiffusionImageGenerator implements GenerateImagesUseCase {
- replicateClient: ReplicateApiClient
- cdnUploader: CDNUploader
- jobWriter: JobWriter
- contentWriter: ContentWriter
- circuitBreaker: CircuitBreaker
- modelVersion: String
+ execute(command: GenerateImagesCommand): JobInfo
- processImageGeneration(jobId: String, command: GenerateImagesCommand): void
- generateImage(prompt: String, platform: Platform): String
- waitForCompletion(predictionId: String): String
- buildPrompt(command: GenerateImagesCommand, style: ImageStyle, platform: Platform): String
- downloadImage(imageUrl: String): byte[]
- createPredictionWithCircuitBreaker(request: ReplicateRequest): ReplicateResponse
- getPredictionWithCircuitBreaker(predictionId: String): ReplicateResponse
}
class JobManagementService implements GetJobStatusUseCase {
- jobReader: JobReader
+ execute(jobId: String): JobInfo
}
class GetEventContentService implements GetEventContentUseCase {
- contentReader: ContentReader
- redisAIDataReader: RedisAIDataReader
+ execute(eventId: String): ContentInfo
}
class GetImageListService implements GetImageListUseCase {
- imageReader: ImageReader
+ execute(eventId: String, style: ImageStyle, platform: Platform): List<ImageInfo>
}
class GetImageDetailService implements GetImageDetailUseCase {
- imageReader: ImageReader
+ execute(imageId: Long): ImageInfo
}
class RegenerateImageService implements RegenerateImageUseCase {
- imageReader: ImageReader
- imageWriter: ImageWriter
- jobWriter: JobWriter
- cdnUploader: CDNUploader
- replicateClient: ReplicateApiClient
+ execute(command: RegenerateImageCommand): JobInfo
}
class DeleteImageService implements DeleteImageUseCase {
- imageWriter: ImageWriter
+ execute(imageId: Long): void
}
}
' ============================================
' DTO Layer
' ============================================
package "com.kt.event.content.biz.dto" <<Rectangle>> {
class ContentCommand
class GenerateImagesCommand {
- eventId: String
- eventTitle: String
- eventDescription: String
- industry: String
- location: String
- trends: List<String>
- styles: List<ImageStyle>
- platforms: List<Platform>
}
class RegenerateImageCommand {
- imageId: Long
- newPrompt: String
}
class ContentInfo {
- id: Long
- eventId: String
- eventTitle: String
- eventDescription: String
- images: List<ImageInfo>
- createdAt: LocalDateTime
- updatedAt: LocalDateTime
+ {static} from(content: Content): ContentInfo
}
class ImageInfo {
- id: Long
- eventId: String
- style: ImageStyle
- platform: Platform
- cdnUrl: String
- prompt: String
- selected: boolean
- createdAt: LocalDateTime
- updatedAt: LocalDateTime
+ {static} from(image: GeneratedImage): ImageInfo
}
class JobInfo {
- id: String
- eventId: String
- jobType: String
- status: String
- progress: int
- resultMessage: String
- errorMessage: String
- createdAt: LocalDateTime
- updatedAt: LocalDateTime
+ {static} from(job: Job): JobInfo
}
class RedisJobData {
- id: String
- eventId: String
- jobType: String
- status: String
- progress: int
- resultMessage: String
- errorMessage: String
- createdAt: LocalDateTime
- updatedAt: LocalDateTime
}
class RedisImageData {
- eventId: String
- style: ImageStyle
- platform: Platform
- imageUrl: String
- prompt: String
- createdAt: LocalDateTime
}
class RedisAIEventData {
- eventId: String
- recommendedStyles: List<String>
- recommendedKeywords: List<String>
- cachedAt: LocalDateTime
}
}
' ============================================
' Infrastructure Layer (Gateway & Adapter)
' ============================================
package "com.kt.event.content.infra.gateway" <<Rectangle>> {
class RedisGateway implements ContentReader, ContentWriter, ImageReader, ImageWriter, JobReader, JobWriter, RedisAIDataReader, RedisImageWriter {
- redisTemplate: RedisTemplate<String, Object>
- objectMapper: ObjectMapper
- nextContentId: Long
- nextImageId: Long
+ getAIRecommendation(eventId: String): Optional<Map<String, Object>>
+ cacheImages(eventId: String, images: List<GeneratedImage>, ttlSeconds: long): void
+ saveImage(imageData: RedisImageData, ttlSeconds: long): void
+ getImage(eventId: String, style: ImageStyle, platform: Platform): Optional<RedisImageData>
+ getImagesByEventId(eventId: String): List<RedisImageData>
+ deleteImage(eventId: String, style: ImageStyle, platform: Platform): void
+ saveImages(eventId: String, images: List<RedisImageData>, ttlSeconds: long): void
+ saveJob(jobData: RedisJobData, ttlSeconds: long): void
+ getJob(jobId: String): Optional<RedisJobData>
+ updateJobStatus(jobId: String, status: String, progress: Integer): void
+ updateJobResult(jobId: String, resultMessage: String): void
+ updateJobError(jobId: String, errorMessage: String): void
+ findByEventDraftIdWithImages(eventId: String): Optional<Content>
+ findImageById(imageId: Long): Optional<GeneratedImage>
+ findImagesByEventDraftId(eventId: String): List<GeneratedImage>
+ save(content: Content): Content
+ saveImage(image: GeneratedImage): GeneratedImage
+ getImageById(imageId: Long): GeneratedImage
+ deleteImageById(imageId: Long): void
- buildImageKey(eventId: String, style: ImageStyle, platform: Platform): String
- getString(map: Map<Object, Object>, key: String): String
- getLong(map: Map<Object, Object>, key: String): Long
- getInteger(map: Map<Object, Object>, key: String): Integer
- getLocalDateTime(map: Map<Object, Object>, key: String): LocalDateTime
}
package "client" {
class ReplicateApiClient {
- apiToken: String
- baseUrl: String
- restClient: RestClient
+ createPrediction(request: ReplicateRequest): ReplicateResponse
+ getPrediction(predictionId: String): ReplicateResponse
}
class AzureBlobStorageUploader implements CDNUploader {
- connectionString: String
- containerName: String
- circuitBreaker: CircuitBreaker
- blobServiceClient: BlobServiceClient
- containerClient: BlobContainerClient
+ init(): void
+ upload(imageData: byte[], fileName: String): String
- doUpload(imageData: byte[], fileName: String): String
- generateBlobName(fileName: String): String
}
class ReplicateRequest {
- version: String
- input: Input
}
class ReplicateInputRequest {
- prompt: String
- negativePrompt: String
- width: int
- height: int
- numOutputs: int
- guidanceScale: double
- numInferenceSteps: int
- seed: long
}
class ReplicateResponse {
- id: String
- status: String
- output: List<String>
- error: String
}
class ReplicateApiConfig {
- apiToken: String
- baseUrl: String
+ restClient(): RestClient
}
}
}
' ============================================
' Presentation Layer (REST Controller)
' ============================================
package "com.kt.event.content.infra.web.controller" <<Rectangle>> {
class ContentController {
- generateImagesUseCase: GenerateImagesUseCase
- getJobStatusUseCase: GetJobStatusUseCase
- getEventContentUseCase: GetEventContentUseCase
- getImageListUseCase: GetImageListUseCase
- getImageDetailUseCase: GetImageDetailUseCase
- regenerateImageUseCase: RegenerateImageUseCase
- deleteImageUseCase: DeleteImageUseCase
+ generateImages(command: GenerateImagesCommand): ResponseEntity<JobInfo>
+ getJobStatus(jobId: String): ResponseEntity<JobInfo>
+ getContentByEventId(eventId: String): ResponseEntity<ContentInfo>
+ getImages(eventId: String, style: String, platform: String): ResponseEntity<List<ImageInfo>>
+ getImageById(imageId: Long): ResponseEntity<ImageInfo>
+ deleteImage(imageId: Long): ResponseEntity<Void>
+ regenerateImage(imageId: Long, requestBody: RegenerateImageCommand): ResponseEntity<JobInfo>
}
}
' ============================================
' Configuration Layer
' ============================================
package "com.kt.event.content.infra.config" <<Rectangle>> {
class RedisConfig {
- host: String
- port: int
+ redisConnectionFactory(): RedisConnectionFactory
+ redisTemplate(): RedisTemplate<String, Object>
+ objectMapper(): ObjectMapper
}
class Resilience4jConfig {
+ replicateCircuitBreaker(): CircuitBreaker
+ azureCircuitBreaker(): CircuitBreaker
}
class SecurityConfig {
+ securityFilterChain(http: HttpSecurity): SecurityFilterChain
}
class SwaggerConfig {
+ openAPI(): OpenAPI
}
}
' ============================================
' 관계 정의 (Domain)
' ============================================
Content "1" o-- "0..*" GeneratedImage : contains
GeneratedImage --> ImageStyle : uses
GeneratedImage --> Platform : uses
Job --> JobStatus : has
' ============================================
' 관계 정의 (Service → Port)
' ============================================
StableDiffusionImageGenerator ..> CDNUploader
StableDiffusionImageGenerator ..> JobWriter
StableDiffusionImageGenerator ..> ContentWriter
StableDiffusionImageGenerator --> ReplicateApiClient
JobManagementService ..> JobReader
GetEventContentService ..> ContentReader
GetEventContentService ..> RedisAIDataReader
GetImageListService ..> ImageReader
GetImageDetailService ..> ImageReader
RegenerateImageService ..> ImageReader
RegenerateImageService ..> ImageWriter
RegenerateImageService ..> JobWriter
RegenerateImageService ..> CDNUploader
DeleteImageService ..> ImageWriter
' ============================================
' 관계 정의 (Gateway → Port Implementation)
' ============================================
RedisGateway ..|> ContentReader
RedisGateway ..|> ContentWriter
RedisGateway ..|> ImageReader
RedisGateway ..|> ImageWriter
RedisGateway ..|> JobReader
RedisGateway ..|> JobWriter
RedisGateway ..|> RedisAIDataReader
RedisGateway ..|> RedisImageWriter
AzureBlobStorageUploader ..|> CDNUploader
' ============================================
' 관계 정의 (Controller → UseCase)
' ============================================
ContentController ..> GenerateImagesUseCase
ContentController ..> GetJobStatusUseCase
ContentController ..> GetEventContentUseCase
ContentController ..> GetImageListUseCase
ContentController ..> GetImageDetailUseCase
ContentController ..> RegenerateImageUseCase
ContentController ..> DeleteImageUseCase
' ============================================
' 관계 정의 (DTO)
' ============================================
ContentInfo ..> ImageInfo
ContentCommand ..> GenerateImagesCommand : uses
ContentCommand ..> RegenerateImageCommand : uses
ReplicateRequest ..> ReplicateInputRequest : contains
' ============================================
' 레이어 노트
' ============================================
note top of Content : Domain Layer\n순수 비즈니스 로직\n외부 의존성 없음
note top of GenerateImagesUseCase : Application Layer (Input Port)\nUse Case 인터페이스
note top of ContentReader : Application Layer (Output Port)\n외부 시스템 의존성 추상화
note top of StableDiffusionImageGenerator : Application Service Layer\nUse Case 구현체\n비즈니스 로직 오케스트레이션
note top of RedisGateway : Infrastructure Layer\nOutput Port 구현체\nRedis 연동
note top of ContentController : Presentation Layer\nREST API 엔드포인트
note top of RedisConfig : Configuration Layer\n인프라 설정
@enduml