diff --git a/.run/analytics-service.run.xml b/.run/analytics-service.run.xml index 03891fe..b0a6a3f 100644 --- a/.run/analytics-service.run.xml +++ b/.run/analytics-service.run.xml @@ -5,11 +5,11 @@ - + - - - + + + 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 index 8d6910f..82263fd 100644 --- 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 @@ -5,6 +5,7 @@ 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.data.redis.core.RedisTemplate; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @@ -23,13 +24,14 @@ public class AnalyticsBatchScheduler { private final AnalyticsService analyticsService; private final EventStatsRepository eventStatsRepository; + private final RedisTemplate redisTemplate; /** * 5분 단위 Analytics 데이터 갱신 배치 * - * - 모든 활성 이벤트의 대시보드 데이터를 갱신 - * - 외부 API 호출을 통해 최신 데이터 수집 - * - Redis 캐시 업데이트 + * - 각 이벤트마다 Redis 캐시 확인 + * - 캐시 있음 → 건너뛰기 (1시간 유효) + * - 캐시 없음 → PostgreSQL + 외부 API → Redis 저장 */ @Scheduled(fixedRate = 300000) // 5분 = 300,000ms public void refreshAnalyticsDashboard() { @@ -40,30 +42,41 @@ public class AnalyticsBatchScheduler { List activeEvents = eventStatsRepository.findAll(); log.info("활성 이벤트 수: {}", activeEvents.size()); - // 2. 각 이벤트별로 대시보드 데이터 갱신 + // 2. 각 이벤트별로 캐시 확인 및 갱신 int successCount = 0; + int skipCount = 0; int failCount = 0; for (EventStats event : activeEvents) { + String cacheKey = "analytics:dashboard:" + event.getEventId(); + try { - log.debug("이벤트 데이터 갱신 시작: eventId={}, title={}", + // 2-1. Redis 캐시 확인 + if (redisTemplate.hasKey(cacheKey)) { + log.debug("✅ 캐시 유효, 건너뜀: eventId={}", event.getEventId()); + skipCount++; + continue; + } + + // 2-2. 캐시 없음 → 데이터 갱신 + log.info("캐시 만료, 갱신 시작: eventId={}, title={}", event.getEventId(), event.getEventTitle()); // refresh=true로 호출하여 캐시 갱신 및 외부 API 호출 analyticsService.getDashboardData(event.getEventId(), null, null, true); successCount++; - log.debug("이벤트 데이터 갱신 완료: eventId={}", event.getEventId()); + log.info("✅ 배치 갱신 완료: eventId={}", event.getEventId()); } catch (Exception e) { failCount++; - log.error("이벤트 데이터 갱신 실패: eventId={}, error={}", + log.error("❌ 배치 갱신 실패: eventId={}, error={}", event.getEventId(), e.getMessage(), e); } } - log.info("===== Analytics 배치 완료: 성공={}, 실패={}, 종료시각={} =====", - successCount, failCount, LocalDateTime.now()); + log.info("===== Analytics 배치 완료: 성공={}, 건너뜀={}, 실패={}, 종료시각={} =====", + successCount, skipCount, failCount, LocalDateTime.now()); } catch (Exception e) { log.error("Analytics 배치 실행 중 오류 발생: {}", e.getMessage(), e); diff --git a/analytics-service/src/main/java/com/kt/event/analytics/config/SampleDataLoader.java b/analytics-service/src/main/java/com/kt/event/analytics/config/SampleDataLoader.java index 634be54..6a13695 100644 --- a/analytics-service/src/main/java/com/kt/event/analytics/config/SampleDataLoader.java +++ b/analytics-service/src/main/java/com/kt/event/analytics/config/SampleDataLoader.java @@ -6,6 +6,7 @@ import com.kt.event.analytics.entity.TimelineData; import com.kt.event.analytics.repository.ChannelStatsRepository; import com.kt.event.analytics.repository.EventStatsRepository; import com.kt.event.analytics.repository.TimelineDataRepository; +import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.ApplicationArguments; @@ -14,6 +15,7 @@ import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; +import jakarta.annotation.PreDestroy; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.ArrayList; @@ -23,8 +25,8 @@ import java.util.Random; /** * 샘플 데이터 로더 * - * 애플리케이션 시작 시 대시보드 테스트를 위한 샘플 데이터를 자동으로 적재합니다. - * 모든 프로파일에서 실행되며, 기존 데이터가 있으면 건너뜁니다. + * - 서비스 시작 시: PostgreSQL 샘플 데이터 자동 생성 + * - 서비스 종료 시: PostgreSQL 전체 데이터 삭제 */ @Slf4j @Component @@ -34,6 +36,7 @@ public class SampleDataLoader implements ApplicationRunner { private final EventStatsRepository eventStatsRepository; private final ChannelStatsRepository channelStatsRepository; private final TimelineDataRepository timelineDataRepository; + private final EntityManager entityManager; private final Random random = new Random(); @@ -41,33 +44,42 @@ public class SampleDataLoader implements ApplicationRunner { @Transactional public void run(ApplicationArguments args) { log.info("========================================"); - log.info("샘플 데이터 적재 시작"); + log.info("🚀 서비스 시작: PostgreSQL 샘플 데이터 생성"); log.info("========================================"); - // 기존 샘플 데이터 확인 - if (eventStatsRepository.count() > 0) { - log.info("기존 데이터가 존재하여 샘플 데이터 적재를 건너뜁니다."); - return; + // 항상 기존 데이터 삭제 후 새로 생성 + long existingCount = eventStatsRepository.count(); + if (existingCount > 0) { + log.info("기존 데이터 {} 건 삭제 중...", existingCount); + timelineDataRepository.deleteAll(); + channelStatsRepository.deleteAll(); + eventStatsRepository.deleteAll(); + + // 삭제 커밋 보장 + entityManager.flush(); + entityManager.clear(); + + log.info("✅ 기존 데이터 삭제 완료"); } try { // 1. 이벤트 통계 데이터 생성 List eventStatsList = createEventStats(); eventStatsRepository.saveAll(eventStatsList); - log.info("이벤트 통계 데이터 적재 완료: {} 건", eventStatsList.size()); + log.info("✅ 이벤트 통계 데이터 적재 완료: {} 건", eventStatsList.size()); // 2. 채널별 통계 데이터 생성 List channelStatsList = createChannelStats(eventStatsList); channelStatsRepository.saveAll(channelStatsList); - log.info("채널별 통계 데이터 적재 완료: {} 건", channelStatsList.size()); + log.info("✅ 채널별 통계 데이터 적재 완료: {} 건", channelStatsList.size()); // 3. 타임라인 데이터 생성 List timelineDataList = createTimelineData(eventStatsList); timelineDataRepository.saveAll(timelineDataList); - log.info("타임라인 데이터 적재 완료: {} 건", timelineDataList.size()); + log.info("✅ 타임라인 데이터 적재 완료: {} 건", timelineDataList.size()); log.info("========================================"); - log.info("샘플 데이터 적재 완료!"); + log.info("🎉 샘플 데이터 적재 완료!"); log.info("========================================"); log.info("테스트 가능한 이벤트:"); eventStatsList.forEach(event -> @@ -80,6 +92,40 @@ public class SampleDataLoader implements ApplicationRunner { } } + /** + * 서비스 종료 시 전체 데이터 삭제 + */ + @PreDestroy + @Transactional + public void onShutdown() { + log.info("========================================"); + log.info("🛑 서비스 종료: PostgreSQL 전체 데이터 삭제"); + log.info("========================================"); + + try { + long timelineCount = timelineDataRepository.count(); + long channelCount = channelStatsRepository.count(); + long eventCount = eventStatsRepository.count(); + + log.info("삭제 대상: 이벤트={}, 채널={}, 타임라인={}", + eventCount, channelCount, timelineCount); + + timelineDataRepository.deleteAll(); + channelStatsRepository.deleteAll(); + eventStatsRepository.deleteAll(); + + // 삭제 커밋 보장 + entityManager.flush(); + entityManager.clear(); + + log.info("✅ 모든 샘플 데이터 삭제 완료!"); + log.info("========================================"); + + } catch (Exception e) { + log.error("샘플 데이터 삭제 중 오류 발생", e); + } + } + /** * 이벤트 통계 샘플 데이터 생성 */ 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 e1d31b1..0969741 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 @@ -41,7 +41,7 @@ public class AnalyticsService { private final ObjectMapper objectMapper; private static final String CACHE_KEY_PREFIX = "analytics:dashboard:"; - private static final long CACHE_TTL = 3600; // 1시간 + private static final long CACHE_TTL = 3600; // 1시간 (단일 캐시) /** * 대시보드 데이터 조회 @@ -57,12 +57,12 @@ public class AnalyticsService { String cacheKey = CACHE_KEY_PREFIX + eventId; - // 캐시 조회 (refresh가 false일 때만) + // 1. Redis 캐시 조회 (refresh가 false일 때만) if (!refresh) { String cachedData = redisTemplate.opsForValue().get(cacheKey); if (cachedData != null) { try { - log.debug("캐시 HIT: {}", cacheKey); + log.info("✅ 캐시 HIT: {} (1시간 캐시)", cacheKey); return objectMapper.readValue(cachedData, AnalyticsDashboardResponse.class); } catch (JsonProcessingException e) { log.warn("캐시 데이터 역직렬화 실패: {}", e.getMessage()); @@ -70,34 +70,37 @@ public class AnalyticsService { } } - // 캐시 MISS: 데이터 통합 작업 - log.debug("캐시 MISS 또는 refresh=true: 데이터 통합 작업 시작"); + // 2. 캐시 MISS: 데이터 통합 작업 + log.info("캐시 MISS 또는 refresh=true: PostgreSQL + 외부 API 호출"); - // 1. Analytics DB 조회 + // 2-1. Analytics DB 조회 (PostgreSQL) EventStats eventStats = eventStatsRepository.findByEventId(eventId) .orElseThrow(() -> new BusinessException(ErrorCode.EVENT_001)); List channelStatsList = channelStatsRepository.findByEventId(eventId); + log.debug("PostgreSQL 조회 완료: eventId={}, 채널 수={}", eventId, channelStatsList.size()); - // 2. 외부 채널 API 병렬 호출 (Circuit Breaker 적용) - // TODO: refresh가 true일 때만 외부 API 호출하도록 개선 필요 - // 현재는 샘플 데이터 사용을 위해 주석 처리 - // if (refresh) { - // externalChannelService.updateChannelStatsFromExternalAPIs(eventId, channelStatsList); - // } + // 2-2. 외부 채널 API 병렬 호출 (Circuit Breaker 적용) + try { + externalChannelService.updateChannelStatsFromExternalAPIs(eventId, channelStatsList); + log.info("외부 API 호출 성공: eventId={}", eventId); + } catch (Exception e) { + log.warn("외부 API 호출 실패, PostgreSQL 샘플 데이터 사용: eventId={}, error={}", + eventId, e.getMessage()); + // Fallback: PostgreSQL 샘플 데이터만 사용 + } // 3. 대시보드 데이터 구성 AnalyticsDashboardResponse response = buildDashboardData(eventStats, channelStatsList, startDate, endDate); - // 4. Redis 캐싱 (읽기 전용 오류 시 무시) + // 4. Redis 캐싱 (1시간 TTL) try { String jsonData = objectMapper.writeValueAsString(response); redisTemplate.opsForValue().set(cacheKey, jsonData, CACHE_TTL, TimeUnit.SECONDS); - log.debug("캐시 저장 완료: {}", cacheKey); + log.info("✅ Redis 캐시 저장 완료: {} (TTL: 1시간)", cacheKey); } catch (JsonProcessingException e) { log.warn("캐시 데이터 직렬화 실패: {}", e.getMessage()); } catch (Exception e) { - // Redis 읽기 전용 오류 등 캐시 저장 실패 시 무시하고 계속 진행 log.warn("캐시 저장 실패 (무시하고 계속 진행): {}", e.getMessage()); }