Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions back/src/main/java/com/back/BackApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableJpaAuditing
@SpringBootApplication
@EnableCaching
@EnableScheduling
@ConfigurationPropertiesScan
public class BackApplication {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,4 @@ public ResponseEntity<UserResponse> me(@AuthenticationPrincipal CustomUserDetail
}
return ResponseEntity.ok(UserResponse.from(cud.getUser()));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

/**
Expand All @@ -18,5 +18,5 @@ public interface UserRepository extends JpaRepository<User, Long> {
boolean existsByEmail(String email);
boolean existsByNickname(String nickname);

int deleteByRoleAndCreatedDateBefore(Role role, LocalDateTime cutoff);
List<User> findByRole(Role role);
}
27 changes: 27 additions & 0 deletions back/src/main/java/com/back/global/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);

template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());

return template;
}
}
Original file line number Diff line number Diff line change
@@ -1,36 +1,119 @@
package com.back.global.scheduler;

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.Session;
import org.springframework.session.SessionRepository;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
* 게스트 계정 자동 정리 스케줄러
* 세션 리스너가 놓친 게스트 계정을 주기적으로 정리하는 안전망
* 실행 시점: 매일 새벽 3시
* 삭제 기준: 생성된 지 24시간이 지난 게스트 계정
* 만료된 게스트 유저를 주기적으로 삭제하는 스케줄러
* - Redis Session을 확인하여 실제로 세션이 없는 게스트만 삭제
* - 매 10분마다 실행
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class GuestCleanupScheduler {

private final UserRepository userRepository;
private final RedisTemplate<String, Object> redisTemplate;
private final SessionRepository sessionRepository;

// 매일 새벽 3시에 실행
@Scheduled(cron = "0 0 3 * * *")
@Scheduled(cron = "0 */30 * * * ?")
@Transactional
public void cleanExpiredGuests() {
LocalDateTime cutoff = LocalDateTime.now().minusHours(24);
int deleted = userRepository.deleteByRoleAndCreatedDateBefore(Role.GUEST, cutoff);
if (deleted > 0) {
log.info("배치 작업으로 만료된 게스트 {}건 삭제", deleted);
public void cleanupExpiredGuests() {
log.info("=== 게스트 정리 작업 시작 ===");

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<Long> deletedIds = expiredGuests.stream()
.map(User::getId)
.toList();

log.info("{}명의 만료된 게스트 삭제 완료 - ID: {}", expiredGuests.size(), deletedIds);
} else {
log.info("삭제할 만료된 게스트가 없습니다.");
}

if (!activeGuestIds.isEmpty()) {
log.info("활성 세션이 있는 게스트: {}명 - ID: {}", activeGuestIds.size(), activeGuestIds);
}

log.info("=== 게스트 정리 작업 완료 ===");

} catch (Exception e) {
log.error("게스트 정리 작업 중 오류 발생", e);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ public Map<String, Object> getAttributes() {
return attributes;
}

@Override public String getName() {
@Override
public String getName() {
if (user.getNickname()!=null && !user.getNickname().isBlank()) return user.getNickname();
return user.getEmail();
}
Expand Down

This file was deleted.

This file was deleted.