본문으로 건너뛰기

런타임 중립 개발 규약

이 문서는 코드를 쓰는 시점의 규약입니다. 런타임을 선택하고 전환·롤백하는 운영 절차는 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. 코드 리뷰는 마커가 없는 위반을 지적하고, 마커가 있으면 이유의 타당성을 판정합니다.

검증

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

관련 문서