Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
@Service
@RequiredArgsConstructor
@Slf4j
class BaseLineService {
public class BaseLineService {

private final BaseLineRepository baseLineRepository;
private final BaseNodeRepository baseNodeRepository;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,11 @@ public ResponseEntity<UserResponse> 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);
Expand Down
27 changes: 0 additions & 27 deletions back/src/main/java/com/back/global/config/RedisConfig.java

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,117 +3,65 @@
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
@RequiredArgsConstructor
public class GuestCleanupScheduler {

private final UserRepository userRepository;
private final RedisTemplate<String, Object> redisTemplate;
private final SessionRepository sessionRepository;
private final FindByIndexNameSessionRepository<? extends Session> sessionIndexRepo;

@Scheduled(cron = "0 */30 * * * ?")
@Scheduled(cron = "0 0 17 * * ?") // 매일 오후 5시
@Transactional
public void cleanupExpiredGuests() {
log.info("=== 게스트 정리 작업 시작 ===");
log.info("=== 게스트 정리 작업 시작 (index 기반) ===");

try {
Set<String> sessionKeys = redisTemplate.keys("spring:session:sessions:*");
Set<Long> 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<User> allGuests = userRepository.findByRole(Role.GUEST);

if (allGuests.isEmpty()) {
log.info("데이터베이스에 게스트 유저가 없습니다.");
return;
}

log.debug("전체 게스트 유저 수: {}", allGuests.size());

// 3. 세션이 없는 게스트 필터링
List<User> expiredGuests = new ArrayList<>();

for (User guest : allGuests) {
if (!activeGuestIds.contains(guest.getId())) {
expiredGuests.add(guest);
}
}

if (!expiredGuests.isEmpty()) {
userRepository.deleteAll(expiredGuests);
List<User> allGuests = userRepository.findByRole(Role.GUEST);
if (allGuests.isEmpty()) {
log.info("게스트 유저가 없습니다.");
return;
}

List<Long> deletedIds = expiredGuests.stream()
.map(User::getId)
.toList();
List<User> 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<String, ? extends Session> 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("=== 게스트 정리 작업 완료 ===");
}
}
Original file line number Diff line number Diff line change
@@ -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<Long> 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;
}
}
}
8 changes: 7 additions & 1 deletion back/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@ spring:
highlight_sql: true
default_batch_fetch_size: 100
open-in-view: false
security: # 여기에 추가

session:
store-type: redis
redis:
repository-type: indexed

security:
oauth2:
client:
registration:
Expand Down