v13.36.0의 컨테이너와 큐 워커, 그리고 Octane의 구현을 확인하고, 서비스 컨테이너의 기본 등록 방법을 패키지의 장기 유지보수와 연결합니다. scoped()를 선택하는 것만으로 객체 간의 참조나 정적 상태가 모두 리셋되는 것은 아닙니다.
등록 방법은 서비스의 보유 기간으로 선택한다
이것은 일반적인 해석을 비교한 표입니다. 파라미터를 명시한 해석이나 contextual binding 등 특수한 경로는 별도로 확인하세요.
bind()라도 조회한 서비스를 다른 singleton의 프로퍼티에 저장하면 그 참조는 남습니다. 등록 방법은 컨테이너의 생성·재사용 규칙이며, 사용자가 보유한 객체까지 강제로 다시 만드는 규칙이 아닙니다.
scoped는 공유 등록과 제거 대상 기록을 조합한다
확인한Container::scoped()는 다음 두 가지를 수행합니다.
- 등록 키를
scopedInstances에 기록합니다. singleton()을 호출하여 공유 바인딩으로 등록합니다.
instances에 저장됩니다. forgetScopedInstances()는 기록된 키에 대해 저장된 인스턴스를 unset()합니다. 바인딩의 생성 처리는 남아 있으므로 다음 일반 해석에서 새 객체를 만들 수 있습니다.
이 처리는 오래된 객체의 프로퍼티를 초기화하거나 다른 객체가 가진 참조를 바꾸지 않습니다. 참조가 남아 있는 오래된 객체는 계속 사용될 수 있습니다.
경계를 만드는 것은 실행 환경
컨테이너 자체가 HTTP 요청이나 잡의 시작을 감시하는 것은 아닙니다.- 큐 워커:
QueueServiceProvider가 스코프를 리셋하는 콜백을 Worker에 전달합니다.Worker::daemon()은 다음 잡을 가져오기 전 루프에서 이를 호출하며, 콜백 안에서forgetScopedInstances()와 Facade의 해석된 인스턴스 정리 등을 수행합니다. - Octane: 공식 기본 설정에서는
OperationTerminated에FlushTemporaryContainerInstances를 등록합니다. 이 리스너가forgetScopedInstances()를 호출합니다. - 자체 상주 커맨드나 루프: 단순히
make()를 반복해도 경계가 되지 않습니다. 실행 기반 쪽에서 라이프사이클을 설계해야 합니다.
처리 안에서 공유하는 상태를 scoped로 만든다
배송 패키지가 하나의 요청·잡 안에서 배송지 식별자를 공유하고 싶은 예를 생각해 봅시다. HTTP Request를 직접 보유하지 않고 필요한 값만 저장합니다.handle()에서 입력 검증과 권한 확인을 마친 식별자를 설정합니다. 같은 라이프사이클에서 이 타입을 해석하는 다른 서비스는 같은 context를 참조할 수 있습니다.
scoped()는 데이터베이스나 외부 캐시로의 영속화도 아닙니다. 다음 잡에 전달할 값은 잡의 데이터로 명시적으로 전달합니다.
Octane에서는 서비스 프로바이더의
register()와 boot()가 워커의 초기 기동 시에 실행됩니다. 현재 사용자나 배송지를 boot()에서 context에 저장하는 설계로 만들지 마세요. 처리별 값은 처리가 시작된 후에 설정합니다.singleton에서 scoped로의 참조는 자동으로 갱신되지 않는다
다음 등록은 context만 scoped로 만들어도 의도대로 동작하지 않습니다.DeliveryService가 생성자에서 context를 프로퍼티에 저장한다면, 처음 생성한 context를 계속 보유합니다. 컨테이너에서 context의 저장을 제거해도 이미 생성된 DeliveryService의 프로퍼티는 바뀌지 않습니다.
대책은 서비스의 책임에 맞게 선택합니다.
- 처리별로 필요한 값을 메서드 인수로 전달한다. 상태를 공유할 필요가 없다면 이것이 가장 단순합니다.
- 보유하는 쪽도 scoped로 만든다.
DeliveryService와 context를 모두 처리 단위로 다시 만듭니다. - 사용 시점에 해석하는 resolver를 전달한다. singleton을 유지해야 하는 경우에는 현재 컨테이너에서 필요한 서비스를 그때마다 가져옵니다.
DeliveryService는 생성자에서 Closure를 받아 작업할 때마다 호출하는 API로 변경해야 합니다. 생성자 안에서 resolver를 한 번만 호출하고 그 결과를 저장하면 해결되지 않습니다. 이는 공식 Octane 문서의 container resolver를 패키지 고유의 context에 응용한 설계 예입니다.
scoped로 사라지지 않는 상태도 확인한다
scoped()는 다음 상태를 자동으로 제거하지 않습니다.
- 패키지 고유의 정적 프로퍼티에 추가한 값
- 다른 singleton이나 전역 변수에 저장한 context에 대한 참조
- singleton 내부에 보유한 “현재 사용자”, “현재 배송지”
- 자체 Facade·캐시 계층이 보유한 참조
같은 컨테이너에서 경계를 넘는 테스트 작성하기
HTTP 테스트가 매번 새 애플리케이션을 만드는 것만으로는 상주 프로세스에서 같은 컨테이너를 계속 사용하는 문제를 놓칠 수 있습니다. 먼저 컨테이너 단독으로 같은 처리 안에서의 공유와 경계 이후의 재생성을 검증합니다. 다음 테스트는 PHPUnit 형식입니다. 첫 번째 테스트는 공식 Container 테스트와 마찬가지로forgetScopedInstances()를 사용하여, Worker 전체를 기동하지 않고 경계의 작용을 확인합니다.
컨테이너 단독 테스트는 Octane의 sandbox나 큐 워커의 실제 실행 경로를 검증하는 것이 아닙니다. 지원을 표명하는 실행 환경에서 연속 처리를 수행하는 통합 테스트도 필요합니다. 등록 방법이나 서비스 구현을 변경한 배포에서는 상주 프로세스를 다시 로드하여 새 코드를 적용합니다.
관련 페이지
패키지의 서비스 해석 후 훅
afterResolving과 공유 인스턴스 조회, 재해석 시 설정의 차이를 확인합니다.
Laravel Octane
Request나 컨테이너의 보유, 상주 프로세스 운영상의 주의점을 확인합니다.
참고한 1차 자료
- Laravel 공식 문서: Binding scoped singletons
- Laravel 공식 문서: Dependency injection and Octane
- Container: scoped()·singleton()·forgetScopedInstances()·resolve()
- ContainerTest: scoped의 공유와 재생성
- QueueServiceProvider: Worker에 전달하는 resetScope
- Worker: daemon()에서의 스코프 리셋
- Octane: FlushTemporaryContainerInstances
- Octane: OperationTerminated의 기본 리스너