From 21b8fe5efbe1821a9e9b8a54843e93bff5ecb1ac Mon Sep 17 00:00:00 2001 From: Hyowon Yang Date: Fri, 24 Oct 2025 14:26:11 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B0=B0=EC=B9=98-redis,db=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .run/analytics-service.run.xml | 8 +-- .../batch/AnalyticsBatchScheduler.java | 31 ++++++--- .../analytics/config/SampleDataLoader.java | 68 ++++++++++++++++--- .../analytics/service/AnalyticsService.java | 33 +++++---- 4 files changed, 101 insertions(+), 39 deletions(-) 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()); }