매 요청 균일하게 발생하던 부팅 오버헤드를 4개 축에서 제거한다. - 설치 완료 상태에서 매 요청 반복되던 information_schema 조회(알림·본인인증 테이블 hasTable)를 installer_completed 가드로 스킵. 미설치 환경은 기존 hasTable 폴백 유지. - 코어+모듈+플러그인 정적 훅 매핑을 bootstrap/cache/hooks.php 에 사전 계산해 매 요청 디렉토리 스캔·리플렉션·클래스 로딩을 제거(route:cache 동형). 등록↔발화 계약·매핑 바이트 동일. 확장/코어 변경 시 자동 재생성, 캐시 부재·손상은 스캔 폴백. - 확장 소스(Modules\*/Plugins\*)를 autoload-extensions.php 의 classmap 에 편입해 findFile 파일시스템 스캔을 제거(느린 FS·cold OPcache 환경 직격). 클래스 로딩은 여전히 lazy, PSR-4 폴백 유지. - config 캐시를 변경 지점(설정 저장/코어·확장 업데이트/APP_KEY 재생성/설치 완료)에서 clear 후 즉시 재생성하도록 ConfigCacheHelper 로 일원화. clear 만 하고 방치돼 캐시가 영구 비활성으로 남던 성능 손실 제거. HookCacheManager::read 요청당 1회 로드(memo)로 중복 파싱 제거. 부팅 매핑 fingerprint 스캔↔캐시 완전 동일(229 action + 35 filter). 신규/수정 8개 스위트 54 pass. 순수 내부 부팅 인프라 — 확장 공개 표면·훅 계약 불변.
19 KiB
ExtensionManager (확장 관리자)
소스: extension-guide.md에서 분리 관련 문서: module-basics.md, plugin-development.md
TL;DR (5초 요약)
1. composer.json 수정 없음 - 런타임 오토로드 방식 사용
2. 캐시 파일: bootstrap/cache/autoload-extensions.php (gitignore 대상)
3. 설치/삭제 시 자동 갱신: updateComposerAutoload() → generateAutoloadFile()
4. 수동 갱신: php artisan extension:update-autoload
5. _bundled/_pending 디렉토리 자동 제외 (str_starts_with($name, '_') → skip)
6. src_classmap: 확장 소스 FQCN→경로 사전 계산 → findFile 파일시스템 stat 제거 (지연 로드·vendor 격리 유지)
목차
개요
모듈과 플러그인에서 공통으로 사용되는 기능을 담당하는 관리자 클래스입니다. 주로 오토로드 설정 관리를 담당합니다.
주요 역할:
- 런타임 오토로드 캐시 파일 생성
- 모듈/플러그인 설치 시 PSR-4 네임스페이스 등록
- Composer ClassLoader에 동적 등록
런타임 오토로드 방식
핵심 원칙
중요: composer.json을 수정하지 않음
✅ 결정: 런타임 오토로드 방식 사용 (캐시 파일 기반)
✅ 장점: 버전 관리 충돌 없음, composer dump-autoload 불필요
동작 방식
모듈/플러그인 설치/삭제 시
↓
ExtensionManager::updateComposerAutoload() 호출
↓
bootstrap/cache/autoload-extensions.php 파일 생성 (gitignore 대상)
↓
public/index.php 또는 artisan 실행 시
↓
$loader->addPsr4()로 런타임에 네임스페이스 등록
캐시 파일 구조
// bootstrap/cache/autoload-extensions.php
return [
'psr4' => [
"Modules\\Sirsoft\\Ecommerce\\" => "modules/sirsoft-ecommerce/src/",
"Plugins\\Sirsoft\\Payment\\" => "plugins/sirsoft-payment/src/",
],
'classmap' => [
"modules/sirsoft-ecommerce/module.php",
"plugins/sirsoft-payment/plugin.php",
],
'src_classmap' => [
"Modules\\Sirsoft\\Ecommerce\\Services\\OrderService" => "modules/sirsoft-ecommerce/src/Services/OrderService.php",
"Plugins\\Sirsoft\\Payment\\Providers\\PaymentServiceProvider" => "plugins/sirsoft-payment/src/Providers/PaymentServiceProvider.php",
],
'files' => [],
'vendor_autoloads' => [
"modules/sirsoft-ecommerce/vendor/autoload.php",
],
];
classmap:module.php/plugin.php등 부트스트랩 파일 목록 — 진입점에서require_once(즉시 로드)src_classmap: 확장 소스 클래스의FQCN → 상대경로맵 — 진입점에서$loader->addClassMap()(지연 로드)vendor_autoloads: 모듈/플러그인의vendor/autoload.php경로 목록 (Composer 의존성이 설치된 경우만 포함)
src_classmap — findFile 파일시스템 스캔 제거 (성능)
확장 소스 클래스는 addPsr4() 로만 등록되면 Composer findFile() 이 클래스마다 파일시스템을 스캔(is_dir/file_exists stat)한다. OPcache 가 cold 이거나 파일시스템이 느린 환경에서는 이 stat 비용이 요청 시간의 큰 비중을 차지할 수 있다. src_classmap 은 generateAutoloadFile() 이 각 PSR-4 디렉토리를 스캔해 FQCN → 경로 를 사전 계산하고, 진입점(public/index.php / CoreServiceProvider / ExtensionManager::registerExtensionAutoload)이 ClassLoader::addClassMap() 으로 등록한다. findFile() 은 classMap 을 최우선 조회하므로 확장 클래스는 파일시스템 접근 0 으로 즉시 경로를 얻는다.
- 지연 로드 유지: classmap 은 경로만 제공하며
include는 실제 사용 시점에. 매 요청 전량 로드가 아니다. - PSR-4 폴백 보존: classmap 에 없는 클래스(신규 추가 후 미재생성 등)는 기존 PSR-4 경로로 폴백 — 안전망.
- vendor 격리 불변:
src_classmap은 확장 소스(Modules\*/Plugins\*)만 담는다. 확장별 독립 서드파티 vendor 는 각 확장vendor/autoload.php(별도 ClassLoader) 가 담당하므로 여기 포함되지 않는다. - 재생성:
psr4와 동일 생명주기 — install / update / uninstall 시updateComposerAutoload(), 코어 업데이트 시extension:update-autoload가 함께 재생성.
중요 규칙
설치된 모듈/플러그인만 캐시 파일에 반영됩니다
_bundled/_pending 디렉토리는 활성 확장으로 인식하지 않습니다
- 디렉토리에 파일이 있어도 DB에 설치 기록이 없으면 반영되지 않음
- identifier는 디렉토리명과 동일 (예:
sirsoft-sample,sirsoft-payment) _로 시작하는 디렉토리(_bundled,_pending)는 디렉토리 스캔 시 자동 제외됨- 캐시 파일에는 활성 디렉토리 경로만 포함 (
_bundled/경로 미포함)
수행 작업
ModuleRepository와PluginRepository를 통해 설치된 확장 목록 조회- 각 모듈/플러그인의
composer.json에서 PSR-4 오토로드 정보 수집 bootstrap/cache/autoload-extensions.php파일 생성module.php,plugin.php파일을classmap에 추가
로딩 진입점
public/index.php 및 artisan에서 ExtensionManager::registerExtensionAutoload($loader) 호출:
registerExtensionAutoload($loader)
↓ 1. PSR-4 네임스페이스 등록 ($loader->addPsr4())
↓ 2. Classmap 파일 로드 (module.php, plugin.php → require_once)
↓ 3. Files 로드 (헬퍼 함수 등 → require_once)
↓ 4. Vendor autoloads 로드 (모듈/플러그인의 vendor/autoload.php → require_once)
로딩 순서가 중요합니다:
| 순서 | 유형 | 설명 |
|---|---|---|
| 1 | PSR-4 | 확장의 src/ 디렉토리 네임스페이스를 Composer ClassLoader에 등록 |
| 2 | Classmap | module.php, plugin.php 엔트리 파일을 즉시 로드 |
| 3 | Files | 헬퍼 함수 등 자동 로드 파일 |
| 4 | Vendor autoloads | 각 확장의 vendor/autoload.php를 require_once하여 외부 패키지 로드 |
- 캐시 파일 미존재 시 아무 작업도 하지 않고 return (설치 전 상태)
- 경로가 존재하지 않는 항목은 자동 스킵 (
is_dir()/file_exists()체크)
Composer 의존성 관리
모듈/플러그인이 외부 Composer 패키지(예: stripe/stripe-php)를 필요로 할 때, 각 확장의 composer.json에 require로 정의하고 확장별 독립 vendor/ 디렉토리에 설치합니다.
핵심 원칙
중요: 루트 composer.json에 모듈/플러그인 패키지 추가 금지
✅ 결정: 각 확장의 composer.json → 확장별 vendor/ 디렉토리에 독립 설치
✅ 장점: 확장 간 의존성 충돌 방지, 삭제 시 깔끔한 정리
Vendor 설치 모드 (VendorMode)
공유 호스팅 등 Composer 실행 불가 환경을 지원하기 위해 VendorResolver 경유로 설치 모드를 결정합니다.
VendorMode enum:
- auto: composer 가능 시 composer, 불가 시 bundled (기본값)
- composer: composer install 강제 (불가 시 예외)
- bundled: vendor-bundle.zip 추출 강제
설치/업데이트 시 installModule() / installPlugin() / updateModule() / updatePlugin() 의 VendorMode $vendorMode 파라미터로 전달되며, VendorResolver::install() 가 환경 감지 + DB 기록 이전 모드 상속 + 번들 zip 무결성 검증을 거쳐 적절한 전략으로 라우팅합니다. modules.vendor_mode / plugins.vendor_mode 컬럼에 최종 사용된 모드가 기록되어 업데이트 시 자동 상속됩니다.
상세: vendor-bundle.md — 번들 구조, 빌드/검증/설치 흐름, 무결성 검증
동작 방식
모듈/플러그인 설치 시 (Phase 4.5)
↓
hasComposerDependencies() → 외부 패키지 존재 여부 확인
↓ (존재 시)
runComposerInstall() → 확장 디렉토리에서 composer install 실행
↓
modules/{name}/vendor/ 또는 plugins/{name}/vendor/ 생성
↓
updateComposerAutoload() → autoload-extensions.php에 vendor_autoloads 등록
↓
public/index.php / artisan → vendor/autoload.php를 require_once
주요 메서드
| 메서드 | 시그니처 | 설명 |
|---|---|---|
hasComposerDependencies |
(string $type, string $dirName): bool |
외부 패키지 의존성 존재 여부 (php, ext-* 제외) |
getComposerDependencies |
(string $type, string $dirName): array |
외부 패키지 목록 반환 |
runComposerInstall |
(string $type, string $dirName, bool $noDev, ?Command $command): bool |
확장 디렉토리에서 composer install 실행 |
detectDuplicatePackages |
(): array |
여러 확장에서 중복 사용되는 패키지 감지 |
isComposerUnchanged |
(string $stagingPath, string $activePath): bool |
스테이징/활성 디렉토리의 composer 변경 여부 확인 |
Composer 의존성 변경 감지 (isComposerUnchanged)
업데이트 시 불필요한 composer install을 스킵하기 위해 composer.json과 composer.lock을 이중 해시 비교합니다:
isComposerUnchanged(stagingPath, activePath)
↓ 활성 디렉토리 또는 vendor/ 미존재 → false (설치 필요)
↓ composer.json 한쪽 미존재 → false (변경으로 간주)
↓ md5_file(staging/composer.json) !== md5_file(active/composer.json) → false
↓ composer.lock 존재 여부 불일치 → false
↓ md5_file(staging/composer.lock) !== md5_file(active/composer.lock) → false
↓ 모두 일치 → true (스킵 가능)
- 이중 비교:
composer.json(의존성 선언) +composer.lock(실제 설치 버전) 모두 확인 - false 반환 = 설치 필요: 하나라도 변경되었으면
composer install실행 - true 반환 = 스킵 가능: 기존
vendor/디렉토리 재사용
runComposerInstall 환경변수
웹 서버 환경에서 Composer 실행 시 시스템 환경변수가 부족하므로, install-worker.php 패턴과 동일하게 환경변수를 명시적으로 구성합니다.
| 환경변수 | 출처/설정값 | 용도 |
|---|---|---|
| PATH, SystemRoot | getenv()으로 시스템에서 복사 |
Composer 바이너리 탐색 |
| TEMP, TMP | 시스템에서 복사, 불가 시 storage/temp |
임시 파일 (Windows 특이) |
| APPDATA, LOCALAPPDATA, USERPROFILE | getenv()으로 시스템에서 복사 |
Windows 사용자 경로 |
| COMPOSER_HOME | storage/composer |
Composer 캐시 디렉토리 |
| HOME | storage/composer |
Unix-style 홈 디렉토리 |
삭제 시 vendor 정리
InspectsUninstallData trait이 제공하는 메서드:
| 메서드 | 설명 |
|---|---|
getVendorDirectoryInfo(string $type, string $dirName): ?array |
vendor/ 디렉토리 크기 정보 조회 (삭제 모달 표시용) |
deleteVendorDirectory(string $type, string $dirName): void |
vendor/ 디렉토리 및 composer.lock 삭제 |
- 삭제 시점:
uninstallModule/uninstallPlugin에서$deleteData === true일 때 실행 - 실패 처리: 삭제 실패 시 예외를 catch하고 로그만 남김 (프로세스 중단하지 않음)
Artisan 커맨드
extension:update-autoload
모듈과 플러그인의 오토로드 캐시 파일을 생성합니다.
php artisan extension:update-autoload
사용 시점:
- 모듈/플러그인 디렉토리를 수동으로 추가/삭제한 경우
- 오토로드 캐시 파일이 손상된 경우
- 개발 중 오토로드 문제 디버깅
출력 예시:
$ php artisan extension:update-autoload
확장 오토로드 파일을 생성합니다...
오토로드 파일이 성공적으로 생성되었습니다.
→ bootstrap/cache/autoload-extensions.php
extension:composer-install
모든 모듈과 플러그인의 Composer 의존성을 일괄 설치합니다.
php artisan extension:composer-install
옵션:
--no-dev: dev 의존성 제외
사용 시점:
- 프로젝트 초기 설정 시 모든 확장의 의존성을 한번에 설치
- 배포 환경 구성 시
동작:
module:composer-install --all과plugin:composer-install --all을 순차 호출
식별자 검증 규칙 (ValidExtensionIdentifier)
모듈/플러그인/템플릿 설치 시 ValidExtensionIdentifier 규칙으로 식별자 형식을 공통 검증합니다:
| 규칙 | 설명 | 예시 |
|---|---|---|
| 하이픈 1차 구분 | 최소 2개 부분 필수 (vendor-name) | sirsoft-board ✅ / sirsoftboard ❌ |
| 언더스코어 2차 구분 | 이름 내 단어 구분 (선택적) | sirsoft-daum_postcode ✅ |
영문 소문자 + 숫자 + _만 |
대문자, 특수문자 불가 | Sirsoft-Board ❌ / sirsoft-my@module ❌ |
| 각 단어 첫 글자 숫자 불가 | 하이픈/언더스코어로 구분된 각 단어 기준 | sirsoft-2shop ❌ / sirsoft-board2 ✅ |
| 빈 부분 불가 | 연속 하이픈/언더스코어, 양끝 하이픈/언더스코어 금지 | sirsoft--board ❌ / -sirsoft-board ❌ |
| 최대 255자 | 길이 제한 | - |
유효/무효 예시
✅ sirsoft-board → Sirsoft\Board
✅ sirsoft-daum_postcode → Sirsoft\DaumPostcode
✅ sirsoft-board2 → Sirsoft\Board2
✅ vendor-my_module_name → Vendor\MyModuleName
❌ sirsoftboard → 하이픈 없음 (최소 2부분 필수)
❌ sirsoft-2shop → '2shop' 첫 글자 숫자
❌ Sirsoft-Board → 대문자 사용
❌ sirsoft-my@module → 특수문자 '@'
❌ sirsoft--board → 연속 하이픈 (빈 부분)
❌ sirsoft-board_ → 끝 언더스코어 (빈 단어)
Artisan 커맨드에서의 검증
module:install, plugin:install, template:install 등에서 ExtensionManager::validateIdentifierFormat()을 호출하여 잘못된 식별자 입력 시 InvalidArgumentException을 발생시킵니다.
관련 파일
app/Rules/ValidExtensionIdentifier.php— 검증 규칙 구현ExtensionManager::validateIdentifierFormat()— Artisan 커맨드용 래퍼
관리자 UI 필터링 (hidden 플래그)
확장 manifest (module.json / plugin.json / template.json) 에 "hidden": true 를 설정하면, 관리자 UI 목록 응답과 artisan *:list 기본 출력에서 제외됩니다. 학습용 샘플 확장, 내부 운영 전용 확장을 숨길 때 사용합니다.
제외 범위
| 대상 | hidden 적용 |
|---|---|
관리자 UI 기본 응답 (GET /api/admin/modules, /plugins, /templates) |
제외 |
artisan 기본 목록 (module:list, plugin:list, template:list) |
제외 |
설치/활성화/비활성화/제거 커맨드 (*:install, *:activate, *:uninstall) |
정상 동작 |
업데이트 감지 (*:check-updates, *:update) |
정상 동작 |
| 확장 로딩/오토로드/훅 실행 | 정상 동작 (활성화되어 있으면 기능 전부 사용 가능) |
숨김 포함 조회
슈퍼관리자는 다음 방법으로 숨김 확장까지 포함하여 조회할 수 있습니다:
# artisan CLI
php artisan module:list --hidden
php artisan plugin:list --hidden
php artisan template:list --hidden
# API 쿼리
GET /api/admin/modules?include_hidden=1
GET /api/admin/plugins?include_hidden=1
GET /api/admin/templates?include_hidden=1
manifest 설정
{
"identifier": "gnuboard7-hello_module",
"version": "0.1.0",
"hidden": true
}
- 기본값은 필드 생략 (hidden=false)
true설정 시에만 숨김 처리됨- 학습용 샘플 확장 예:
gnuboard7-hello_module,gnuboard7-hello_plugin,gnuboard7-hello_admin_template,gnuboard7-hello_user_template
상세: sample-extensions.md — 학습용 샘플 확장 전체 가이드
서비스 등록
CoreServiceProvider에서 싱글톤으로 등록됩니다.
// app/Providers/CoreServiceProvider.php
public function register(): void
{
// 확장 매니저 등록 (모듈/플러그인 공통 기능)
$this->app->singleton(ExtensionManager::class, function ($app) {
return new ExtensionManager;
});
// 모듈 매니저에 주입
$this->app->singleton(ModuleManager::class, function ($app) {
return new ModuleManager($app->make(ExtensionManager::class));
});
// 플러그인 매니저에 주입
$this->app->singleton(PluginManager::class, function ($app) {
return new PluginManager($app->make(ExtensionManager::class));
});
}
의존성 관계
ExtensionManager (독립)
↓
ModuleManager ← ExtensionManager 주입
↓
PluginManager ← ExtensionManager 주입
비활성화 사유 추적 (DeactivationReason)
확장 비활성화는 사용자 수동 비활성화와 시스템 자동 비활성화를 DB 레벨에서 구분합니다.
Enum 값
| 값 | 의미 | 시스템 트리거 |
|---|---|---|
manual |
관리자가 관리자 UI 또는 Artisan 커맨드로 직접 비활성화 | false |
incompatible_core |
코어 버전 호환성 검사 실패로 자동 비활성화 | true |
DeactivationReason::isSystemTriggered() 가 true 인 사유는 알림 영속화 + 재호환 시 원클릭 복구 UX 대상이 됩니다.
Manager 시그니처
ModuleManager::deactivateModule(
string $identifier,
string $reason = DeactivationReason::Manual->value,
?string $incompatibleRequiredVersion = null,
): void
PluginManager::deactivatePlugin / TemplateManager::deactivateTemplate 도 동일 시그니처. $incompatibleRequiredVersion 은 incompatible_core 사유에서 어떤 코어 버전을 요구했는지 기록 (재호환 알림에서 사용).
자동 복구 흐름
코어 업그레이드 후 incompatible_core 사유로 비활성화된 확장이 다시 호환 범위에 들어오면 관리자 대시보드 카드에 "다시 활성화" CTA 가 표시됩니다 (수동 비활성화 manual 은 대상 아님 — 사용자 의도 존중).
DB 컬럼: modules/plugins/templates 테이블의 deactivated_reason (string) + incompatible_required_version (string|null).
관련 문서
- 모듈 개발 기초 - 모듈 구조 및 설정
- 플러그인 개발 - 플러그인 구조 및 설정
- 모듈 Artisan 커맨드 - 모듈 관련 커맨드
- 확장 업데이트 시스템 - _bundled/_pending 및 업데이트
- 인덱스 - 확장 시스템 전체 문서 목록