본문으로 건너뛰기

런타임 중립 개발 규약

이 문서는 코드를 쓰는 시점의 규약입니다. 런타임을 선택하고 전환·롤백하는 운영 절차는 FPM과 Octane/RoadRunner 런타임을 참고하세요.

왜 필요한가​

플랫폼의 Laravel 코드는 PHP-FPM과 Laravel Octane 양쪽에서 같은 보안·권한·테넌트 의미를 유지해야 합니다. 어떤 프로젝트가 언제 Octane을 켜든, 그 시점에 코드를 고쳐야 한다면 이미 늦습니다.

두 런타임의 차이​

PHP-FPMOctane
프로세스 수명요청 1건 — 끝나면 메모리를 통째로 폐기워커가 계속 살아 수천 건을 처리
전역 상태다음 요청에 남을 수 없음명시적으로 지우지 않으면 그대로 남음

어겼을 때 무슨 일이 생기는가​

PHP-FPM에서는 아무 일도 생기지 않습니다. 프로세스가 매번 종료되므로 잘못된 코드도 정상으로 보입니다. 문제는 Octane을 켜는 순간 드러납니다.

남는 것결과
인증 사용자와 guard다음 요청이 앞 사용자로 인식됨
SaaS·테넌트·조직 컨텍스트, RLS 세션다른 테넌트의 데이터가 조회됨
권한 판정 컨텍스트하위 관리자가 상위 권한으로 통과
locale·timezone·debug다른 사용자에게 잘못된 언어·시간·디버그 정보 노출

즉 이것은 성능 규약이 아니라 보안 규약입니다.

FPM에도 비용이 없습니다​

규약을 지킨 코드는 FPM에서 동작도 성능도 동일합니다. 잃는 것이 없으므로 "지금은 Octane을 쓰지 않으니 나중에"라고 미룰 이유가 없습니다. 미루면 그 사이 쌓인 코드를 나중에 전수 조사해야 합니다.

규칙 요약​

카테고리판정대신 사용할 방법
scoped_binding허용요청 스코프 상태의 권장 패턴
rls_session_read허용읽기는 안전
singleton_binding조건부부팅 시점 불변 서비스만. 요청·사용자·테넌트 상태를 필드에 담지 않기
static_local_cache조건부입력에 독립적인 순수 계산 결과만 캐시
container_context_mutation조건부새 컨텍스트 키는 정리 대상에 반드시 등록
rls_session_write조건부요청 종료 시 리셋 보장 필수
global_locale_mutation조건부표준 미들웨어 경로 사용
mutable_static_property금지인스턴스 프로퍼티와 의존성 주입
runtime_config_mutation금지값을 인자로 전달
request_container_capture금지메서드 인자로 전달받기
auth_state_mutation금지표준 guard와 미들웨어
global_timezone_mutation금지UTC 저장 후 표시 시점 변환
provider_boot_capture금지boot은 등록만, 값은 해석 시점에 읽기

대표 사례​

싱글톤이 요청 상태를 보관하는 경우​

// 위험 — 첫 요청의 테넌트가 워커 수명 동안 고정됩니다
$this->app->singleton(ReportBuilder::class, function () {
return new ReportBuilder(tenant: current_tenant());
});

// 안전 — 상태는 호출 시점에 전달합니다
$this->app->singleton(ReportBuilder::class, fn () => new ReportBuilder());
// 사용: $builder->for(current_tenant())->build();

요청마다 새 인스턴스가 필요하면 scoped 바인딩을 사용하세요.

함수 내부 static 캐시​

// 위험 — 첫 요청의 설정이 고정됩니다
function currentSaasSettings(): array {
static $cache = null;
return $cache ??= current_saas()->settings;
}

// 안전 — 입력에 독립적인 값만 캐시합니다
function supportedLocales(): array {
static $cache = null;
return $cache ??= array_keys(config('app.available_locales'));
}

