#275 (데몬): - DaemonConfigService.updateConfig: 정수 필드 가드 (비숫자/0/음수 → 400) - DaemonScheduler: 외부 호출(scan/process) try-finally로 updateLastX 보장 (예외 시에도 다음 cron까지 backoff) - DaemonController.getConfig: AuthUtil.requireAdmin() 추가 (운영 설정 노출 차단) #276 (캐시): - CacheService 생성자: ping을 try-with-resources로 자원 누수 차단, ConnectionFactory null 가드 - makeKey: null/빈 parts 가드 (잘못된 키 생성 방지) #274 (통계): - SiteVisitStats: int → long (21억 누적 시 오버플로 방지) - StatsMapper: getTodayVisits/getTotalVisits long 반환 - StatsService.recordVisit: 자정 경계 동시성 DataIntegrityViolationException 1회 재시도, 2회 실패 시 1건 손실 수용 (운영 영향 미미) 후속 분리: - #336 (#275 분산 락 + DTO + 테스트) - #337 (#276 SCAN + 자동복구 + 메트릭) - #338 (#274 봇/레이트리밋 + Redis INCR + 테스트) Refs: #275 #276 #274
44 lines
1.5 KiB
Java
44 lines
1.5 KiB
Java
package com.tasteby.service;
|
|
|
|
import com.tasteby.domain.SiteVisitStats;
|
|
import com.tasteby.mapper.StatsMapper;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
import org.springframework.dao.DataIntegrityViolationException;
|
|
import org.springframework.stereotype.Service;
|
|
|
|
@Service
|
|
public class StatsService {
|
|
|
|
private static final Logger log = LoggerFactory.getLogger(StatsService.class);
|
|
|
|
private final StatsMapper mapper;
|
|
|
|
public StatsService(StatsMapper mapper) {
|
|
this.mapper = mapper;
|
|
}
|
|
|
|
public void recordVisit() {
|
|
// #274 — 자정 경계 동시성: 두 트랜잭션이 동시에 'NOT MATCHED' 판정 → 둘 다 INSERT
|
|
// → PK/UNIQUE 충돌 시 한 쪽 500. 1회 재시도(다음엔 MATCHED → UPDATE 분기).
|
|
try {
|
|
mapper.recordVisit();
|
|
} catch (DataIntegrityViolationException e) {
|
|
log.debug("recordVisit conflict (midnight race), retry once: {}", e.getMessage());
|
|
try {
|
|
mapper.recordVisit();
|
|
} catch (DataIntegrityViolationException retryFail) {
|
|
// 두 번째 시도도 실패: 카운트 1건 손실은 수용 (운영 영향 미미)
|
|
log.warn("recordVisit double-conflict, dropping one count: {}", retryFail.getMessage());
|
|
}
|
|
}
|
|
}
|
|
|
|
public SiteVisitStats getVisits() {
|
|
return SiteVisitStats.builder()
|
|
.today(mapper.getTodayVisits())
|
|
.total(mapper.getTotalVisits())
|
|
.build();
|
|
}
|
|
}
|