33import com .back .domain .user .entity .Role ;
44import com .back .domain .user .entity .User ;
55import com .back .domain .user .repository .UserRepository ;
6- import com .back .global .security .CustomUserDetails ;
76import lombok .RequiredArgsConstructor ;
87import lombok .extern .slf4j .Slf4j ;
9- import org .springframework .data .redis .core .RedisTemplate ;
108import org .springframework .scheduling .annotation .Scheduled ;
11- import org .springframework .security . core . context . SecurityContext ;
9+ import org .springframework .session . FindByIndexNameSessionRepository ;
1210import org .springframework .session .Session ;
13- import org .springframework .session .SessionRepository ;
1411import org .springframework .stereotype .Component ;
1512import org .springframework .transaction .annotation .Transactional ;
1613
1714import java .util .ArrayList ;
18- import java .util .HashSet ;
1915import java .util .List ;
20- import java .util .Set ;
16+ import java .util .Map ;
2117
2218/**
23- * 만료된 게스트 유저를 주기적으로 삭제하는 스케줄러
24- * - Redis Session을 확인하여 실제로 세션이 없는 게스트만 삭제
25- * - 매 10분마다 실행
19+ * 게스트 유저 정리 보조 scheduler
20+ * - 세션 리스너에서 처리하지 못하고 남은 게스트가 있다면 삭제
2621 */
2722@ Slf4j
2823@ Component
2924@ RequiredArgsConstructor
3025public class GuestCleanupScheduler {
3126
3227 private final UserRepository userRepository ;
33- private final RedisTemplate <String , Object > redisTemplate ;
34- private final SessionRepository sessionRepository ;
28+ private final FindByIndexNameSessionRepository <? extends Session > sessionIndexRepo ;
3529
36- @ Scheduled (cron = "0 */30 * * * ?" )
30+ @ Scheduled (cron = "0 0 17 * * ?" ) // 매일 오후 5시
3731 @ Transactional
3832 public void cleanupExpiredGuests () {
39- log .info ("=== 게스트 정리 작업 시작 ===" );
33+ log .info ("=== 게스트 정리 작업 시작 (index 기반) ===" );
4034
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 );
35+ List <User > allGuests = userRepository .findByRole (Role .GUEST );
36+ if (allGuests .isEmpty ()) {
37+ log .info ("게스트 유저가 없습니다." );
38+ return ;
39+ }
9940
100- List <Long > deletedIds = expiredGuests .stream ()
101- .map (User ::getId )
102- .toList ();
41+ List <User > expiredGuests = new ArrayList <>();
10342
104- log .info ("{}명의 만료된 게스트 삭제 완료 - ID: {}" , expiredGuests .size (), deletedIds );
105- } else {
106- log .info ("삭제할 만료된 게스트가 없습니다." );
107- }
43+ for (User guest : allGuests ) {
44+ String principal = guest .getEmail ();
10845
109- if (!activeGuestIds .isEmpty ()) {
110- log .info ("활성 세션이 있는 게스트: {}명 - ID: {}" , activeGuestIds .size (), activeGuestIds );
111- }
46+ Map <String , ? extends Session > sessions =
47+ sessionIndexRepo .findByIndexNameAndIndexValue (
48+ FindByIndexNameSessionRepository .PRINCIPAL_NAME_INDEX_NAME ,
49+ principal
50+ );
11251
113- log .info ("=== 게스트 정리 작업 완료 ===" );
52+ boolean hasActive = sessions != null && !sessions .isEmpty ();
53+ if (!hasActive ) expiredGuests .add (guest );
54+ }
11455
115- } catch (Exception e ) {
116- log .error ("게스트 정리 작업 중 오류 발생" , e );
56+ if (!expiredGuests .isEmpty ()) {
57+ userRepository .deleteAllInBatch (expiredGuests );
58+ log .info ("만료 게스트 {}명 삭제 완료 - IDs={}" ,
59+ expiredGuests .size (),
60+ expiredGuests .stream ().map (User ::getId ).toList ());
61+ } else {
62+ log .info ("삭제할 만료 게스트가 없습니다." );
11763 }
64+
65+ log .info ("=== 게스트 정리 작업 완료 ===" );
11866 }
11967}
0 commit comments