11package com .back .global .scheduler ;
22
33import com .back .domain .user .entity .Role ;
4+ import com .back .domain .user .entity .User ;
45import com .back .domain .user .repository .UserRepository ;
6+ import com .back .global .security .CustomUserDetails ;
57import lombok .RequiredArgsConstructor ;
68import lombok .extern .slf4j .Slf4j ;
9+ import org .springframework .data .redis .core .RedisTemplate ;
710import org .springframework .scheduling .annotation .Scheduled ;
11+ import org .springframework .security .core .context .SecurityContext ;
12+ import org .springframework .session .Session ;
13+ import org .springframework .session .SessionRepository ;
814import org .springframework .stereotype .Component ;
915import org .springframework .transaction .annotation .Transactional ;
1016
11- import java .time .LocalDateTime ;
17+ import java .util .ArrayList ;
18+ import java .util .HashSet ;
19+ import java .util .List ;
20+ import java .util .Set ;
1221
1322/**
14- * 게스트 계정 자동 정리 스케줄러
15- * 세션 리스너가 놓친 게스트 계정을 주기적으로 정리하는 안전망
16- * 실행 시점: 매일 새벽 3시
17- * 삭제 기준: 생성된 지 24시간이 지난 게스트 계정
23+ * 만료된 게스트 유저를 주기적으로 삭제하는 스케줄러
24+ * - Redis Session을 확인하여 실제로 세션이 없는 게스트만 삭제
25+ * - 매 10분마다 실행
1826 */
1927@ Slf4j
2028@ Component
2129@ RequiredArgsConstructor
2230public class GuestCleanupScheduler {
2331
2432 private final UserRepository userRepository ;
33+ private final RedisTemplate <String , Object > redisTemplate ;
34+ private final SessionRepository sessionRepository ;
2535
26- // 매일 새벽 3시에 실행
27- @ Scheduled (cron = "0 0 3 * * *" )
36+ @ Scheduled (cron = "0 */10 * * * ?" ) // 매 10분마다 실행
2837 @ Transactional
29- public void cleanExpiredGuests () {
30- LocalDateTime cutoff = LocalDateTime .now ().minusHours (24 );
31- int deleted = userRepository .deleteByRoleAndCreatedDateBefore (Role .GUEST , cutoff );
32- if (deleted > 0 ) {
33- log .info ("배치 작업으로 만료된 게스트 {}건 삭제" , deleted );
38+ public void cleanupExpiredGuests () {
39+ log .info ("=== 게스트 정리 작업 시작 ===" );
40+
41+ try {
42+ Set <String > sessionKeys = redisTemplate .keys ("spring:session:sessions:*" );
43+ Set <Long > activeGuestIds = new HashSet <>();
44+
45+ if (sessionKeys != null && !sessionKeys .isEmpty ()) {
46+ log .debug ("현재 활성 세션 수: {}" , sessionKeys .size ());
47+
48+ for (String key : sessionKeys ) {
49+ if (key .contains ("expires" )) continue ; // expires 키는 스킵
50+
51+ String sessionId = key .replace ("spring:session:sessions:" , "" );
52+
53+ try {
54+ Session session = sessionRepository .findById (sessionId );
55+ if (session == null ) continue ;
56+
57+ SecurityContext securityContext = session .getAttribute ("SPRING_SECURITY_CONTEXT" );
58+ if (securityContext == null || securityContext .getAuthentication () == null ) {
59+ continue ;
60+ }
61+
62+ Object principal = securityContext .getAuthentication ().getPrincipal ();
63+
64+ if (principal instanceof CustomUserDetails ) {
65+ CustomUserDetails userDetails = (CustomUserDetails ) principal ;
66+ User user = userDetails .getUser ();
67+
68+ if (user .getRole () == Role .GUEST ) {
69+ activeGuestIds .add (user .getId ());
70+ log .debug ("활성 게스트 세션: id={}" , user .getId ());
71+ }
72+ }
73+ } catch (Exception e ) {
74+ log .warn ("세션 읽기 실패: {} - {}" , sessionId , e .getMessage ());
75+ }
76+ }
77+ }
78+
79+ List <User > allGuests = userRepository .findByRole (Role .GUEST );
80+
81+ if (allGuests .isEmpty ()) {
82+ log .info ("데이터베이스에 게스트 유저가 없습니다." );
83+ return ;
84+ }
85+
86+ log .debug ("전체 게스트 유저 수: {}" , allGuests .size ());
87+
88+ // 3. 세션이 없는 게스트 필터링
89+ List <User > expiredGuests = new ArrayList <>();
90+
91+ for (User guest : allGuests ) {
92+ if (!activeGuestIds .contains (guest .getId ())) {
93+ expiredGuests .add (guest );
94+ }
95+ }
96+
97+ if (!expiredGuests .isEmpty ()) {
98+ userRepository .deleteAll (expiredGuests );
99+
100+ List <Long > deletedIds = expiredGuests .stream ()
101+ .map (User ::getId )
102+ .toList ();
103+
104+ log .info ("{}명의 만료된 게스트 삭제 완료 - ID: {}" , expiredGuests .size (), deletedIds );
105+ } else {
106+ log .info ("삭제할 만료된 게스트가 없습니다." );
107+ }
108+
109+ if (!activeGuestIds .isEmpty ()) {
110+ log .info ("활성 세션이 있는 게스트: {}명 - ID: {}" , activeGuestIds .size (), activeGuestIds );
111+ }
112+
113+ log .info ("=== 게스트 정리 작업 완료 ===" );
114+
115+ } catch (Exception e ) {
116+ log .error ("게스트 정리 작업 중 오류 발생" , e );
34117 }
35118 }
36- }
119+ }
0 commit comments