mirror of
https://github.com/ktds-dg0501/kt-event-marketing.git
synced 2025-12-06 06:46:25 +00:00
배치-redis,db조회수정
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
b0b0ba3263
commit
21b8fe5efb
@ -5,11 +5,11 @@
|
||||
<map>
|
||||
<!-- Database Settings -->
|
||||
<entry key="DB_KIND" value="postgresql" />
|
||||
<entry key="DB_HOST" value="localhost" />
|
||||
<entry key="DB_HOST" value="4.230.49.9" />
|
||||
<entry key="DB_PORT" value="5432" />
|
||||
<entry key="DB_NAME" value="analytics_db" />
|
||||
<entry key="DB_USERNAME" value="analytics_user" />
|
||||
<entry key="DB_PASSWORD" value="analytics_pass" />
|
||||
<entry key="DB_NAME" value="analyticdb" />
|
||||
<entry key="DB_USERNAME" value="eventuser" />
|
||||
<entry key="DB_PASSWORD" value="Hi5Jessica!" />
|
||||
|
||||
<!-- Redis Settings -->
|
||||
<entry key="REDIS_HOST" value="20.214.210.71" />
|
||||
|
||||
@ -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<String, String> 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<EventStats> 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);
|
||||
|
||||
@ -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<EventStats> eventStatsList = createEventStats();
|
||||
eventStatsRepository.saveAll(eventStatsList);
|
||||
log.info("이벤트 통계 데이터 적재 완료: {} 건", eventStatsList.size());
|
||||
log.info("✅ 이벤트 통계 데이터 적재 완료: {} 건", eventStatsList.size());
|
||||
|
||||
// 2. 채널별 통계 데이터 생성
|
||||
List<ChannelStats> channelStatsList = createChannelStats(eventStatsList);
|
||||
channelStatsRepository.saveAll(channelStatsList);
|
||||
log.info("채널별 통계 데이터 적재 완료: {} 건", channelStatsList.size());
|
||||
log.info("✅ 채널별 통계 데이터 적재 완료: {} 건", channelStatsList.size());
|
||||
|
||||
// 3. 타임라인 데이터 생성
|
||||
List<TimelineData> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 이벤트 통계 샘플 데이터 생성
|
||||
*/
|
||||
|
||||
@ -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<ChannelStats> 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());
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user