예약 게시글 시나리오 기준으로 정리하면, 스케줄러는 “예약된 글 중에서 게시 시간이 된 것만 자동으로 게시 상태로 전환”해주는 백그라운드 작업이다. Spring에서는 @EnableScheduling + @Scheduled 조합으로 주기 실행을 쉽게 구현할 수 있고, 멀티 서버에서는 중복 실행을 막기 위한 분산 락(예: Redis)이 사실상 필수다.
글쓰기에서 “게시 시간을 지정”하는 기능을 만들면 보통 아래 상태를 저장하게 된다.
appointment = "NO"appointment = "YES", appointmentTime = 2026-01-28 12:00:00 같은 예약시간 저장그리고 시간이 흘러 예약 시간이 지나 실제로 게시되면, 더 이상 “예약글”이 아니므로 appointment를 YES → NO로 바꾸는 흐름이 된다. 이 처리를 사람이 클릭해서 하는 게 아니라, 서버가 일정 간격으로 자동 수행해주는 역할이 바로 스케줄러다.
예약 게시 시나리오에서 스케줄러 로직은 딱 4단계로 정리된다.
YES인 글만 조회한다.now)과 예약시간(appointmentTime)을 비교한다.appointment = "NO"로 변경)한다.즉, “조회는 NO인 글들만 하면 됨” 같은 정책을 만들고 싶다면, 게시 API/목록 조회 API에서 appointment="NO"만 노출하도록 맞추면 된다(예약글은 사용자에게 아직 안 보이게).
스케줄러 기준점은 년/월/일/시/분/초 단위로 잡을 수 있고, Spring에서는 크게 두 방식이 많이 쓰인다.
fixedRate / fixedDelay: 단순히 “N초마다 실행” 같은 주기 실행에 적합cron: “매일 03:30:00”, “매 30분마다”처럼 실행 시점을 정밀하게 제어할 때 사용cron 표현식은 기본적으로 "초 분 시 일 월 요일" 순서로 이해하면 된다.
예시)
0 30 3 * * * : 매일 3시 30분 0초 실행0 0/30 * * * * : 매 30분마다 실행0 0/1 * * * * : 매 1분마다 실행스프링에서 스케줄러를 쓰는 절차는 다음 흐름이다.
@EnableScheduling 추가@Scheduled(...)를 붙인다@Component 등으로 스프링 빈으로 등록한다주의할 점은, 스케줄러 메서드가 DB 업데이트를 수행한다면 트랜잭션 범위를 명확히 잡아주는 게 좋다(예: 클래스 또는 메서드에 @Transactional 적용).
아래 코드는 “1분마다” 실행되면서 예약 글 중 예약 시간이 지난 글을 찾아 appointment를 NO로 바꾸는 예시다.
package com.beyond.basic.b2_board.post.service;
import com.beyond.basic.b2_board.post.domain.Post;
import com.beyond.basic.b2_board.post.repository.PostRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
@Slf4j
@Component
@Transactional
public class PostScheduler {
private final PostRepository postRepository;
public PostScheduler(PostRepository postRepository) {
this.postRepository = postRepository;
}
// (2) cron을 통해 작업 수행 미세 조정 가능
// - cron의 각 자리는 "초 분 시 일 월 요일"의 의미를 갖고있다.
// * * * * * * : 매월, 매일, 매시, 매분, 매초 마다의 의미
// 0 0 * * * * : 매월, 매일, 매시, 0분, 0초에 의미
// 0 0 11 * * * : 매월, 매일, 11시, 0분, 0초에 의미
// 0 0/1 * * * * : 매월, 매일, 매시, 1분 마다의 의미
@Scheduled(cron = "0 0/1 * * * *")
public void cronScheduler() {
log.info("==== 스케쥴러 시작 ====");
log.info("==== 스케쥴러 로직 수행 ====");
LocalDateTime now = LocalDateTime.now(); // 현재 시간
List<Post> postList = postRepository.findByAppointment("YES");
for (Post p : postList) {
if (p.getAppointmentTime().isBefore(now)) {
p.updateAppointment("NO");
}
}
log.info("==== 스케쥴러 끝 ====");
}
}
이 구조의 포인트는 “예약글만 조회”해서 비교하니까 불필요한 데이터 스캔을 줄이고, “예약 시간이 지난 것만 업데이트”해서 DB 변경도 최소화한다는 점이다.
서버가 1대일 때는 스케줄러가 한 번만 돌면 끝이다. 그런데 서버를 2대, 3대로 확장하면 같은 스케줄러 코드가 각 서버에서 동시에 실행될 수 있다.
그러면 이런 문제가 생긴다.
예를 들어 “정산 작업”처럼 매출 데이터를 집계해서 정산 테이블에 적재하는 작업은, 여러 서버가 동시에 돌면 같은 매출을 중복으로 집계/적재해서 결과 자체가 틀어질 수 있다.
멀티 서버에서 “한 번만 실행되게” 만들려면 스케줄러 실행 전에 락을 잡고, 락을 가진 서버만 작업을 수행하도록 제어해야 한다.
대표적인 방식이 Redis를 이용한 분산 락이다.
scheduler:settlement:lock)를 생성해서 “내가 실행 중”을 표시결과적으로 여러 서버가 떠 있어도 스케줄러는 한 대만 실행하는 구조가 된다.