@startuml !theme mono title AI Service 클래스 다이어그램 (Clean Architecture) ' ===== Presentation Layer (Interface Adapters) ===== package "com.kt.ai.controller" <> #E8F5E9 { class HealthController { + checkHealth(): ResponseEntity - getServiceStatus(): ServiceStatus - checkRedisConnection(): boolean } class InternalRecommendationController { - aiRecommendationService: AIRecommendationService - cacheService: CacheService - redisTemplate: RedisTemplate + getRecommendation(eventId: String): ResponseEntity + debugRedisKeys(): ResponseEntity> + debugRedisKey(key: String): ResponseEntity> + searchAllDatabases(): ResponseEntity> + createTestData(eventId: String): ResponseEntity> } class InternalJobController { - jobStatusService: JobStatusService - cacheService: CacheService + getJobStatus(jobId: String): ResponseEntity + createTestJob(jobId: String): ResponseEntity> } } ' ===== Application Layer (Use Cases) ===== package "com.kt.ai.service" <> #FFF9C4 { class AIRecommendationService { - cacheService: CacheService - jobStatusService: JobStatusService - trendAnalysisService: TrendAnalysisService - claudeApiClient: ClaudeApiClient - circuitBreakerManager: CircuitBreakerManager - fallback: AIServiceFallback - objectMapper: ObjectMapper - aiProvider: String - apiKey: String - anthropicVersion: String - model: String - maxTokens: Integer - temperature: Double + getRecommendation(eventId: String): AIRecommendationResult + generateRecommendations(message: AIJobMessage): void - analyzeTrend(message: AIJobMessage): TrendAnalysis - createRecommendations(message: AIJobMessage, trendAnalysis: TrendAnalysis): List - callClaudeApiForRecommendations(message: AIJobMessage, trendAnalysis: TrendAnalysis): List - buildRecommendationPrompt(message: AIJobMessage, trendAnalysis: TrendAnalysis): String - parseRecommendationResponse(responseText: String): List - parseEventRecommendation(node: JsonNode): EventRecommendation - parseRange(node: JsonNode): ExpectedMetrics.Range - extractJsonFromMarkdown(text: String): String } class TrendAnalysisService { - claudeApiClient: ClaudeApiClient - circuitBreakerManager: CircuitBreakerManager - fallback: AIServiceFallback - objectMapper: ObjectMapper - apiKey: String - anthropicVersion: String - model: String - maxTokens: Integer - temperature: Double + analyzeTrend(industry: String, region: String): TrendAnalysis - callClaudeApi(industry: String, region: String): TrendAnalysis - buildPrompt(industry: String, region: String): String - parseResponse(responseText: String): TrendAnalysis - extractJsonFromMarkdown(text: String): String - parseTrendKeywords(arrayNode: JsonNode): List } class JobStatusService { - cacheService: CacheService - objectMapper: ObjectMapper + getJobStatus(jobId: String): JobStatusResponse + updateJobStatus(jobId: String, status: JobStatus, message: String): void - calculateProgress(status: JobStatus): int } class CacheService { - redisTemplate: RedisTemplate - recommendationTtl: long - jobStatusTtl: long - trendTtl: long + set(key: String, value: Object, ttlSeconds: long): void + get(key: String): Object + delete(key: String): void + saveJobStatus(jobId: String, status: Object): void + getJobStatus(jobId: String): Object + saveRecommendation(eventId: String, recommendation: Object): void + getRecommendation(eventId: String): Object + saveTrend(industry: String, region: String, trend: Object): void + getTrend(industry: String, region: String): Object } } ' ===== Domain Layer (Entities & Business Logic) ===== package "com.kt.ai.model" <> #E1BEE7 { package "dto.response" { class AIRecommendationResult { - eventId: String - trendAnalysis: TrendAnalysis - recommendations: List - generatedAt: LocalDateTime - expiresAt: LocalDateTime - aiProvider: AIProvider } class TrendAnalysis { - industryTrends: List - regionalTrends: List - seasonalTrends: List } class "TrendAnalysis.TrendKeyword" as TrendKeyword { - keyword: String - relevance: Double - description: String } class EventRecommendation { - optionNumber: Integer - concept: String - title: String - description: String - targetAudience: String - duration: Duration - mechanics: Mechanics - promotionChannels: List - estimatedCost: EstimatedCost - expectedMetrics: ExpectedMetrics - differentiator: String } class "EventRecommendation.Duration" as Duration { - recommendedDays: Integer - recommendedPeriod: String } class "EventRecommendation.Mechanics" as Mechanics { - type: EventMechanicsType - details: String } class "EventRecommendation.EstimatedCost" as EstimatedCost { - min: Integer - max: Integer - breakdown: Map } class ExpectedMetrics { - newCustomers: Range - revenueIncrease: Range - roi: Range } class "ExpectedMetrics.Range" as Range { - min: Double - max: Double } class JobStatusResponse { - jobId: String - status: JobStatus - progress: Integer - message: String - createdAt: LocalDateTime } class HealthCheckResponse { - status: ServiceStatus - timestamp: LocalDateTime - redisConnected: boolean } class ErrorResponse { - success: boolean - errorCode: String - message: String - timestamp: LocalDateTime - details: Map } } package "enums" { enum AIProvider { CLAUDE GPT_4 } enum JobStatus { PENDING PROCESSING COMPLETED FAILED } enum EventMechanicsType { DISCOUNT GIFT STAMP EXPERIENCE LOTTERY COMBO } enum ServiceStatus { UP DOWN DEGRADED } enum CircuitBreakerState { CLOSED OPEN HALF_OPEN } } } ' ===== Infrastructure Layer (External Interfaces) ===== package "com.kt.ai.client" <> #FFCCBC { interface ClaudeApiClient { + sendMessage(apiKey: String, anthropicVersion: String, request: ClaudeRequest): ClaudeResponse } package "dto" { class ClaudeRequest { - model: String - messages: List - maxTokens: Integer - temperature: Double - system: String } class "ClaudeRequest.Message" as Message { - role: String - content: String } class ClaudeResponse { - id: String - type: String - role: String - content: List - model: String - stopReason: String - stopSequence: String - usage: Usage + extractText(): String } class "ClaudeResponse.Content" as Content { - type: String - text: String } class "ClaudeResponse.Usage" as Usage { - inputTokens: Integer - outputTokens: Integer } } package "config" { class FeignClientConfig { + feignEncoder(): Encoder + feignDecoder(): Decoder + feignLoggerLevel(): Logger.Level + feignErrorDecoder(): ErrorDecoder } } } package "com.kt.ai.circuitbreaker" <> #FFCCBC { class CircuitBreakerManager { - circuitBreakerRegistry: CircuitBreakerRegistry + executeWithCircuitBreaker(circuitBreakerName: String, supplier: Supplier, fallback: Supplier): T + executeWithCircuitBreaker(circuitBreakerName: String, supplier: Supplier): T + getCircuitBreakerState(circuitBreakerName: String): CircuitBreaker.State } package "fallback" { class AIServiceFallback { + getDefaultTrendAnalysis(industry: String, region: String): TrendAnalysis + getDefaultRecommendations(objective: String, industry: String): List - createDefaultTrendKeyword(keyword: String, relevance: Double, description: String): TrendAnalysis.TrendKeyword - createDefaultRecommendation(optionNumber: Integer, concept: String, title: String): EventRecommendation } } } package "com.kt.ai.kafka" <> #FFCCBC { package "consumer" { class AIJobConsumer { - aiRecommendationService: AIRecommendationService - objectMapper: ObjectMapper + consumeAIJobMessage(message: String): void - parseAIJobMessage(message: String): AIJobMessage } } package "message" { class AIJobMessage { - jobId: String - eventId: String - storeName: String - industry: String - region: String - objective: String - targetAudience: String - budget: Integer - requestedAt: LocalDateTime } } } ' ===== Exception Layer ===== package "com.kt.ai.exception" <> #FFEBEE { class GlobalExceptionHandler { + handleBusinessException(e: BusinessException): ResponseEntity + handleJobNotFoundException(e: JobNotFoundException): ResponseEntity + handleRecommendationNotFoundException(e: RecommendationNotFoundException): ResponseEntity + handleCircuitBreakerOpenException(e: CircuitBreakerOpenException): ResponseEntity + handleAIServiceException(e: AIServiceException): ResponseEntity + handleException(e: Exception): ResponseEntity - buildErrorResponse(errorCode: String, message: String): ErrorResponse } class AIServiceException { - errorCode: String - details: String + AIServiceException(message: String) + AIServiceException(message: String, cause: Throwable) + AIServiceException(errorCode: String, message: String) + AIServiceException(errorCode: String, message: String, details: String) } class JobNotFoundException { - jobId: String + JobNotFoundException(jobId: String) + JobNotFoundException(jobId: String, cause: Throwable) } class RecommendationNotFoundException { - eventId: String + RecommendationNotFoundException(eventId: String) + RecommendationNotFoundException(eventId: String, cause: Throwable) } class CircuitBreakerOpenException { - circuitBreakerName: String + CircuitBreakerOpenException(circuitBreakerName: String) + CircuitBreakerOpenException(circuitBreakerName: String, cause: Throwable) } } ' ===== Configuration Layer ===== package "com.kt.ai.config" <> #E3F2FD { class SecurityConfig { + securityFilterChain(http: HttpSecurity): SecurityFilterChain + passwordEncoder(): PasswordEncoder } class RedisConfig { - host: String - port: int - password: String + redisConnectionFactory(): LettuceConnectionFactory + redisTemplate(): RedisTemplate } class CircuitBreakerConfig { + circuitBreakerRegistry(): CircuitBreakerRegistry + circuitBreakerConfigCustomizer(): Customizer } class KafkaConsumerConfig { - bootstrapServers: String - groupId: String + consumerFactory(): ConsumerFactory + kafkaListenerContainerFactory(): ConcurrentKafkaListenerContainerFactory } class JacksonConfig { + objectMapper(): ObjectMapper } class SwaggerConfig { + openAPI(): OpenAPI } } ' ===== Main Application ===== package "com.kt.ai" <> #F3E5F5 { class AiServiceApplication { + {static} main(args: String[]): void } } ' ===== 관계 정의 ===== ' Controller → Service InternalRecommendationController --> AIRecommendationService : uses InternalRecommendationController --> CacheService : uses InternalJobController --> JobStatusService : uses InternalJobController --> CacheService : uses ' Service → Service AIRecommendationService --> TrendAnalysisService : uses AIRecommendationService --> JobStatusService : uses AIRecommendationService --> CacheService : uses JobStatusService --> CacheService : uses ' Service → Client AIRecommendationService --> ClaudeApiClient : uses TrendAnalysisService --> ClaudeApiClient : uses ' Service → CircuitBreaker AIRecommendationService --> CircuitBreakerManager : uses AIRecommendationService --> AIServiceFallback : uses TrendAnalysisService --> CircuitBreakerManager : uses TrendAnalysisService --> AIServiceFallback : uses ' Kafka → Service AIJobConsumer --> AIRecommendationService : uses AIJobConsumer --> AIJobMessage : uses ' Service → Domain AIRecommendationService --> AIRecommendationResult : creates AIRecommendationService --> EventRecommendation : creates TrendAnalysisService --> TrendAnalysis : creates JobStatusService --> JobStatusResponse : creates ' Domain Relationships AIRecommendationResult *-- TrendAnalysis AIRecommendationResult *-- EventRecommendation AIRecommendationResult --> AIProvider TrendAnalysis *-- TrendKeyword EventRecommendation *-- Duration EventRecommendation *-- Mechanics EventRecommendation *-- EstimatedCost EventRecommendation *-- ExpectedMetrics Mechanics --> EventMechanicsType ExpectedMetrics *-- Range JobStatusResponse --> JobStatus HealthCheckResponse --> ServiceStatus ' Client Relationships ClaudeApiClient --> ClaudeRequest : uses ClaudeApiClient --> ClaudeResponse : returns ClaudeRequest *-- Message ClaudeResponse *-- Content ClaudeResponse *-- Usage ' Exception Relationships GlobalExceptionHandler ..> ErrorResponse : creates GlobalExceptionHandler ..> AIServiceException : handles GlobalExceptionHandler ..> JobNotFoundException : handles GlobalExceptionHandler ..> RecommendationNotFoundException : handles GlobalExceptionHandler ..> CircuitBreakerOpenException : handles AIServiceException <|-- JobNotFoundException AIServiceException <|-- RecommendationNotFoundException AIServiceException <|-- CircuitBreakerOpenException note top of AiServiceApplication Spring Boot Application Entry Point - @SpringBootApplication - @EnableFeignClients - @EnableKafka end note note top of AIRecommendationService **Use Case (Application Layer)** - AI 추천 생성 비즈니스 로직 - 트렌드 분석 → 추천안 생성 - Circuit Breaker 적용 - Redis 캐싱 전략 end note note top of TrendAnalysisService **Use Case (Application Layer)** - Claude AI를 통한 트렌드 분석 - 업종/지역/계절 트렌드 - Circuit Breaker Fallback end note note top of ClaudeApiClient **Infrastructure (External Interface)** - Feign Client - Claude API 연동 - HTTP 통신 처리 end note note top of CircuitBreakerManager **Infrastructure (Resilience)** - Resilience4j Circuit Breaker - 외부 API 장애 격리 - Fallback 메커니즘 end note note top of CacheService **Infrastructure (Data Access)** - Redis 캐싱 인프라 - TTL 관리 - 추천/트렌드/상태 캐싱 end note note right of AIRecommendationResult **Domain Entity** - AI 추천 결과 - 불변 객체 (Immutable) - Builder 패턴 end note note right of TrendAnalysis **Domain Entity** - 트렌드 분석 결과 - 업종/지역/계절별 구분 end note @enduml