메인 콘텐츠로 건너뛰기
패키지를 업데이트해도 업데이트 전에 디스패치한 잡을 처리할 수 있도록 합니다. 큐에 저장되는 것은 실행할 코드가 아니라 잡 클래스의 이름과 상태입니다. 새 코드의 단위 테스트뿐 아니라 이전 코드가 저장한 상태를 새 코드로 복원하는 테스트가 필요합니다.
이 페이지는 Laravel 13을 대상으로 Framework v13.42.0의 구현을 확인했습니다. 큐의 기본과 패키지 테스트를 전제로 합니다. 아래 호환성 설계는 패키지 측의 방침이며, Laravel이 자체 페이로드 형식을 자동으로 마이그레이션하는 기능이 아닙니다.

저장과 실행 사이에 코드가 바뀐다

객체 잡에서는 Queue::createObjectPayload()가 serialize(clone $job)를 실행하고 결과를 페이로드의 data.command에 넣습니다. 암호화하는 경우에도 그 앞 단계에서 같은 직렬화가 이루어집니다. 워커 측의 CallQueuedHandler::getCommand()는 저장된 데이터를 unserialize()하고, 복원한 잡을 Dispatcher에 넘깁니다. 일반적인 handle()은 그 시점에 동작 중인 워커의 코드와 서비스 컨테이너로 실행됩니다. 장기 실행 워커를 재시작하지 않으면 업데이트 후에도 이전 코드가 계속 동작합니다. 롤링 배포에서는 “이전 디스패치 측과 새 워커”뿐 아니라 “새 디스패치 측과 이전 워커” 조합도 고려합니다. 클래스의 이름 변경이나 삭제는 사용자가 그 클래스를 직접 호출하는지만으로 판단하지 않습니다. 대기 중·지연 중·재시도 대기 중인 잡과 실패한 잡의 보존 기간도 호환성의 대상입니다.

SerializesModels가 저장하는 것

Illuminate\Foundation\Queue\Queueable은 내부에서 SerializesModels를 사용합니다. 이는 모델 치환뿐 아니라, 잡 자신의 프로퍼티를 나열하는 __serialize()와 값을 되돌리는 __unserialize()도 제공합니다.

선언 시 기본값과 같은 값은 저장되지 않는다

__serialize()는 static·미초기화 등의 프로퍼티를 제외하고, 프로퍼티의 선언 시 기본값과 현재 값이 엄격하게 같은 경우에도 저장을 생략합니다.
이 프로퍼티가 'csv' 그대로라면 저장 배열에 format이 들어가지 않습니다. 이후 릴리스에서 선언 시 기본값을 'json'으로 바꾸면 이전 잡에도 'json'이 적용될 수 있습니다. 단순한 겉모습의 기본값 변경이 아니라 이전 데이터를 읽는 방식의 변경입니다.

복원 시 생성자는 다시 호출되지 않는다

__unserialize()는 현재 클래스에 존재하고 저장 배열에도 키가 있는 프로퍼티에 값을 대입합니다. 생성자를 통한 입력 보완이나 검증은 실행하지 않습니다.
이 예의 'csv'는 생성자 인수의 기본값입니다. 프로퍼티 자체의 선언 시 기본값이 아니므로, format이 없는 이전 페이로드를 복원할 때 보완되지 않습니다. 추가한 타입 지정 프로퍼티가 미초기화 상태로 남으면 나중에 접근할 때 실패합니다. 또한 public의 키는 이름 그대로, protected는 가시성을 나타내는 접두사가 붙은 키, private은 클래스명도 포함한 키가 됩니다. 프로퍼티를 public에서 private으로 바꾸는 변경도 이전 페이로드로 검증합니다.
SerializesModels가 나열하는 것은 잡의 프로퍼티입니다. 임의의 DTO 내부나 배열 깊숙이 있는 모델까지 재귀적으로 같은 변환이 이루어진다고 생각하지 않습니다. 모델 이외의 객체를 넘기는 경우 그 클래스와 저장 형식도 호환성의 대상이 됩니다.

식별자와 실행 서비스를 분리한다

실행에 필요한 식별자나 선택지는 잡에 저장하고, API 클라이언트나 Exporter 같은 실행 서비스는 handle()에 주입합니다. 생성자에 서비스를 넘겨 보관하면 그 내부 상태까지 저장 대상이 될 수 있습니다. 다음은 이전 버전에서는 reportId만 가지고 있던 같은 이름의 잡에 형식 선택과 페이로드 버전을 추가하는 예입니다. ReportExporter는 패키지가 제공하고 컨테이너에 bind하는 계약으로, export(string $reportId, string $format): void를 가진다고 가정합니다.
이 설계에서는 누락된 값의 의미와 새로 디스패치하는 값을 분리합니다.
  • 이전 페이로드에 format이 없으면 선언 시 기본값인 'csv'를 사용한다.
  • 이전 페이로드에 payloadVersion이 없으면 선언 시 기본값인 1로 취급한다.
  • 새로 생성할 때는 생성자가 payloadVersion을 2로 설정한다. 선언 시 기본값과 다르므로 이 값은 저장된다.
  • 새 잡의 format이 'json'이면 그 값도 저장된다. 'csv'면 생략되므로 선언 시 기본값 'csv'를 앞으로도 유지한다.
나중에 새 잡의 기본 형식만 바꾸려면 생성자 인수의 기본값과 대입을 변경하고, 이전 데이터용 프로퍼티 기본값과는 분리해서 검토할 수 있습니다. 페이로드 버전은 패키지의 릴리스 번호가 아니라 저장 형식과 해석의 세대로 관리합니다.
버전 번호를 추가하는 것만으로는 호환성을 얻을 수 없습니다. 이 예의 새 버전은 이전 형식도 처리할 수 있지만, 이전 버전의 워커가 새 format이나 payloadVersion을 이해한다는 보장은 없습니다. 뒤에서 설명할 디스패치 측과 워커의 업데이트 순서도 필요합니다.

모델을 넘기는 경우는 스냅숏과 구별한다

모델을 직접 프로퍼티에 저장하면 SerializesAndRestoresModelIdentifiers는 클래스·ID·연결명·로드된 관계명 등을 ModelIdentifier로 치환합니다. 단일 모델을 복원할 때는 DB에서 firstOrFail()로 다시 조회하고 대상 관계를 로드합니다. 이는 디스패치 시점의 속성이나 관련 데이터를 고정하는 구조가 아닙니다.
  • 실행 전에 레코드가 업데이트되면 복원 후에는 업데이트된 내용을 참조한다.
  • 미리 적용한 관계의 필터 조건은 저장되지 않으므로, 필요한 조건은 실행 시에 다시 붙인다.
  • 불필요한 관계는 withoutRelations()나 #[WithoutRelations]로 저장 대상에서 제외한다.
  • 모델이 삭제된 경우 잡의 handle() 이전의 복원 단계에서 실패할 수 있다.
삭제된 모델의 잡을 폐기해도 된다면 공식 #[DeleteWhenMissingModels]를 검토할 수 있습니다. 다만 패키지가 임의로 폐기를 기본 동작으로 삼지 말고, 사용자에게 실패 방침을 공개합니다. ID만 넘기고 handle() 안에서 검색하는 설계라면 그 검색의 실패나 건너뛰기는 직접 제어합니다. 디스패치 시점의 내용이 필요하다면 필요한 값을 명시적으로 저장하거나, 불변의 리포트 요청 등을 DB에 기록하고 그 ID를 넘깁니다. 단순히 모델을 넘기는 것과 업무 데이터의 스냅숏을 저장하는 것을 구별합니다.

커밋 후 디스패치는 별개의 문제

