From c6b33885e0fad3d0918074853632578dafb74e96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=B8=EC=9B=90?= Date: Thu, 30 Oct 2025 16:44:23 +0900 Subject: [PATCH] =?UTF-8?q?AI=20Service=20Security=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EB=8B=A8=EC=88=9C=ED=99=94=20=EB=B0=8F=20=EC=9B=8C=ED=81=AC?= =?UTF-8?q?=ED=94=8C=EB=A1=9C=EC=9A=B0=20=EB=AC=B8=EC=84=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SecurityConfig CORS 설정 제거 및 단순화 - 모든 요청 허용으로 변경 (내부 API 특성 반영) - DevTools 요청 정적 리소스 제외 처리 - AI Service 워크플로우 문서 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../java/com/kt/ai/config/SecurityConfig.java | 35 +- develop/dev/ai-service-workflow.md | 390 ++++++++++++++++++ 2 files changed, 403 insertions(+), 22 deletions(-) create mode 100644 develop/dev/ai-service-workflow.md diff --git a/ai-service/src/main/java/com/kt/ai/config/SecurityConfig.java b/ai-service/src/main/java/com/kt/ai/config/SecurityConfig.java index 08e9b2e..298aebf 100644 --- a/ai-service/src/main/java/com/kt/ai/config/SecurityConfig.java +++ b/ai-service/src/main/java/com/kt/ai/config/SecurityConfig.java @@ -4,6 +4,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; @@ -27,21 +28,19 @@ import java.util.List; @EnableWebSecurity public class SecurityConfig { - /** - * Security Filter Chain 설정 - * - 모든 요청 허용 (내부 API) - * - CSRF 비활성화 - * - Stateless 세션 - */ @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http + // CSRF 비활성화 (REST API는 CSRF 불필요) .csrf(AbstractHttpConfigurer::disable) - .cors(cors -> cors.configurationSource(corsConfigurationSource())) - .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + + // 세션 사용 안 함 (JWT 기반 인증) + .sessionManagement(session -> + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + + // 모든 요청 허용 (테스트용) .authorizeHttpRequests(auth -> auth - .requestMatchers("/health", "/actuator/**", "/v3/api-docs/**", "/swagger-ui/**").permitAll() - .requestMatchers("/internal/**").permitAll() // Internal API .anyRequest().permitAll() ); @@ -49,19 +48,11 @@ public class SecurityConfig { } /** - * CORS 설정 + * Chrome DevTools 요청 등 정적 리소스 요청을 Spring Security에서 제외 */ @Bean - public CorsConfigurationSource corsConfigurationSource() { - CorsConfiguration configuration = new CorsConfiguration(); - configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000", "http://localhost:8080")); - configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")); - configuration.setAllowedHeaders(List.of("*")); - configuration.setAllowCredentials(true); - configuration.setMaxAge(3600L); - - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", configuration); - return source; + public WebSecurityCustomizer webSecurityCustomizer() { + return (web) -> web.ignoring() + .requestMatchers("/.well-known/**"); } } diff --git a/develop/dev/ai-service-workflow.md b/develop/dev/ai-service-workflow.md new file mode 100644 index 0000000..c8796a3 --- /dev/null +++ b/develop/dev/ai-service-workflow.md @@ -0,0 +1,390 @@ +# AI Service 전체 메서드 워크플로우 + +## 1. AI 추천 생성 워크플로우 (Kafka 기반 비동기) + +```mermaid +sequenceDiagram + participant ES as Event Service + participant Kafka as Kafka Topic + participant Consumer as AIJobConsumer + participant ARS as AIRecommendationService + participant JSS as JobStatusService + participant TAS as TrendAnalysisService + participant CS as CacheService + participant CAC as ClaudeApiClient + participant Claude as Claude API + participant Redis as Redis + + %% 1. Event Service가 Kafka 메시지 발행 + ES->>Kafka: Publish AIJobMessage
(ai-event-generation-job topic) + + %% 2. Kafka Consumer가 메시지 수신 + Kafka->>Consumer: consume(AIJobMessage) + Note over Consumer: @KafkaListener
groupId: ai-service-consumers + + %% 3. AI 추천 생성 시작 + Consumer->>ARS: generateRecommendations(message) + activate ARS + + %% 4. Job 상태: PROCESSING (10%) + ARS->>JSS: updateJobStatus(jobId, PROCESSING, "트렌드 분석 중") + JSS->>CS: saveJobStatus(jobId, status) + CS->>Redis: SET ai:job:status:{jobId} + + %% 5. 트렌드 분석 + ARS->>ARS: analyzeTrend(message) + ARS->>CS: getTrend(industry, region) + CS->>Redis: GET ai:trend:{industry}:{region} + + alt 캐시 HIT + Redis-->>CS: TrendAnalysis (cached) + CS-->>ARS: TrendAnalysis + else 캐시 MISS + ARS->>TAS: analyzeTrend(industry, region) + activate TAS + + %% Circuit Breaker 적용 + TAS->>TAS: circuitBreakerManager.executeWithCircuitBreaker() + TAS->>CAC: sendMessage(apiKey, version, request) + CAC->>Claude: POST /v1/messages + Note over Claude: Model: claude-sonnet-4-5
System: "트렌드 분석 전문가"
Prompt: 업종/지역/계절 트렌드 + Claude-->>CAC: ClaudeResponse + CAC-->>TAS: ClaudeResponse + + TAS->>TAS: parseResponse(responseText) + TAS-->>ARS: TrendAnalysis + deactivate TAS + + %% 트렌드 캐싱 + ARS->>CS: saveTrend(industry, region, analysis) + CS->>Redis: SET ai:trend:{industry}:{region} (TTL: 1시간) + end + + %% 6. Job 상태: PROCESSING (50%) + ARS->>JSS: updateJobStatus(jobId, PROCESSING, "이벤트 추천안 생성 중") + JSS->>CS: saveJobStatus(jobId, status) + CS->>Redis: SET ai:job:status:{jobId} + + %% 7. 이벤트 추천안 생성 + ARS->>ARS: createRecommendations(message, trendAnalysis) + ARS->>ARS: circuitBreakerManager.executeWithCircuitBreaker() + ARS->>CAC: sendMessage(apiKey, version, request) + CAC->>Claude: POST /v1/messages + Note over Claude: Model: claude-sonnet-4-5
System: "이벤트 기획 전문가"
Prompt: 3가지 추천안 생성 + Claude-->>CAC: ClaudeResponse + CAC-->>ARS: ClaudeResponse + + ARS->>ARS: parseRecommendationResponse(responseText) + + %% 8. Job 상태: PROCESSING (90%) + ARS->>JSS: updateJobStatus(jobId, PROCESSING, "결과 저장 중") + JSS->>CS: saveJobStatus(jobId, status) + CS->>Redis: SET ai:job:status:{jobId} + + %% 9. 결과 저장 + ARS->>CS: saveRecommendation(eventId, result) + CS->>Redis: SET ai:recommendation:{eventId} (TTL: 24시간) + + %% 10. Job 상태: COMPLETED (100%) + ARS->>JSS: updateJobStatus(jobId, COMPLETED, "AI 추천 완료") + JSS->>CS: saveJobStatus(jobId, status) + CS->>Redis: SET ai:job:status:{jobId} + + deactivate ARS + + %% 11. Kafka ACK + Consumer->>Kafka: acknowledgment.acknowledge() +``` + +--- + +## 2. Job 상태 조회 워크플로우 (동기) + +```mermaid +sequenceDiagram + participant ES as Event Service + participant Controller as InternalJobController + participant JSS as JobStatusService + participant CS as CacheService + participant Redis as Redis + + %% 1. Event Service가 Job 상태 조회 + ES->>Controller: GET /api/v1/ai-service/internal/jobs/{jobId}/status + + %% 2. Job 상태 조회 + Controller->>JSS: getJobStatus(jobId) + activate JSS + + JSS->>CS: getJobStatus(jobId) + CS->>Redis: GET ai:job:status:{jobId} + + alt 상태 존재 + Redis-->>CS: JobStatusResponse + CS-->>JSS: Object (JobStatusResponse) + JSS->>JSS: objectMapper.convertValue() + JSS-->>Controller: JobStatusResponse + Controller-->>ES: 200 OK + JobStatusResponse + else 상태 없음 + Redis-->>CS: null + CS-->>JSS: null + JSS-->>Controller: JobNotFoundException + Controller-->>ES: 404 Not Found + end + + deactivate JSS +``` + +--- + +## 3. AI 추천 결과 조회 워크플로우 (동기) + +```mermaid +sequenceDiagram + participant ES as Event Service + participant Controller as InternalRecommendationController + participant ARS as AIRecommendationService + participant CS as CacheService + participant Redis as Redis + + %% 1. Event Service가 AI 추천 결과 조회 + ES->>Controller: GET /api/v1/ai-service/internal/recommendations/{eventId} + + %% 2. 추천 결과 조회 + Controller->>ARS: getRecommendation(eventId) + activate ARS + + ARS->>CS: getRecommendation(eventId) + CS->>Redis: GET ai:recommendation:{eventId} + + alt 결과 존재 + Redis-->>CS: AIRecommendationResult + CS-->>ARS: Object (AIRecommendationResult) + ARS->>ARS: objectMapper.convertValue() + ARS-->>Controller: AIRecommendationResult + Controller-->>ES: 200 OK + AIRecommendationResult + else 결과 없음 + Redis-->>CS: null + CS-->>ARS: null + ARS-->>Controller: RecommendationNotFoundException + Controller-->>ES: 404 Not Found + end + + deactivate ARS +``` + +--- + +## 4. 헬스체크 워크플로우 (동기) + +```mermaid +sequenceDiagram + participant Client as Client/Actuator + participant Controller as HealthController + participant Redis as Redis + + %% 1. 헬스체크 요청 + Client->>Controller: GET /api/v1/ai-service/health + + %% 2. Redis 상태 확인 + Controller->>Controller: checkRedis() + + alt RedisTemplate 존재 + Controller->>Redis: PING + alt Redis 정상 + Redis-->>Controller: PONG + Controller->>Controller: redisStatus = UP + else Redis 오류 + Redis-->>Controller: Exception + Controller->>Controller: redisStatus = DOWN + end + else RedisTemplate 없음 + Controller->>Controller: redisStatus = UNKNOWN + end + + %% 3. 전체 상태 판단 + alt Redis DOWN + Controller->>Controller: overallStatus = DEGRADED + else Redis UP/UNKNOWN + Controller->>Controller: overallStatus = UP + end + + %% 4. 응답 + Controller-->>Client: 200 OK + HealthCheckResponse +``` + +--- + +## 5. 주요 컴포넌트 메서드 목록 + +### 5.1 Controller Layer + +#### InternalJobController +| 메서드 | HTTP | 엔드포인트 | 설명 | +|--------|------|-----------|------| +| `getJobStatus(jobId)` | GET | `/api/v1/ai-service/internal/jobs/{jobId}/status` | Job 상태 조회 | +| `createTestJob(jobId)` | GET | `/api/v1/ai-service/internal/jobs/debug/create-test-job/{jobId}` | 테스트 Job 생성 (디버그) | + +#### InternalRecommendationController +| 메서드 | HTTP | 엔드포인트 | 설명 | +|--------|------|-----------|------| +| `getRecommendation(eventId)` | GET | `/api/v1/ai-service/internal/recommendations/{eventId}` | AI 추천 결과 조회 | +| `debugRedisKeys()` | GET | `/api/v1/ai-service/internal/recommendations/debug/redis-keys` | Redis 모든 키 조회 | +| `debugRedisKey(key)` | GET | `/api/v1/ai-service/internal/recommendations/debug/redis-key/{key}` | Redis 특정 키 조회 | +| `searchAllDatabases()` | GET | `/api/v1/ai-service/internal/recommendations/debug/search-all-databases` | 전체 DB 검색 | +| `createTestData(eventId)` | GET | `/api/v1/ai-service/internal/recommendations/debug/create-test-data/{eventId}` | 테스트 데이터 생성 | + +#### HealthController +| 메서드 | HTTP | 엔드포인트 | 설명 | +|--------|------|-----------|------| +| `healthCheck()` | GET | `/api/v1/ai-service/health` | 서비스 헬스체크 | +| `checkRedis()` | - | (내부) | Redis 연결 확인 | + +--- + +### 5.2 Service Layer + +#### AIRecommendationService +| 메서드 | 호출자 | 설명 | +|--------|-------|------| +| `getRecommendation(eventId)` | Controller | Redis에서 추천 결과 조회 | +| `generateRecommendations(message)` | AIJobConsumer | AI 추천 생성 (전체 프로세스) | +| `analyzeTrend(message)` | 내부 | 트렌드 분석 (캐시 확인 포함) | +| `createRecommendations(message, trendAnalysis)` | 내부 | 이벤트 추천안 생성 | +| `callClaudeApiForRecommendations(message, trendAnalysis)` | 내부 | Claude API 호출 (추천안) | +| `buildRecommendationPrompt(message, trendAnalysis)` | 내부 | 추천안 프롬프트 생성 | +| `parseRecommendationResponse(responseText)` | 내부 | 추천안 응답 파싱 | +| `parseEventRecommendation(node)` | 내부 | EventRecommendation 파싱 | +| `parseRange(node)` | 내부 | Range 객체 파싱 | +| `extractJsonFromMarkdown(text)` | 내부 | Markdown에서 JSON 추출 | + +#### TrendAnalysisService +| 메서드 | 호출자 | 설명 | +|--------|-------|------| +| `analyzeTrend(industry, region)` | AIRecommendationService | 트렌드 분석 수행 | +| `callClaudeApi(industry, region)` | 내부 | Claude API 호출 (트렌드) | +| `buildPrompt(industry, region)` | 내부 | 트렌드 분석 프롬프트 생성 | +| `parseResponse(responseText)` | 내부 | 트렌드 응답 파싱 | +| `extractJsonFromMarkdown(text)` | 내부 | Markdown에서 JSON 추출 | +| `parseTrendKeywords(arrayNode)` | 내부 | TrendKeyword 리스트 파싱 | + +#### JobStatusService +| 메서드 | 호출자 | 설명 | +|--------|-------|------| +| `getJobStatus(jobId)` | Controller | Job 상태 조회 | +| `updateJobStatus(jobId, status, message)` | AIRecommendationService | Job 상태 업데이트 | +| `calculateProgress(status)` | 내부 | 상태별 진행률 계산 | + +#### CacheService +| 메서드 | 호출자 | 설명 | +|--------|-------|------| +| `set(key, value, ttlSeconds)` | 내부 | 범용 캐시 저장 | +| `get(key)` | 내부 | 범용 캐시 조회 | +| `delete(key)` | 외부 | 캐시 삭제 | +| `saveJobStatus(jobId, status)` | JobStatusService | Job 상태 저장 | +| `getJobStatus(jobId)` | JobStatusService | Job 상태 조회 | +| `saveRecommendation(eventId, recommendation)` | AIRecommendationService | AI 추천 결과 저장 | +| `getRecommendation(eventId)` | AIRecommendationService | AI 추천 결과 조회 | +| `saveTrend(industry, region, trend)` | AIRecommendationService | 트렌드 분석 결과 저장 | +| `getTrend(industry, region)` | AIRecommendationService | 트렌드 분석 결과 조회 | + +--- + +### 5.3 Consumer Layer + +#### AIJobConsumer +| 메서드 | 트리거 | 설명 | +|--------|-------|------| +| `consume(message, topic, offset, ack)` | Kafka Message | Kafka 메시지 수신 및 처리 | + +--- + +### 5.4 Client Layer + +#### ClaudeApiClient (Feign) +| 메서드 | 호출자 | 설명 | +|--------|-------|------| +| `sendMessage(apiKey, anthropicVersion, request)` | TrendAnalysisService, AIRecommendationService | Claude API 호출 | + +--- + +## 6. Redis 캐시 키 구조 + +| 키 패턴 | 설명 | TTL | +|--------|------|-----| +| `ai:job:status:{jobId}` | Job 상태 정보 | 24시간 (86400초) | +| `ai:recommendation:{eventId}` | AI 추천 결과 | 24시간 (86400초) | +| `ai:trend:{industry}:{region}` | 트렌드 분석 결과 | 1시간 (3600초) | + +--- + +## 7. Claude API 호출 정보 + +### 7.1 트렌드 분석 +- **URL**: `https://api.anthropic.com/v1/messages` +- **Model**: `claude-sonnet-4-5-20250929` +- **Max Tokens**: 4096 +- **Temperature**: 0.7 +- **System Prompt**: "당신은 마케팅 트렌드 분석 전문가입니다. 업종별, 지역별 트렌드를 분석하고 인사이트를 제공합니다." +- **응답 형식**: JSON (industryTrends, regionalTrends, seasonalTrends) + +### 7.2 이벤트 추천안 생성 +- **URL**: `https://api.anthropic.com/v1/messages` +- **Model**: `claude-sonnet-4-5-20250929` +- **Max Tokens**: 4096 +- **Temperature**: 0.7 +- **System Prompt**: "당신은 소상공인을 위한 마케팅 이벤트 기획 전문가입니다. 트렌드 분석을 바탕으로 실행 가능한 이벤트 추천안을 제공합니다." +- **응답 형식**: JSON (recommendations: 3가지 옵션) + +--- + +## 8. Circuit Breaker 설정 + +### 적용 대상 +- `claudeApi`: 모든 Claude API 호출 + +### 설정값 +```yaml +failure-rate-threshold: 50% +slow-call-duration-threshold: 60초 +sliding-window-size: 10 +minimum-number-of-calls: 5 +wait-duration-in-open-state: 60초 +timeout-duration: 300초 (5분) +``` + +### Fallback 메서드 +- `AIServiceFallback.getDefaultTrendAnalysis()`: 기본 트렌드 분석 +- `AIServiceFallback.getDefaultRecommendations()`: 기본 추천안 + +--- + +## 9. 에러 처리 + +### Exception 종류 +| Exception | HTTP Code | 발생 조건 | +|-----------|-----------|---------| +| `RecommendationNotFoundException` | 404 | Redis에 추천 결과 없음 | +| `JobNotFoundException` | 404 | Redis에 Job 상태 없음 | +| `AIServiceException` | 500 | AI 서비스 내부 오류 | + +### 에러 응답 예시 +```json +{ + "timestamp": "2025-10-30T15:30:00", + "status": 404, + "error": "Not Found", + "message": "추천 결과를 찾을 수 없습니다: eventId=evt-123", + "path": "/api/v1/ai-service/internal/recommendations/evt-123" +} +``` + +--- + +## 10. 로깅 레벨 + +```yaml +com.kt.ai: DEBUG +org.springframework.kafka: INFO +org.springframework.data.redis: INFO +io.github.resilience4j: DEBUG +```