diff --git a/back/src/main/java/com/back/domain/node/service/BaseLineService.java b/back/src/main/java/com/back/domain/node/service/BaseLineService.java index 171aec1..eaa2ded 100644 --- a/back/src/main/java/com/back/domain/node/service/BaseLineService.java +++ b/back/src/main/java/com/back/domain/node/service/BaseLineService.java @@ -34,7 +34,7 @@ @Service @RequiredArgsConstructor @Slf4j -class BaseLineService { +public class BaseLineService { private final BaseLineRepository baseLineRepository; private final BaseNodeRepository baseNodeRepository; diff --git a/back/src/main/java/com/back/domain/user/controller/UserAuthController.java b/back/src/main/java/com/back/domain/user/controller/UserAuthController.java index 2c5667a..000f90f 100644 --- a/back/src/main/java/com/back/domain/user/controller/UserAuthController.java +++ b/back/src/main/java/com/back/domain/user/controller/UserAuthController.java @@ -73,7 +73,11 @@ public ResponseEntity guestLogin(HttpServletRequest request, HttpS HttpSession session = request.getSession(true); session.setMaxInactiveInterval(600); - session.setAttribute("guestId", savedGuest.getId()); + + session.setAttribute( + org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, + savedGuest.getEmail() + ); new HttpSessionSecurityContextRepository() .saveContext(SecurityContextHolder.getContext(), request, response); diff --git a/back/src/main/java/com/back/global/config/RedisConfig.java b/back/src/main/java/com/back/global/config/RedisConfig.java deleted file mode 100644 index c28f85e..0000000 --- a/back/src/main/java/com/back/global/config/RedisConfig.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.back.global.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.serializer.StringRedisSerializer; - -/** - * Redis 설정 - * - RedisTemplate 빈 생성 - * - 세션 스케줄러에서 Redis 키 확인용 - */ -@Configuration -public class RedisConfig { - - @Bean - public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { - RedisTemplate template = new RedisTemplate<>(); - template.setConnectionFactory(connectionFactory); - - template.setKeySerializer(new StringRedisSerializer()); - template.setHashKeySerializer(new StringRedisSerializer()); - - return template; - } -} \ No newline at end of file diff --git a/back/src/main/java/com/back/global/scheduler/GuestCleanupScheduler.java b/back/src/main/java/com/back/global/scheduler/GuestCleanupScheduler.java index 2f22133..8dd2d10 100644 --- a/back/src/main/java/com/back/global/scheduler/GuestCleanupScheduler.java +++ b/back/src/main/java/com/back/global/scheduler/GuestCleanupScheduler.java @@ -3,26 +3,21 @@ import com.back.domain.user.entity.Role; import com.back.domain.user.entity.User; import com.back.domain.user.repository.UserRepository; -import com.back.global.security.CustomUserDetails; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.data.redis.core.RedisTemplate; import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.security.core.context.SecurityContext; +import org.springframework.session.FindByIndexNameSessionRepository; import org.springframework.session.Session; -import org.springframework.session.SessionRepository; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; -import java.util.Set; +import java.util.Map; /** - * 만료된 게스트 유저를 주기적으로 삭제하는 스케줄러 - * - Redis Session을 확인하여 실제로 세션이 없는 게스트만 삭제 - * - 매 10분마다 실행 + * 게스트 유저 정리 보조 scheduler + * - 세션 리스너에서 처리하지 못하고 남은 게스트가 있다면 삭제 */ @Slf4j @Component @@ -30,90 +25,43 @@ public class GuestCleanupScheduler { private final UserRepository userRepository; - private final RedisTemplate redisTemplate; - private final SessionRepository sessionRepository; + private final FindByIndexNameSessionRepository sessionIndexRepo; - @Scheduled(cron = "0 */30 * * * ?") + @Scheduled(cron = "0 0 17 * * ?") // 매일 오후 5시 @Transactional public void cleanupExpiredGuests() { - log.info("=== 게스트 정리 작업 시작 ==="); + log.info("=== 게스트 정리 작업 시작 (index 기반) ==="); - try { - Set sessionKeys = redisTemplate.keys("spring:session:sessions:*"); - Set activeGuestIds = new HashSet<>(); - - if (sessionKeys != null && !sessionKeys.isEmpty()) { - log.debug("현재 활성 세션 수: {}", sessionKeys.size()); - - for (String key : sessionKeys) { - if (key.contains("expires")) continue; // expires 키는 스킵 - - String sessionId = key.replace("spring:session:sessions:", ""); - - try { - Session session = sessionRepository.findById(sessionId); - if (session == null) continue; - - SecurityContext securityContext = session.getAttribute("SPRING_SECURITY_CONTEXT"); - if (securityContext == null || securityContext.getAuthentication() == null) { - continue; - } - - Object principal = securityContext.getAuthentication().getPrincipal(); - - if (principal instanceof CustomUserDetails) { - CustomUserDetails userDetails = (CustomUserDetails) principal; - User user = userDetails.getUser(); - - if (user.getRole() == Role.GUEST) { - activeGuestIds.add(user.getId()); - log.debug("활성 게스트 세션: id={}", user.getId()); - } - } - } catch (Exception e) { - log.warn("세션 읽기 실패: {} - {}", sessionId, e.getMessage()); - } - } - } - - List allGuests = userRepository.findByRole(Role.GUEST); - - if (allGuests.isEmpty()) { - log.info("데이터베이스에 게스트 유저가 없습니다."); - return; - } - - log.debug("전체 게스트 유저 수: {}", allGuests.size()); - - // 3. 세션이 없는 게스트 필터링 - List expiredGuests = new ArrayList<>(); - - for (User guest : allGuests) { - if (!activeGuestIds.contains(guest.getId())) { - expiredGuests.add(guest); - } - } - - if (!expiredGuests.isEmpty()) { - userRepository.deleteAll(expiredGuests); + List allGuests = userRepository.findByRole(Role.GUEST); + if (allGuests.isEmpty()) { + log.info("게스트 유저가 없습니다."); + return; + } - List deletedIds = expiredGuests.stream() - .map(User::getId) - .toList(); + List expiredGuests = new ArrayList<>(); - log.info("{}명의 만료된 게스트 삭제 완료 - ID: {}", expiredGuests.size(), deletedIds); - } else { - log.info("삭제할 만료된 게스트가 없습니다."); - } + for (User guest : allGuests) { + String principal = guest.getEmail(); - if (!activeGuestIds.isEmpty()) { - log.info("활성 세션이 있는 게스트: {}명 - ID: {}", activeGuestIds.size(), activeGuestIds); - } + Map sessions = + sessionIndexRepo.findByIndexNameAndIndexValue( + FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, + principal + ); - log.info("=== 게스트 정리 작업 완료 ==="); + boolean hasActive = sessions != null && !sessions.isEmpty(); + if (!hasActive) expiredGuests.add(guest); + } - } catch (Exception e) { - log.error("게스트 정리 작업 중 오류 발생", e); + if (!expiredGuests.isEmpty()) { + userRepository.deleteAllInBatch(expiredGuests); + log.info("만료 게스트 {}명 삭제 완료 - IDs={}", + expiredGuests.size(), + expiredGuests.stream().map(User::getId).toList()); + } else { + log.info("삭제할 만료 게스트가 없습니다."); } + + log.info("=== 게스트 정리 작업 완료 ==="); } } \ No newline at end of file diff --git a/back/src/main/java/com/back/global/session/GuestSessionListener.java b/back/src/main/java/com/back/global/session/GuestSessionListener.java new file mode 100644 index 0000000..053f9c4 --- /dev/null +++ b/back/src/main/java/com/back/global/session/GuestSessionListener.java @@ -0,0 +1,80 @@ +package com.back.global.session; + +import com.back.domain.node.entity.BaseLine; +import com.back.domain.node.repository.BaseLineRepository; +import com.back.domain.node.service.BaseLineService; +import com.back.domain.user.entity.Role; +import com.back.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.session.FindByIndexNameSessionRepository; +import org.springframework.session.Session; +import org.springframework.session.events.AbstractSessionEvent; +import org.springframework.session.events.SessionDeletedEvent; +import org.springframework.session.events.SessionExpiredEvent; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * 게스트 세션이 만료되거나 삭제될 때 해당 게스트 계정과 관련된 모든 데이터를 정리하는 리스너 + * (Spring Session의 SessionExpiredEvent / SessionDeletedEvent를 수신) + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class GuestSessionListener { + + private final UserRepository userRepository; + private final BaseLineRepository baseLineRepository; + private final BaseLineService baseLineService; + + @EventListener({SessionExpiredEvent.class, SessionDeletedEvent.class}) + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void onSessionEvent(AbstractSessionEvent event) { + final Session session = event.getSession(); + if (session == null) return; + + final String principalName = + session.getAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME); + if (principalName == null || !principalName.startsWith("guest_")) return; + + var userOpt = userRepository.findByEmail(principalName) + .filter(u -> u.getRole() == Role.GUEST); + + if (userOpt.isEmpty()) { + log.info("[GuestSessionListener] 이미 삭제되었거나 게스트 아님: {}", principalName); + return; + } + + var user = userOpt.get(); + Long userId = user.getId(); + + try { + // 게스트 소유 baseline 조회 + List baseLineIds = baseLineRepository.findByUser_IdOrderByIdDesc(userId).stream().map(BaseLine::getId).toList(); + + // deleteBaseLineDeep -> baseLine부터 baseNode, decisionLine, scenario 등등 모두 다 삭제 + for (Long baseLineId : baseLineIds) { + try { + baseLineService.deleteBaseLineDeep(userId, baseLineId); + } catch (Exception ex) { + log.error("[GuestSessionListener] baseline({}) 삭제 실패 - 계속 진행", baseLineId, ex); + } + } + + userRepository.delete(user); + + log.info("[GuestSessionListener] {} -> 게스트 및 소유 리소스 삭제 완료: {}", + (event instanceof SessionExpiredEvent) ? "SessionExpiredEvent" : "SessionDeletedEvent", + principalName); + + } catch (Exception e) { + log.error("[GuestSessionListener] 게스트({}) 삭제 처리 중 오류", principalName, e); + throw e; + } + } +} diff --git a/back/src/main/resources/application.yml b/back/src/main/resources/application.yml index ee335f7..87e4720 100644 --- a/back/src/main/resources/application.yml +++ b/back/src/main/resources/application.yml @@ -28,7 +28,12 @@ spring: highlight_sql: true default_batch_fetch_size: 100 open-in-view: false - security: # 여기에 추가 + + session: + redis: + repository-type: indexed + + security: oauth2: client: registration: