존재하지만 한 번도 실행되지 않는 커맨드: 스케줄 등록을 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.php2. 명령줄로 교차 확인한다
테스트는 코드가 말하는 스케줄을 검사합니다. 운영 서버가 실제로 그 코드를 읽는지는 별개입니다.
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 상태를 검증한 것은 아닙니다.