스케줄러 (@Scheduled)

📁 적용 코드: FileCleanupScheduler.java, DemoApplication.java


1. 스케줄러란?

서버가 주기적으로 자동 실행하는 작업. 사용자 요청 없이 서버가 알아서 돌린다.

일반 API 요청:
  사용자 클릭 → Controller → Service → 결과 응답

스케줄러:
  정해진 시간 도달 → Spring이 자동으로 메서드 호출 → 작업 수행
  (사용자 요청 없음)

실무 활용 예시

작업 주기 설명
고아 파일 정리 매일 새벽 4시 연결 안 된 파일 물리 삭제 (이 프로젝트)
메일 발송 매일 오전 9시 알림 이메일 일괄 발송
데이터 백업 매일 새벽 2시 DB 덤프
캐시 갱신 1시간마다 인기 게시글 순위 등
임시 데이터 삭제 매주 일요일 탈퇴 유예 기간 지난 계정 삭제

2. Spring Boot 스케줄러 설정

① @EnableScheduling 선언 (1회)

// DemoApplication.java (이 프로젝트 실제 코드)
@EnableScheduling        // ← 이 한 줄로 스케줄러 기능 활성화
@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

@EnableScheduling이 없으면 @Scheduled를 붙여도 아무 일도 안 일어남.

② @Scheduled 메서드 작성

@Component  // Bean으로 등록되어야 Spring이 관리 가능
public class MyScheduler {

    @Scheduled(cron = "0 0 4 * * *")  // 매일 새벽 4시에 실행
    public void myTask() {
        // 여기에 자동으로 실행할 로직 작성
    }
}

규칙


3. @Scheduled 옵션

cron 표현식 (가장 많이 사용)

@Scheduled(cron = "초 분 시 일 월 요일")
필드 범위 설명
0-59  
0-59  
0-23  
1-31  
1-12  
요일 0-7 (0,7=일) 또는 MON-SUN  
특수문자 의미 예시
* 매번 * * * * * * = 매초
? 사용 안 함 (일/요일 중 하나에)  
/ 간격 0/10 * * * * * = 10초마다
- 범위 0 0 9-18 * * * = 9시~18시 매 정각
, 목록 0 0 9,18 * * * = 9시, 18시

자주 쓰는 cron 예시

@Scheduled(cron = "0 0 4 * * *")       // 매일 새벽 4시
@Scheduled(cron = "0 0 0 * * MON")     // 매주 월요일 자정
@Scheduled(cron = "0 0/30 * * * *")    // 30분마다
@Scheduled(cron = "0 0 9-18 * * MON-FRI")  // 평일 9시~18시 매 정각

기타 옵션

// fixedRate: 이전 실행 시작 시점 기준 밀리초 간격
@Scheduled(fixedRate = 60000)  // 1분마다 (이전 실행 시작 후 60초)

// fixedDelay: 이전 실행 완료 시점 기준 밀리초 간격
@Scheduled(fixedDelay = 60000)  // 이전 실행 끝나고 1분 후

// initialDelay: 서버 시작 후 최초 실행까지 대기 시간
@Scheduled(fixedRate = 60000, initialDelay = 5000)  // 서버 시작 5초 후부터 1분마다
옵션 기준 적합한 상황
cron 정해진 시각 “매일 새벽 4시”, “매주 월요일”
fixedRate 시작~시작 간격 “30초마다 상태 체크” (작업이 빨리 끝나는 경우)
fixedDelay 종료~시작 간격 “이전 작업 끝나고 1분 후” (작업 시간이 불확실한 경우)

4. 이 프로젝트 적용 — FileCleanupScheduler

왜 필요한가?

에디터에서 이미지를 업로드하면 즉시 물리 파일이 저장됨.
하지만 사용자가 글을 등록하지 않고 이탈하면 → 연결 안 된 고아 파일이 남음.

고아 파일이 생기는 시나리오:
1. 에디터에서 이미지 업로드 → 파일 저장 (refId=0, 임시 상태)
   → 글 등록 안 하고 이탈 → refId=0인 파일이 디스크에 남음

2. 글 수정 시 에디터 이미지 삭제 → 화면에서만 제거
   → 서버에는 파일이 남아있고, 글 저장 시 고아 처리(refId=0)

3. 글 삭제 → 파일은 고아 처리(refId=0)만 함
   → 물리 파일은 그대로 남아있음

이런 고아 파일들을 스케줄러가 주기적으로 정리한다.

실제 코드

// FileCleanupScheduler.java
@Component
@RequiredArgsConstructor
public class FileCleanupScheduler {

    private final FileService fileService;

    // 매일 새벽 4시에 실행
    @Scheduled(cron = "0 0 4 * * *")
    public void cleanupOrphanFiles() {
        // 24시간 이전의 고아 파일만 삭제 (작성 중인 파일이 바로 삭제되지 않도록 유예)
        LocalDateTime timeLimit = LocalDateTime.now().minusHours(24);
        int count = fileService.deleteOrphanFiles(timeLimit);
    }
}
// FileService.deleteOrphanFiles() — 스케줄러에서 호출
@Transactional
public int deleteOrphanFiles(LocalDateTime timeLimit) {
    // 1. 고아 파일 조회 (refId=0 + 수정일 < 24시간 전)
    List<FileEntity> orphans = fileRepository.findByRefIdAndUpdatedAtBefore(0L, timeLimit);
    if (orphans.isEmpty()) return 0;

    // 2. 물리 파일 삭제 (디스크에서 삭제)
    for (FileEntity file : orphans) {
        Path filePath = uploadPath.resolve(file.getStoredFileName());
        Files.deleteIfExists(filePath);
    }

    // 3. DB에서 하드 삭제
    fileRepository.deleteAll(orphans);

    return orphans.size();
}

동작 흐름

매일 새벽 4시
    ↓
FileCleanupScheduler.cleanupOrphanFiles() 자동 호출
    ↓
fileService.deleteOrphanFiles(24시간 전)
    ↓
DB 조회: refId=0 AND updatedAt < 24시간 전인 파일들
    ↓
물리 파일 삭제 (uploads/ 폴더에서 삭제)
    ↓
DB 하드 삭제 (files 테이블에서 DELETE)

왜 바로 삭제하지 않고 스케줄러를 쓰는가?

방식 장점 단점
즉시 삭제 고아 파일 없음 글 작성 중 취소하면? 트랜잭션 불일치 위험
스케줄러 삭제 (이 프로젝트) 트랜잭션 안전, 유예 기간 일시적으로 고아 파일 존재

에디터 이미지 업로드 → 글 등록은 별개의 요청이므로 트랜잭션으로 묶을 수 없음.
24시간 유예를 두면 사용자가 작성 중인 파일이 삭제되는 것을 방지.


5. 스케줄러 vs 배치

  스케줄러 (@Scheduled) 배치 (Spring Batch)
복잡도 간단 복잡
적합한 작업 단순 반복 작업 대량 데이터 처리, 단계별 처리
재시도/복구 직접 구현 프레임워크 제공
모니터링 직접 구현 프레임워크 제공
이 프로젝트 ✅ 사용 ❌ 불필요

고아 파일 정리 정도의 단순 작업은 @Scheduled로 충분.
수백만 건의 데이터 마이그레이션, 정산 같은 작업은 Spring Batch 고려.