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
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
55 changes: 55 additions & 0 deletions src/main/java/kdt/web_ide/common/config/BatchConfig.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
36 changes: 36 additions & 0 deletions src/main/java/kdt/web_ide/members/batch/TokenBatchScheduler.java
Original file line number Diff line number Diff line change
@@ -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("배치 작업 실행 스킵 : 만료된 토큰 없음");
}
}
}
25 changes: 25 additions & 0 deletions src/main/java/kdt/web_ide/members/batch/TokenCleanupTasklet.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 = "임시 로그인")
Expand Down Expand Up @@ -127,10 +135,23 @@ public ResponseEntity<TokenResponse> recreateAccessToken(
}

@Operation(summary = "카카오 엑세스 토큰 재발급 API")
@GetMapping("/kakao")
@PostMapping("/kakao")
public ResponseEntity<TokenResponse> getKakaoAccessToken(
@AuthenticationPrincipal CustomUserDetails userDetails) {
return ResponseEntity.status(HttpStatus.OK)
.body(memberService.getKakaoAccessToken(userDetails.getMember().getMemberId()));
}

@Operation(summary = "블랙리스트 토큰 수동 삭제")
@PostMapping("/cleanup")
public ResponseEntity<String> 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.");
}
}
}
Original file line number Diff line number Diff line change
@@ -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<TokenBlacklist, Long> {

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);
}
5 changes: 5 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -25,3 +28,5 @@ oauth:
url:
auth: https://kauth.kakao.com
api: https://kapi.kakao.com