트랜잭션 중에 생성한 레코드를 처리하는 경우 커밋 전에 워커가 읽기 시작하지 않도록 합니다.
afterCommit()은 열려 있는 부모 트랜잭션이 커밋될 때까지 디스패치를 기다리고, 롤백 시에는 그 디스패치를 폐기합니다. 클래스명·프로퍼티의 호환성이나 외부 처리의 중복 실행을 해결하는 기능이 아닙니다.

fake와 이전 페이로드 테스트를 분리한다

일반적인 Queue::fake()는 디스패치를 기록하기 위한 것입니다. assertPushed()가 통과했다고 해서 저장·복원이나 새 버전에 의한 이전 페이로드 처리를 확인한 것은 아닙니다. 현재 클래스에서의 왕복은 다음과 같이 검증할 수 있습니다.
serializeAndRestore()는 fake 안에서 serialize()와 unserialize()를 실행합니다. 다만 실제 백엔드로의 디스패치·워커에 의한 실행·이전 버전 코드에서의 생성은 검증하지 않습니다.

이전 버전에서 생성한 fixture를 남긴다

이전 버전의 GenerateReport가 reportId만 가진 상태에서, 실제 데이터를 포함하지 않는 고정된 잡을 생성합니다. 아래는 이전 버전의 테스트 환경에서 한 번 실행하는 준비용 코드입니다. tests/fixtures/는 미리 준비합니다.
fixture를 버전 관리하고 새 버전의 테스트에서 읽어 들입니다. 테스트할 때마다 새 버전으로 다시 생성하면 하위 호환성 검증이 아니게 되므로 고정한 채로 둡니다.
이 예의 TestCase는 패키지 테스트 환경에서 준비하는 기본 클래스입니다. unserialize()에는 직접 관리하는 fixture만 넘기고 외부 입력을 넘기지 마세요. 또한 복원한 이전 잡의 handle()을 테스트용 Exporter로 실행해 출력과 저장 결과까지 검증합니다. 모델을 넘기는 경우는 업데이트·삭제·연결·관계 재조회를, 실제로 사용하는 큐 연결에서는 커밋 후 디스패치와 재시도도 별도로 검증합니다.

새 형식을 디스패치하기 전에 읽는 쪽을 업데이트한다

1

이전 형식을 읽을 수 있는 릴리스를 준비한다

이전 클래스명과 이전 값의 해석을 유지하면서 새 형식도 읽을 수 있는 코드를 먼저 배포합니다. fixture로 복원뿐 아니라 처리 결과도 확인합니다.
2

이전 워커가 남지 않았는지 확인한다

사용하는 앱의 배포 절차에 워커 교체를 포함합니다. 일반적인 queue:work에서는 php artisan queue:restart로 현재 잡 처리 후의 종료를 요청하고, 프로세스 매니저가 새 버전을 기동하는지 확인합니다. 재시작 시그널에는 캐시가 사용됩니다.
3

새 형식의 디스패치를 활성화한다

읽는 쪽이 대응한 후에 기능 플래그 등으로 새 형식의 디스패치로 전환합니다. 같은 코드의 배포만으로 새 형식의 생성도 시작되는 경우, 디스패치를 일시 정지하거나 새·이전 큐와 대응하는 워커를 분리하는 등 이전 워커에 넘기지 않는 절차가 필요합니다.
4

재시도와 롤백 기간도 유지보수한다

실패한 잡의 재시도나 체인 안에서 나중에 디스패치되는 잡도 확인합니다. 새 형식을 저장한 후 이전 코드로 되돌린다면 이전 코드에서도 읽을 수 있는지 검증합니다. 큐가 한 번 비었다는 것만으로 이전 클래스나 이전 형식의 대응을 삭제하지 않습니다.
queue:restart는 저장된 페이로드를 다시 쓰는 커맨드가 아닙니다. 패키지의 업데이트 안내에는 지원 형식, 필요한 워커 교체, 이전 형식을 폐지하는 조건을 명시합니다. 버전 호환성 관리와 Changelog 운용에도 같은 변경을 기록합니다.

참조한 1차 정보

마지막 수정일 2026년 11월 12일