diff --git a/build.gradle b/build.gradle index ffc0704..1997091 100644 --- a/build.gradle +++ b/build.gradle @@ -111,6 +111,10 @@ dependencies { // oauth implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + + // batch + implementation 'org.springframework.boot:spring-boot-starter-batch' + implementation'org.springframework.boot:spring-boot-starter-quartz' } tasks.named('test') { diff --git a/src/main/java/kdt/web_ide/common/config/BatchConfig.java b/src/main/java/kdt/web_ide/common/config/BatchConfig.java new file mode 100644 index 0000000..cfe51b9 --- /dev/null +++ b/src/main/java/kdt/web_ide/common/config/BatchConfig.java @@ -0,0 +1,55 @@ +package kdt.web_ide.common.config; + +import javax.sql.DataSource; + +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.repository.support.JobRepositoryFactoryBean; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; + +import kdt.web_ide.members.batch.TokenCleanupTasklet; +import kdt.web_ide.members.entity.repository.TokenBlacklistRepository; + +@Configuration +@EnableBatchProcessing +public class BatchConfig { + + @Bean + public Job tokenCleanupJob(JobRepository jobRepository, Step tokenCleanupStep) { + return new JobBuilder("tokenCleanupJob", jobRepository) // ✅ JobBuilder 직접 사용 + .start(tokenCleanupStep) + .build(); + } + + @Bean + public Step tokenCleanupStep( + JobRepository jobRepository, + Tasklet tokenCleanupTasklet, + PlatformTransactionManager transactionManager) { + return new StepBuilder("tokenCleanupStep", jobRepository) + .tasklet(tokenCleanupTasklet, transactionManager) + .build(); + } + + @Bean + public Tasklet tokenCleanupTasklet(TokenBlacklistRepository tokenBlacklistRepository) { + return new TokenCleanupTasklet(tokenBlacklistRepository); + } + + @Bean + public JobRepository jobRepository( + DataSource dataSource, PlatformTransactionManager transactionManager) throws Exception { + JobRepositoryFactoryBean factoryBean = new JobRepositoryFactoryBean(); + factoryBean.setDataSource(dataSource); + factoryBean.setTransactionManager(transactionManager); + factoryBean.afterPropertiesSet(); + return factoryBean.getObject(); + } +} diff --git a/src/main/java/kdt/web_ide/members/batch/TokenBatchScheduler.java b/src/main/java/kdt/web_ide/members/batch/TokenBatchScheduler.java new file mode 100644 index 0000000..7407ce5 --- /dev/null +++ b/src/main/java/kdt/web_ide/members/batch/TokenBatchScheduler.java @@ -0,0 +1,36 @@ +package kdt.web_ide.members.batch; + +import java.time.LocalDateTime; + +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import kdt.web_ide.members.entity.repository.TokenBlacklistRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Component +@RequiredArgsConstructor +@Slf4j +public class TokenBatchScheduler { + private final TokenBlacklistRepository tokenBlacklistRepository; + private final JobLauncher jobLauncher; + private final Job tokenCleanupJob; + + @Scheduled(cron = "0 0 0,6,12,18 * * ?") + public void runTokenCleanupJob() { + if (tokenBlacklistRepository.countExpiredTokens(LocalDateTime.now()) > 0) { + try { + jobLauncher.run(tokenCleanupJob, new JobParameters()); + log.info("배치 작업 실행 완료: 블랙리스트 토큰 삭제"); + } catch (Exception e) { + log.error("배치 실행 중 오류 발생", e); + } + } else { + log.info("배치 작업 실행 스킵 : 만료된 토큰 없음"); + } + } +} diff --git a/src/main/java/kdt/web_ide/members/batch/TokenCleanupTasklet.java b/src/main/java/kdt/web_ide/members/batch/TokenCleanupTasklet.java new file mode 100644 index 0000000..706b683 --- /dev/null +++ b/src/main/java/kdt/web_ide/members/batch/TokenCleanupTasklet.java @@ -0,0 +1,25 @@ +package kdt.web_ide.members.batch; + +import java.time.LocalDateTime; + +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; + +import kdt.web_ide.members.entity.repository.TokenBlacklistRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@RequiredArgsConstructor +@Slf4j +public class TokenCleanupTasklet implements Tasklet { + private final TokenBlacklistRepository tokenBlacklistRepository; + + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) { + int deletedCount = tokenBlacklistRepository.deleteExpiredTokens(LocalDateTime.now()); + log.info("배치 작업 실행: 블랙리스트에서 만료된 토큰 {}개 삭제 완료", deletedCount); + return RepeatStatus.FINISHED; + } +} diff --git a/src/main/java/kdt/web_ide/members/controller/MemberController.java b/src/main/java/kdt/web_ide/members/controller/MemberController.java index ce7ba90..a189af6 100644 --- a/src/main/java/kdt/web_ide/members/controller/MemberController.java +++ b/src/main/java/kdt/web_ide/members/controller/MemberController.java @@ -6,6 +6,9 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.launch.JobLauncher; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseCookie; @@ -25,13 +28,18 @@ import kdt.web_ide.members.service.CustomUserDetails; import kdt.web_ide.members.service.MemberService; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; @RestController @RequiredArgsConstructor @Tag(name = "회원 API") @RequestMapping("/api/auth") +@Slf4j public class MemberController { + private final JobLauncher jobLauncher; + private final Job tokenCleanupJob; + private final MemberService memberService; @Operation(summary = "임시 로그인 API", description = "임시 로그인") @@ -127,10 +135,23 @@ public ResponseEntity recreateAccessToken( } @Operation(summary = "카카오 엑세스 토큰 재발급 API") - @GetMapping("/kakao") + @PostMapping("/kakao") public ResponseEntity getKakaoAccessToken( @AuthenticationPrincipal CustomUserDetails userDetails) { return ResponseEntity.status(HttpStatus.OK) .body(memberService.getKakaoAccessToken(userDetails.getMember().getMemberId())); } + + @Operation(summary = "블랙리스트 토큰 수동 삭제") + @PostMapping("/cleanup") + public ResponseEntity runTokenCleanUp() { + try { + jobLauncher.run(tokenCleanupJob, new JobParameters()); + return ResponseEntity.ok("Token cleanup batch job started."); + } catch (Exception e) { + log.error("에러", e.getMessage(), e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Failed to start batch job."); + } + } } diff --git a/src/main/java/kdt/web_ide/members/entity/repository/TokenBlacklistRepository.java b/src/main/java/kdt/web_ide/members/entity/repository/TokenBlacklistRepository.java index 09f3e0b..feafa38 100644 --- a/src/main/java/kdt/web_ide/members/entity/repository/TokenBlacklistRepository.java +++ b/src/main/java/kdt/web_ide/members/entity/repository/TokenBlacklistRepository.java @@ -1,10 +1,32 @@ package kdt.web_ide.members.entity.repository; +import java.time.LocalDateTime; + import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; import kdt.web_ide.members.entity.TokenBlacklist; public interface TokenBlacklistRepository extends JpaRepository { boolean existsByRefreshToken(String refreshToken); + + @Modifying + @Transactional + @Query(""" + DELETE + FROM TokenBlacklist t + WHERE t.expiredAt <= :now + """) + int deleteExpiredTokens(@Param("now") LocalDateTime now); + + @Query(""" + SELECT COUNT(t) + FROM TokenBlacklist t + WHERE t.expiredAt <= :now + """) + long countExpiredTokens(@Param("now") LocalDateTime now); } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 674f37b..8d4476a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -3,6 +3,9 @@ spring: url: ${SPRING_DATASOURCE_URL} username: ${SPRING_DATASOURCE_USERNAME} password: ${SPRING_DATASOURCE_PASSWORD} + batch: + jdbc: + initialize-schema: never jpa: database-platform: ${SPRING_JPA_DATABASE_PLATFORM} hibernate: @@ -25,3 +28,5 @@ oauth: url: auth: https://kauth.kakao.com api: https://kapi.kakao.com + +