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(); } }