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