본문 바로가기
← 아티클 목록
실무 가이드Laravel 13스케줄러운영Pest

존재하지만 한 번도 실행되지 않는 커맨드: 스케줄 등록을 Pest로 고정하기

작성: 라라벨 코리아

발행: 2026년 10월 10일

커맨드가 있고 단위 테스트도 통과하지만 스케줄러에 등록되지 않아 아무 일도 하지 않는 상황을 다룹니다. 실제 운영에서 넉 달 동안 비어 있던 커맨드 네 개의 사례를 바탕으로, 등록 여부와 실행 주기를 테스트로 확인하고 schedule:list로 교차 검증하는 절차를 정리합니다.

테스트가 전부 초록색인데 아무 일도 일어나지 않는다

라라벨 코리아에는 번역된 문서의 제목과 설명을 복구하는 커맨드, 새 패키지를 기존 문서와 다시 연결하는 커맨드, 아티클 초안을 만드는 커맨드가 있었습니다. 각각 테스트가 있었고 모두 통과했습니다. 그런데 넉 달 동안 한 번도 실행되지 않았습니다. 스케줄러에 등록하는 한 줄이 빠져 있었기 때문입니다.

이런 구멍은 기존 테스트로 잡히지 않습니다. 커맨드 테스트는 "실행하면 올바르게 동작한다"를 검사하지 "실행된다"를 검사하지 않습니다. 이 글은 두 번째 조건을 테스트로 만드는 방법입니다.

적용 환경

Laravel 13, Pest 4를 기준으로 합니다. Laravel 11 이후 스케줄은 보통 routes/console.php나 bootstrap/app.php의 withSchedule()에 정의합니다. 테스트는 정의 위치와 무관하게 컨테이너에서 Schedule 객체를 꺼내 확인합니다.

1. 스케줄 객체는 테스트에서 읽을 수 있다

스케줄에 등록된 항목은 Illuminate\Console\Scheduling\Schedule의 events()로 읽을 수 있습니다. 각 항목은 실행할 명령 문자열(command)과 크론식(expression)을 가집니다. 명령 문자열에는 PHP 실행 파일 경로와 artisan이 앞에 붙으므로 정확히 일치시키는 대신 커맨드 이름이 포함돼 있는지 봅니다.

라라벨 코리아의 사이트맵 재생성을 예로 든 테스트입니다. tests/Feature/ScheduleRegistrationTest.php에 넣습니다.

