+
+
+
+
+
+ false
+ false
+
+
+
diff --git a/analytics-service/src/main/java/com/kt/event/analytics/AnalyticsServiceApplication.java b/analytics-service/src/main/java/com/kt/event/analytics/AnalyticsServiceApplication.java
index b0c2342..c109743 100644
--- a/analytics-service/src/main/java/com/kt/event/analytics/AnalyticsServiceApplication.java
+++ b/analytics-service/src/main/java/com/kt/event/analytics/AnalyticsServiceApplication.java
@@ -7,6 +7,7 @@ import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.kafka.annotation.EnableKafka;
+import org.springframework.scheduling.annotation.EnableScheduling;
/**
* Analytics Service 애플리케이션 메인 클래스
@@ -19,6 +20,7 @@ import org.springframework.kafka.annotation.EnableKafka;
@EnableJpaAuditing
@EnableFeignClients
@EnableKafka
+@EnableScheduling
public class AnalyticsServiceApplication {
public static void main(String[] args) {
diff --git a/analytics-service/src/main/java/com/kt/event/analytics/batch/AnalyticsBatchScheduler.java b/analytics-service/src/main/java/com/kt/event/analytics/batch/AnalyticsBatchScheduler.java
new file mode 100644
index 0000000..8d6910f
--- /dev/null
+++ b/analytics-service/src/main/java/com/kt/event/analytics/batch/AnalyticsBatchScheduler.java
@@ -0,0 +1,103 @@
+package com.kt.event.analytics.batch;
+
+import com.kt.event.analytics.entity.EventStats;
+import com.kt.event.analytics.repository.EventStatsRepository;
+import com.kt.event.analytics.service.AnalyticsService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * Analytics 배치 스케줄러
+ *
+ * 5분 단위로 Analytics 대시보드 데이터를 갱신하는 배치 작업
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class AnalyticsBatchScheduler {
+
+ private final AnalyticsService analyticsService;
+ private final EventStatsRepository eventStatsRepository;
+
+ /**
+ * 5분 단위 Analytics 데이터 갱신 배치
+ *
+ * - 모든 활성 이벤트의 대시보드 데이터를 갱신
+ * - 외부 API 호출을 통해 최신 데이터 수집
+ * - Redis 캐시 업데이트
+ */
+ @Scheduled(fixedRate = 300000) // 5분 = 300,000ms
+ public void refreshAnalyticsDashboard() {
+ log.info("===== Analytics 배치 시작: {} =====", LocalDateTime.now());
+
+ try {
+ // 1. 모든 활성 이벤트 조회
+ List activeEvents = eventStatsRepository.findAll();
+ log.info("활성 이벤트 수: {}", activeEvents.size());
+
+ // 2. 각 이벤트별로 대시보드 데이터 갱신
+ int successCount = 0;
+ int failCount = 0;
+
+ for (EventStats event : activeEvents) {
+ try {
+ log.debug("이벤트 데이터 갱신 시작: eventId={}, title={}",
+ event.getEventId(), event.getEventTitle());
+
+ // refresh=true로 호출하여 캐시 갱신 및 외부 API 호출
+ analyticsService.getDashboardData(event.getEventId(), null, null, true);
+
+ successCount++;
+ log.debug("이벤트 데이터 갱신 완료: eventId={}", event.getEventId());
+
+ } catch (Exception e) {
+ failCount++;
+ log.error("이벤트 데이터 갱신 실패: eventId={}, error={}",
+ event.getEventId(), e.getMessage(), e);
+ }
+ }
+
+ log.info("===== Analytics 배치 완료: 성공={}, 실패={}, 종료시각={} =====",
+ successCount, failCount, LocalDateTime.now());
+
+ } catch (Exception e) {
+ log.error("Analytics 배치 실행 중 오류 발생: {}", e.getMessage(), e);
+ }
+ }
+
+ /**
+ * 초기 데이터 로딩 (애플리케이션 시작 후 30초 뒤 1회 실행)
+ *
+ * - 서버 시작 직후 캐시 워밍업
+ * - 첫 API 요청 시 응답 시간 단축
+ */
+ @Scheduled(initialDelay = 30000, fixedDelay = Long.MAX_VALUE)
+ public void initialDataLoad() {
+ log.info("===== 초기 데이터 로딩 시작: {} =====", LocalDateTime.now());
+
+ try {
+ List allEvents = eventStatsRepository.findAll();
+ log.info("초기 로딩 대상 이벤트 수: {}", allEvents.size());
+
+ for (EventStats event : allEvents) {
+ try {
+ analyticsService.getDashboardData(event.getEventId(), null, null, true);
+ log.debug("초기 데이터 로딩 완료: eventId={}", event.getEventId());
+ } catch (Exception e) {
+ log.warn("초기 데이터 로딩 실패: eventId={}, error={}",
+ event.getEventId(), e.getMessage());
+ }
+ }
+
+ log.info("===== 초기 데이터 로딩 완료: {} =====", LocalDateTime.now());
+
+ } catch (Exception e) {
+ log.error("초기 데이터 로딩 중 오류 발생: {}", e.getMessage(), e);
+ }
+ }
+}
diff --git a/analytics-service/src/main/java/com/kt/event/analytics/config/RedisConfig.java b/analytics-service/src/main/java/com/kt/event/analytics/config/RedisConfig.java
index 29e6be5..5c6eebb 100644
--- a/analytics-service/src/main/java/com/kt/event/analytics/config/RedisConfig.java
+++ b/analytics-service/src/main/java/com/kt/event/analytics/config/RedisConfig.java
@@ -1,8 +1,11 @@
package com.kt.event.analytics.config;
+import io.lettuce.core.ReadFrom;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
+import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@@ -20,6 +23,13 @@ public class RedisConfig {
template.setValueSerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new StringRedisSerializer());
+
+ // Read-only 오류 방지: 마스터 노드 우선 사용
+ if (connectionFactory instanceof LettuceConnectionFactory) {
+ LettuceConnectionFactory lettuceFactory = (LettuceConnectionFactory) connectionFactory;
+ lettuceFactory.setValidateConnection(true);
+ }
+
return template;
}
}
diff --git a/analytics-service/src/main/java/com/kt/event/analytics/service/AnalyticsService.java b/analytics-service/src/main/java/com/kt/event/analytics/service/AnalyticsService.java
index 79ae326..e1d31b1 100644
--- a/analytics-service/src/main/java/com/kt/event/analytics/service/AnalyticsService.java
+++ b/analytics-service/src/main/java/com/kt/event/analytics/service/AnalyticsService.java
@@ -89,13 +89,16 @@ public class AnalyticsService {
// 3. 대시보드 데이터 구성
AnalyticsDashboardResponse response = buildDashboardData(eventStats, channelStatsList, startDate, endDate);
- // 4. Redis 캐싱
+ // 4. Redis 캐싱 (읽기 전용 오류 시 무시)
try {
String jsonData = objectMapper.writeValueAsString(response);
redisTemplate.opsForValue().set(cacheKey, jsonData, CACHE_TTL, TimeUnit.SECONDS);
log.debug("캐시 저장 완료: {}", cacheKey);
} catch (JsonProcessingException e) {
log.warn("캐시 데이터 직렬화 실패: {}", e.getMessage());
+ } catch (Exception e) {
+ // Redis 읽기 전용 오류 등 캐시 저장 실패 시 무시하고 계속 진행
+ log.warn("캐시 저장 실패 (무시하고 계속 진행): {}", e.getMessage());
}
return response;
diff --git a/analytics-service/src/main/resources/application.yml b/analytics-service/src/main/resources/application.yml
index 2be762a..ed32f2b 100644
--- a/analytics-service/src/main/resources/application.yml
+++ b/analytics-service/src/main/resources/application.yml
@@ -29,9 +29,9 @@ spring:
# Redis
data:
redis:
- host: ${REDIS_HOST:localhost}
+ host: ${REDIS_HOST:20.214.210.71}
port: ${REDIS_PORT:6379}
- password: ${REDIS_PASSWORD:}
+ password: ${REDIS_PASSWORD:Hi5Jessica!}
timeout: 2000ms
lettuce:
pool:
@@ -136,3 +136,10 @@ resilience4j:
failure-rate-threshold: 50
wait-duration-in-open-state: 30s
sliding-window-size: 10
+
+# Batch Scheduler
+batch:
+ analytics:
+ refresh-interval: ${BATCH_REFRESH_INTERVAL:300000} # 5분 (밀리초)
+ initial-delay: ${BATCH_INITIAL_DELAY:30000} # 30초 (밀리초)
+ enabled: ${BATCH_ENABLED:true} # 배치 활성화 여부
diff --git a/claude/make-run-profile.md b/claude/make-run-profile.md
new file mode 100644
index 0000000..f363a91
--- /dev/null
+++ b/claude/make-run-profile.md
@@ -0,0 +1,175 @@
+# 서비스실행파일작성가이드
+
+[요청사항]
+- <수행원칙>을 준용하여 수행
+- <수행순서>에 따라 수행
+- [결과파일] 안내에 따라 파일 작성
+
+[가이드]
+<수행원칙>
+- 설정 Manifest(src/main/resources/application*.yml)의 각 항목의 값은 하드코딩하지 않고 환경변수 처리
+- Kubernetes에 배포된 데이터베이스는 LoadBalacer유형의 Service를 만들어 연결
+- MQ 이용 시 'MQ설치결과서'의 연결 정보를 실행 프로파일의 환경변수로 등록
+<수행순서>
+- 준비:
+ - 데이터베이스설치결과서(develop/database/exec/db-exec-dev.md) 분석
+ - 캐시설치결과서(develop/database/exec/cache-exec-dev.md) 분석
+ - MQ설치결과서(develop/mq/mq-exec-dev.md) 분석 - 연결 정보 확인
+ - kubectl get svc -n tripgen-dev | grep LoadBalancer 실행하여 External IP 목록 확인
+- 실행:
+ - 각 서비스별를 서브에이젼트로 병렬 수행
+ - 설정 Manifest 수정
+ - 하드코딩 되어 있는 값이 있으면 환경변수로 변환
+ - 특히, 데이터베이스, MQ 등의 연결 정보는 반드시 환경변수로 변환해야 함
+ - 민감한 정보의 디퐅트값은 생략하거나 간략한 값으로 지정
+ - '<로그설정>'을 참조하여 Log 파일 설정
+ - '<실행프로파일 작성 가이드>'에 따라 서비스 실행프로파일 작성
+ - LoadBalancer External IP를 DB_HOST, REDIS_HOST로 설정
+ - MQ 연결 정보를 application.yml의 환경변수명에 맞춰 설정
+ - 서비스 실행 및 오류 수정
+ - 'IntelliJ서비스실행기'를 'tools' 디렉토리에 다운로드
+ - python 또는 python3 명령으로 백그라우드로 실행하고 결과 로그를 분석
+ nohup python3 tools/run-intellij-service-profile.py {service-name} > logs/{service-name}.log 2>&1 & echo "Started {service-name} with PID: $!"
+ - 서비스 실행은 다른 방법 사용하지 말고 **반드시 python 프로그램 이용**
+ - 오류 수정 후 필요 시 실행파일의 환경변수를 올바르게 변경
+ - 서비스 정상 시작 확인 후 서비스 중지
+ - 결과: {service-name}/.run
+<서비스 중지 방법>
+- Window
+ - netstat -ano | findstr :{PORT}
+ - powershell "Stop-Process -Id {Process number} -Force"
+- Linux/Mac
+ - netstat -ano | grep {PORT}
+ - kill -9 {Process number}
+<로그설정>
+- **application.yml 로그 파일 설정**:
+ ```yaml
+ logging:
+ file:
+ name: ${LOG_FILE:logs/trip-service.log}
+ logback:
+ rollingpolicy:
+ max-file-size: 10MB
+ max-history: 7
+ total-size-cap: 100MB
+ ```
+
+<실행프로파일 작성 가이드>
+- {service-name}/.run/{service-name}.run.xml 파일로 작성
+- Spring Boot가 아니고 **Gradle 실행 프로파일**이어야 함: '[실행프로파일 예시]' 참조
+- Kubernetes에 배포된 데이터베이스의 LoadBalancer Service 확인:
+ - kubectl get svc -n {namespace} | grep LoadBalancer 명령으로 LoadBalancer IP 확인
+ - 각 서비스별 데이터베이스의 LoadBalancer External IP를 DB_HOST로 사용
+ - 캐시(Redis)의 LoadBalancer External IP를 REDIS_HOST로 사용
+- MQ 연결 설정:
+ - MQ설치결과서(develop/mq/mq-exec-dev.md)에서 연결 정보 확인
+ - MQ 유형에 따른 연결 정보 설정 예시:
+ - RabbitMQ: RABBITMQ_HOST, RABBITMQ_PORT, RABBITMQ_USERNAME, RABBITMQ_PASSWORD
+ - Kafka: KAFKA_BOOTSTRAP_SERVERS, KAFKA_SECURITY_PROTOCOL
+ - Azure Service Bus: SERVICE_BUS_CONNECTION_STRING
+ - AWS SQS: AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
+ - Redis (Pub/Sub): REDIS_HOST, REDIS_PORT, REDIS_PASSWORD
+ - ActiveMQ: ACTIVEMQ_BROKER_URL, ACTIVEMQ_USER, ACTIVEMQ_PASSWORD
+ - 기타 MQ: 해당 MQ의 연결에 필요한 호스트, 포트, 인증정보, 연결문자열 등을 환경변수로 설정
+ - application.yml에 정의된 환경변수명 확인 후 매핑
+- 백킹서비스 연결 정보 매핑:
+ - 데이터베이스설치결과서에서 각 서비스별 DB 인증 정보 확인
+ - 캐시설치결과서에서 각 서비스별 Redis 인증 정보 확인
+ - LoadBalancer의 External IP를 호스트로 사용 (내부 DNS 아님)
+- 개발모드의 DDL_AUTO값은 update로 함
+- JWT Secret Key는 모든 서비스가 동일해야 함
+- application.yaml의 환경변수와 일치하도록 환경변수 설정
+- application.yaml의 민감 정보는 기본값으로 지정하지 않고 실제 백킹서비스 정보로 지정
+- 백킹서비스 연결 확인 결과를 바탕으로 정확한 값을 지정
+- 기존에 파일이 있으면 내용을 분석하여 항목 추가/수정/삭제
+
+[실행프로파일 예시]
+```
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+ false
+
+
+
+```
+
+[참고자료]
+- 데이터베이스설치결과서: develop/database/exec/db-exec-dev.md
+ - 각 서비스별 DB 연결 정보 (사용자명, 비밀번호, DB명)
+ - LoadBalancer Service External IP 목록
+- 캐시설치결과서: develop/database/exec/cache-exec-dev.md
+ - 각 서비스별 Redis 연결 정보
+ - LoadBalancer Service External IP 목록
+- MQ설치결과서: develop/mq/mq-exec-dev.md
+ - MQ 유형 및 연결 정보
+ - 연결에 필요한 호스트, 포트, 인증 정보
+ - LoadBalancer Service External IP (해당하는 경우)