[Spring] 스케줄러로 예약 게시 기능 만들기(@Scheduled, cron)

이지연·2026년 1월 28일

예약 게시글 시나리오 기준으로 정리하면, 스케줄러는 “예약된 글 중에서 게시 시간이 된 것만 자동으로 게시 상태로 전환”해주는 백그라운드 작업이다. Spring에서는 @EnableScheduling + @Scheduled 조합으로 주기 실행을 쉽게 구현할 수 있고, 멀티 서버에서는 중복 실행을 막기 위한 분산 락(예: Redis)이 사실상 필수다.


스케줄러가 필요한 이유(예약 게시)

글쓰기에서 “게시 시간을 지정”하는 기능을 만들면 보통 아래 상태를 저장하게 된다.

  • 예약 안함(즉시 게시): appointment = "NO"
  • 예약 함(나중에 게시): appointment = "YES", appointmentTime = 2026-01-28 12:00:00 같은 예약시간 저장

그리고 시간이 흘러 예약 시간이 지나 실제로 게시되면, 더 이상 “예약글”이 아니므로 appointmentYES → NO로 바꾸는 흐름이 된다. 이 처리를 사람이 클릭해서 하는 게 아니라, 서버가 일정 간격으로 자동 수행해주는 역할이 바로 스케줄러다.


스케줄러가 하는 일(업무 로직)

예약 게시 시나리오에서 스케줄러 로직은 딱 4단계로 정리된다.

  • 예약여부가 YES인 글만 조회한다.
  • 현재시간(now)과 예약시간(appointmentTime)을 비교한다.
  • 예약시간이 지났으면 게시 처리(예: appointment = "NO"로 변경)한다.
  • 아직 시간이 안 됐으면 pass 한다.

즉, “조회는 NO인 글들만 하면 됨” 같은 정책을 만들고 싶다면, 게시 API/목록 조회 API에서 appointment="NO"만 노출하도록 맞추면 된다(예약글은 사용자에게 아직 안 보이게).


Spring Scheduling 핵심 옵션

스케줄러 기준점은 년/월/일/시/분/초 단위로 잡을 수 있고, 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)

스프링에서 스케줄러를 쓰는 절차는 다음 흐름이다.

  • 메인 애플리케이션(또는 설정 클래스)에 @EnableScheduling 추가
  • 스케줄링할 메서드에 @Scheduled(...)를 붙인다
  • 해당 클래스를 @Component 등으로 스프링 빈으로 등록한다

주의할 점은, 스케줄러 메서드가 DB 업데이트를 수행한다면 트랜잭션 범위를 명확히 잡아주는 게 좋다(예: 클래스 또는 메서드에 @Transactional 적용).


실습 코드 예시(예약 게시 처리)

아래 코드는 “1분마다” 실행되면서 예약 글 중 예약 시간이 지난 글을 찾아 appointmentNO로 바꾸는 예시다.

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대로 확장하면 같은 스케줄러 코드가 각 서버에서 동시에 실행될 수 있다.

그러면 이런 문제가 생긴다.

  • 같은 DB를 여러 서버가 동시에 조회/업데이트하면서 DB 부하 증가
  • 작업 성격에 따라 데이터가 꼬일 수 있음(중복 처리, 중복 적재)

예를 들어 “정산 작업”처럼 매출 데이터를 집계해서 정산 테이블에 적재하는 작업은, 여러 서버가 동시에 돌면 같은 매출을 중복으로 집계/적재해서 결과 자체가 틀어질 수 있다.


해결 아이디어: 분산 락(예: Redis)

멀티 서버에서 “한 번만 실행되게” 만들려면 스케줄러 실행 전에 락을 잡고, 락을 가진 서버만 작업을 수행하도록 제어해야 한다.

대표적인 방식이 Redis를 이용한 분산 락이다.

  • 실행 전 Redis에 key(예: scheduler:settlement:lock)를 생성해서 “내가 실행 중”을 표시
  • 다른 서버가 시작할 때 해당 key가 이미 존재하면 실행하지 않고 pass
  • 작업이 끝나면 락을 해제(또는 TTL 기반으로 자동 만료)

결과적으로 여러 서버가 떠 있어도 스케줄러는 한 대만 실행하는 구조가 된다.

profile
Eazy하게

0개의 댓글