판단 기준은 하나입니다. 같은 워커에서 다른 사용자·테넌트가 호출해도 같은 값이어야 하는가.

런타임 설정 변경​

// 위험 — 워커 전역 설정을 오염시킵니다
config(['services.kakao.client_id' => $saas->kakao_client_id]);

// 안전 — 값을 그때그때 전달합니다
$client = new KakaoClient($saas->kakao_client_id);

ServiceProvider boot에서 요청 상태 읽기​

boot()은 워커당 한 번만 실행됩니다. 여기서 요청 상태를 읽어 보관하면 그 값이 이후 모든 요청에 적용됩니다.

// 위험 — 첫 요청의 값이 고정됩니다
public function boot(): void {
View::share('tenant', current_tenant());
}

// 안전 — 해석 시점에 읽습니다
public function boot(): void {
View::composer('*', fn ($view) => $view->with('tenant', current_tenant()));
}

시간대​

프로세스 전역 시간대를 바꾸지 않습니다. 저장은 UTC로 하고 표시 시점에 변환합니다.

// 위험
date_default_timezone_set($user->timezone);

// 안전
$when->timezone($user->display_timezone ?? config('app.display_timezone'));

요청 경계 계약​

조건부 규칙 대부분은 "요청이 끝날 때 정리되도록 등록하라"로 수렴합니다. Core는 그 계약을 제공합니다.

구성요소역할
런타임 생명주기 코디네이터요청·작업 경계에서 begin()/end()를 실행하는 단일 계약
HTTP 경계 미들웨어웹 요청 경계
큐 경계 리스너큐 작업 경계
리셋터 5종인증·테넌시, RLS 세션, 도메인 컨텍스트, 설정 상태, 컨테이너 컨텍스트

이 계약은 Octane 전용이 아닙니다. HTTP 경계 미들웨어와 큐 경계 리스너는 FPM에서도 등록되어 동작하므로, 리셋터에 등록해 두면 런타임과 무관하게 같은 의미가 보장됩니다.

또한 Core는 Octane 패키지에 의존하지 않습니다. 이 무의존은 문서상의 약속이 아니라 테스트로 강제되며, Octane 결합은 선택형 런타임 플러그인 안에만 존재합니다.

플러그인이 자기 컨텍스트 키를 등록하는 방법​

플러그인이나 프로젝트가 컨테이너에 요청 컨텍스트를 바인딩한다면, Core의 기본 목록을 고치지 말고 부팅 시점에 자기 키를 등록하세요. 컨테이너 컨텍스트 리셋터는 그 확장점을 제공하며, 리셋터 레지스트리는 싱글톤이므로 부팅 시 얻은 인스턴스가 실제 요청 종료 시 동작하는 인스턴스입니다.

Core 파일을 직접 수정하면 다음 업데이트에서 충돌합니다. 반대로 SaaS 식별자로 파생되는 캐시 키({고정 접두사}.{현재 SaaS id} 형태)는 리셋터가 자동으로 조합해 정리하므로 별도 등록이 필요 없습니다.

예외​

기계적인 허용 목록은 두지 않습니다. 예외는 감추지 않고 드러냅니다.

  1. 코드에 마커 주석을 남깁니다.
// runtime-neutral: singleton_binding 예외 — 부팅 시점 불변 설정 캐시, 요청 상태 미보유
  1. 규약 문서의 예외 목록에 파일·카테고리·이유·승인일을 기록합니다.
  2. 코드 리뷰는 마커가 없는 위반을 지적하고, 마커가 있으면 이유의 타당성을 판정합니다.

검증​

플랫폼은 위 카테고리를 자동으로 탐지하는 감사 스크립트를 제공합니다. 감사 결과의 카테고리 이름은 이 문서의 항목명과 같으므로 바로 대조할 수 있습니다. 코드 리뷰 단계에서도 동일한 기준이 규칙으로 적용됩니다.

관련 문서​