<?php use Illuminate\Console\Scheduling\Event; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Contracts\Console\Kernel; function scheduledEventFor(string $commandName): ?Event { // withSchedule() 콜백은 콘솔 애플리케이션이 시작될 때 등록된다. // HTTP 테스트에서는 아무도 시작하지 않으므로 커맨드 목록을 한 번 읽어 기동한다. app(Kernel::class)->all(); foreach (app(Schedule::class)->events() as $event) { if (str_contains((string) $event->command, ' '.$commandName)) { return $event; } } return null; } test('the sitemap is regenerated every night after the content pipeline', function () { config(['laravel-korea.automation.enabled' => true]); $event = scheduledEventFor('sitemap:generate'); expect($event)->not->toBeNull('sitemap:generate is not scheduled'); expect($event->expression)->toBe('0 4 * * *'); }); test('bulk editor approval is a manual action and never runs on a schedule', function () { expect(scheduledEventFor('content:approve'))->toBeNull(); }); test('every scheduled command name exists', function () { config(['laravel-korea.automation.enabled' => true]); $registered = array_keys(app(Kernel::class)->all()); foreach (app(Schedule::class)->events() as $event) { if (! str_contains((string) $event->command, "'artisan' ")) { continue; } $name = explode(' ', trim(explode("'artisan' ", (string) $event->command, 2)[1]))[0]; expect($registered)->toContain($name); } });

세 테스트가 각각 다른 실패를 잡습니다.

헬퍼 첫 줄이 커맨드 목록을 읽는 이유가 있습니다. Laravel 11 이후 withSchedule()에 적은 콜백은 콘솔 애플리케이션이 시작될 때 등록됩니다. HTTP 요청만 보내는 테스트에서는 아무도 콘솔을 시작하지 않아 Schedule이 비어 있고, 테스트는 "등록되지 않았다"고 잘못 결론 냅니다. $this->artisan()이나 $this->seed()를 먼저 호출한 테스트 파일에서는 우연히 통과하므로, 헬퍼 안에서 명시적으로 기동해 두는 편이 안전합니다.

  • 첫 번째는 등록 누락과 주기 변경입니다. 누군가 dailyAt('04:00')을 hourly()로 바꾸면 크론식이 달라져 실패합니다. 사이트맵은 아티클 검수가 끝난 뒤에 돌아야 하므로 시각 자체가 요구사항입니다. 라라벨 코리아의 콘텐츠 스케줄은 laravel-korea.automation.enabled 설정이 꺼져 있으면 아예 등록되지 않으므로, 테스트 첫 줄에서 플래그를 켜고 Schedule을 읽습니다. 플래그를 켜지 않은 채 "등록되지 않았다"고 결론 내리는 것이 이 테스트의 첫 번째 함정입니다.
  • 두 번째는 등록되면 안 되는 것입니다. 편집자가 직접 읽고 승인하는 커맨드가 스케줄에 올라가면 "사람이 검수했다"는 기록이 거짓이 됩니다. 등록을 막는 테스트는 등록을 요구하는 테스트만큼 중요합니다.
  • 세 번째는 오타입니다. $schedule->command('sitemap:genrate')는 등록 시점에 실패하지 않고 실행 시점에 "command not found"를 로그에 남깁니다. 등록된 모든 이름이 실제 커맨드 목록에 있는지 확인하면 배포 전에 잡힙니다.
php artisan test --compact tests/Feature/ScheduleRegistrationTest.php

2. 명령줄로 교차 확인한다

테스트는 코드가 말하는 스케줄을 검사합니다. 운영 서버가 실제로 그 코드를 읽는지는 별개입니다.

php artisan schedule:list

커맨드, 크론식, 다음 실행 시각이 표로 나옵니다. 배포 직후 이 출력을 저장해 두면 "어제까지는 돌았는데"를 확인할 근거가 됩니다. 특정 항목을 바로 실행해 보려면 schedule:test가 대화형으로 골라 줍니다.

그리고 서버의 crontab에 다음 한 줄이 있어야 합니다. 이것은 코드 테스트가 검증할 수 없는 부분입니다.

* * * * * cd /path/to/app && php artisan schedule:run >> /dev/null 2>&1

라라벨 코리아에서 넉 달 동안 비어 있던 커맨드들은 crontab이 있고 schedule:run도 매분 돌았지만, 스케줄 정의에 그 커맨드가 없었던 경우입니다. 반대로 정의는 완벽한데 crontab이 없는 서버도 흔합니다. 둘 다 확인해야 합니다.

3. 등록할 때 같이 정해야 하는 것

옵션언제 필요한가빠지면 생기는 일
withoutOverlapping()실행 시간이 주기보다 길어질 수 있을 때같은 작업이 겹쳐 돌아 DB 경합·중복 결과
onOneServer()웹 서버가 두 대 이상일 때서버 수만큼 중복 실행, 외부 API 할당량 소진
runInBackground()긴 작업이 뒤 항목을 막을 때같은 분의 다른 작업이 지연
environments(['production'])스테이징에서 돌면 안 될 때스테이징이 운영 데이터·외부 서비스를 건드림
when(fn () => config('...'))기능 플래그로 끄고 켤 때배포 없이 중단할 방법이 없음

onOneServer()는 캐시 잠금을 쓰므로 모든 서버가 같은 Redis·DB 캐시를 바라봐야 합니다. 서버별 파일 캐시에서는 효과가 없습니다. 라라벨 코리아는 외부 모델을 호출하는 작업 전부에 자동화 플래그를 달아 두어, 비용이 튀면 설정 하나로 멈출 수 있게 했습니다.

4. 순서가 있는 파이프라인은 시각으로 표현한다

라라벨 코리아의 야간 파이프라인은 02:50 초안 생성, 03:20 AI 검수, 03:30 번역 재시도, 04:00 사이트맵 순서입니다. 사이트맵이 검수보다 먼저 돌면 그날 승인된 글이 하루 늦게 색인됩니다. 이런 의존 관계는 코드에 주석으로만 두지 말고, 첫 번째 테스트처럼 크론식을 고정하거나 두 항목의 시각을 비교하는 테스트로 남기세요. 주석은 바뀌어도 아무것도 실패하지 않습니다.

커맨드가 등록되고 제때 도는 것을 확인했다면, 배포 뒤 워커와 스케줄러가 새 코드를 읽는지도 확인해야 합니다. 그 절차는 배포 검증 가이드에 있습니다.

출처와 검증 범위

Laravel 13 작업 스케줄링의 등록 방법·주기 표기·schedule:list·중복 방지 옵션과 콘솔 테스트 문서를 대조했습니다. 본문의 테스트는 이 사이트의 회귀 테스트에서 그대로 실행되며, 넉 달 공백 사례는 이 사이트의 변경 이력에 근거합니다. AI 작성·원문 대조이며, 운영 서버의 crontab 상태를 검증한 것은 아닙니다.