빌드가 composer install 의 입력(스테이징에 복사할 composer.json/lock)만 활성 디렉토리에서 가져오고 manifest 해시는 _bundled 기준으로 기록해, 두 판단이 서로 다른 파일을 보고 있었다. 개발자는 규정상 _bundled 에서만 작업하므로 새 패키지를 추가하면 _bundled 에만 반영되고 활성은 다음 update 전까지 구버전으로 남는다. 그 결과 구버전 lock 으로 설치한 zip 에 신버전 해시가 붙어, 무결성 검증은 통과하는데 새 패키지가 번들에서 누락되고 설치 후 런타임에 클래스 not found 로 터졌다. 설치 입력·해시 기준·의존성 판정·stale 판정 네 가지를 모두 resolveHashTarget (출력 우선, 없으면 소스 폴백) 으로 통일. 코어는 source == output 이라 무영향. 실증: 이커머스에 psr/log 추가 → 빌드 2 packages(수정 전 1) → zip 에 vendor/psr/log 포함 → module:update → 활성 vendor 배포 + Psr\Log\LoggerInterface 런타임 로드 확인.
14 KiB
Vendor 번들 시스템 (Vendor Bundle System)
TL;DR (5초 요약)
- Composer 사용 불가 환경(공유 호스팅 등)을 위해 vendor/ 디렉토리를 zip으로 선탑재
- 개별 빌드: php artisan {core|module|plugin}:vendor-bundle [identifier|--all]
- 일괄 빌드: php artisan vendor-bundle:build-all (코어 + 모든 _bundled)
- 개별/일괄 검증: {core|module|plugin}:vendor-verify, vendor-bundle:verify-all
- 모드: VendorMode = auto | composer | bundled
- 결정 순서: 명시 → DB 이전 모드 → config → 환경감지(composer 가능 시 우선)
- composer.json/lock 수정 후 재빌드 필수 — scripts 섹션 같은 런타임 무관 변경도 파일 전체 SHA256 변경 → VendorIntegrityChecker 실패
1. 개요
G7 코어와 모듈/플러그인은 PHP 의존성 관리에 Composer를 사용합니다. 그러나 일부 운영 환경(공유 호스팅, 제한된 PaaS)에서는 proc_open()/shell_exec() 함수가 차단되어 Composer를 실행할 수 없습니다. Vendor 번들 시스템은 미리 압축된 vendor-bundle.zip 파일을 추출하여 vendor/ 디렉토리를 구성하는 대안 방식을 제공합니다.
1.1 핵심 파일
각 코어/확장 루트에 두 파일이 함께 존재합니다:
{root}/
├── composer.json (의존성 정의)
├── composer.lock (락 파일)
├── vendor-bundle.zip (압축된 vendor 디렉토리)
└── vendor-bundle.json (메타파일: SHA256, 패키지 목록 등)
이 네 파일(composer.json / composer.lock / vendor-bundle.json / vendor-bundle.zip)은 코어 루트 및 각 _bundled 확장 루트에 함께 존재하며 모두 Git 으로 추적한다. 추출 결과인 vendor/ 디렉토리만 .gitignore 대상이다. composer.lock 이 누락되면 VendorIntegrityChecker 가 composer_lock_sha256 을 검증할 수 없으므로, composer update 직후 생성된 composer.lock 과 재빌드된 번들 두 파일을 같은 커밋에 함께 포함한다.
1.1 빌드가 보는 composer.json / composer.lock
확장의 번들 빌드는 산출물을 _bundled 에 쓰지만(활성 디렉토리 오염 방지), 입력으로 삼는 composer.json / composer.lock 도 _bundled 를 기준으로 한다. 출력 디렉토리에 해당 파일이 없을 때만 활성 디렉토리로 폴백하며, 코어는 소스와 출력이 같은 경로(base_path())이므로 언제나 동일한 파일을 본다. 이 규칙은 VendorBundler::resolveHashTarget() 한 곳에 있고, 빌드의 네 가지 판단이 모두 그것을 쓴다.
| 판단 | 사용처 |
|---|---|
| 외부 의존성 유무 (번들링 대상인가) | build(), isStale() |
composer install 입력 (스테이징에 복사할 파일) |
build() |
manifest 해시 (composer_{json,lock}_sha256) |
build() |
| stale 판정 (재빌드 필요한가) | isStale() |
빌드는 활성 디렉토리의 vendor/ 를 읽지 않는다. 스테이징 디렉토리에서 composer install --no-dev 를 새로 실행해 dev 의존성이 섞이지 않은 vendor/ 를 만들고 그것을 압축한다.
네 판단이 같은 파일을 보아야 하는 이유는 두 가지다.
첫째, 검증자의 대조 위치. VendorIntegrityChecker::verify($sourceDir) 의 $sourceDir 은 zip 과 manifest 가 놓인 디렉토리를 뜻하고, 실제 호출 경로는 모두 _bundled 계열을 넘긴다.
| 경로 | verify 에 넘기는 디렉토리 |
|---|---|
| 신규 설치 | _pending/{id} (_bundled 복사본) |
| 업데이트 | _pending/{id}_updating_* (_bundled 복사본) |
| 코어 업데이트 | pending 경로 (소스 = 출력) |
vendor-bundle:verify-all |
_bundled/{id} |
해시를 활성에서 계산하면 생성 기준과 검증 기준이 어긋난다. 두 composer.json 의 내용이 같을 때만 우연히 통과하며, _bundled 의 composer.json 만 변경된 상태(버전 bump 직후 등)에서는 manifest 가 영구히 불일치하여 module:update 가 composer_json_sha_mismatch 로 차단된다. 그 오류를 해소할 유일한 명령이 module:update 이므로 순환이 발생한다. isStale() 이 활성만 보면 --check 가 stale 을 up-to-date 로 오보하는 것도 같은 뿌리다.
둘째, 개발자가 _bundled 에서만 작업한다는 규정. 새 패키지를 추가하면 _bundled 의 composer.json / composer.lock 에만 반영되고 활성 디렉토리는 다음 {module|plugin}:update 전까지 구버전으로 남는다. 이때 composer install 의 입력만 활성에서 가져오면, 구버전 lock 으로 설치한 zip 에 신버전 해시를 붙인 manifest 가 만들어진다. 해시는 정합하므로 무결성 검증은 통과하는데 정작 새 패키지가 번들에서 빠져, 설치 후 런타임에 클래스 not found 로 터진다. 설치 입력과 해시 기준이 같은 파일을 보면 이 어긋남 자체가 성립하지 않는다.
vendor-bundle.json 스키마 (v1.0):
{
"schema_version": "1.0",
"generated_at": "2026-04-14T12:34:56+09:00",
"generator": "g7 vendor-bundle:build",
"target": "core" | "module:identifier" | "plugin:identifier",
"composer_json_sha256": "...",
"composer_lock_sha256": "...",
"zip_sha256": "...",
"zip_size": 12345678,
"package_count": 142,
"php_requirement": "^8.2",
"g7_version": "7.0.0-beta.4",
"packages": [
{ "name": "laravel/framework", "version": "12.0.5", "type": "library" }
]
}
2. VendorMode
세 가지 모드가 있습니다:
| 모드 | 설명 |
|---|---|
auto |
환경 감지 — composer 사용 가능 시 composer, 불가 시 bundled |
composer |
강제 composer 실행 (불가 시 예외) |
bundled |
강제 vendor-bundle.zip 추출 (zip 없으면 예외) |
2.1 모드 결정 우선순위
VendorResolver::resolveMode() 의 우선순위:
1. CLI/API 명시 (--vendor-mode 또는 vendor_mode 파라미터)
└─ composer/bundled 명시 시 그대로 사용
2. 업데이트 시 DB 이전 모드 상속
└─ modules.vendor_mode / plugins.vendor_mode 컬럼이 auto가 아니면 상속
3. 전역 설정 config('app.install.default_vendor_mode')
4. auto 해석 → 환경 감지
a. EnvironmentDetector::canExecuteComposer() == true → Composer
b. composer 불가 + vendor-bundle.zip 존재 → Bundled
c. 둘 다 불가 → 예외 (no_vendor_strategy_available)
핵심 원칙: auto 모드에서는 composer가 우선입니다 (개발 환경과 프로덕션 동작 일치).
3. CLI 사용법
본 시스템은 개별 명령 6개와 일괄 알리아스 2개로 구성됩니다. 개별 명령은 기존
core:build / module:build / plugin:build 패턴과 정렬되어 있습니다.
3.1 개별 빌드 — 코어/모듈/플러그인
# 코어 단독 빌드
php artisan core:vendor-bundle [--check] [--force]
# 특정 모듈 빌드 (positional identifier — module:build 와 동일 패턴)
php artisan module:vendor-bundle sirsoft-ecommerce [--check] [--force]
# 모든 _bundled 모듈 빌드 (--all 명시 필요)
php artisan module:vendor-bundle --all [--check] [--force]
# 특정 플러그인 빌드
php artisan plugin:vendor-bundle sirsoft-payment [--check] [--force]
# 모든 _bundled 플러그인 빌드
php artisan plugin:vendor-bundle --all [--check] [--force]
식별자도 --all 도 지정하지 않으면 명시적 에러가 발생합니다 (의도하지 않은 일괄
실행 방지). 기존 module:build / plugin:build 와 동일한 안전 동작입니다.
3.2 개별 검증
php artisan core:vendor-verify
php artisan module:vendor-verify sirsoft-ecommerce
php artisan module:vendor-verify --all
php artisan plugin:vendor-verify sirsoft-payment
php artisan plugin:vendor-verify --all
3.3 일괄 알리아스 — 운영/CI 시나리오
코어 + 모든 _bundled 모듈/플러그인을 한 번에 처리하려면 일괄 알리아스를 사용합니다.
# 일괄 빌드 (코어 + 모든 _bundled 모듈/플러그인)
php artisan vendor-bundle:build-all [--check] [--force]
# 일괄 검증
php artisan vendor-bundle:verify-all
vendor-bundle:build-all --check 는 stale 감지 시 종료 코드 1을 반환하므로 CI
파이프라인의 사전 검증 단계에 유용합니다.
3.4 옵션 요약
| 옵션 | 동작 |
|---|---|
--check |
실제 빌드 없이 stale 여부만 확인. stale 발견 시 종료 코드 1 |
--force |
composer.json/lock 해시 체크를 무시하고 강제 재빌드 |
--all |
(개별 명령에서) 해당 타입의 모든 _bundled 확장 대상 |
3.5 검증 항목
- vendor-bundle.zip / vendor-bundle.json 파일 존재
- manifest의 schema_version 지원 여부
- zip 파일의 SHA256 해시 일치
- composer.json/composer.lock SHA256 일치 (소스에 존재 시)
4. 코어 업데이트
# Auto 모드 (기본값)
php artisan core:update
# Composer 강제
php artisan core:update --vendor-mode=composer
# 번들 강제 (공유 호스팅 환경)
php artisan core:update --vendor-mode=bundled
# 로컬 + 번들 강제 (개발 시 번들 검증)
php artisan core:update --local --force --vendor-mode=bundled
5. 확장 설치/업데이트
5.1 CLI
# 모듈 설치 (auto)
php artisan module:install sirsoft-ecommerce
# 모듈 bundled 강제 설치
php artisan module:install sirsoft-ecommerce --vendor-mode=bundled
# 플러그인 업데이트 (이전 모드 상속)
php artisan plugin:update sirsoft-payment
# 플러그인 force 업데이트 + composer 강제
php artisan plugin:update sirsoft-payment --force --vendor-mode=composer
5.2 Admin UI
관리자 페이지의 모듈/플러그인 설치 모달에 Vendor 설치 방식 Select 필드가 있습니다:
- 자동 (권장)
- Composer 실행
- 번들 Vendor 사용
5.3 API
POST /api/admin/modules/install
Content-Type: application/json
{
"module_name": "sirsoft-ecommerce",
"vendor_mode": "bundled"
}
6. 웹 인스톨러
public/install/ 마법사의 Step 3(환경 설정)에서 Vendor 설치 방식을 선택할 수 있습니다.
- 환경 감지: ZipArchive 확장 + proc_open 사용 가능 여부
- 비활성 옵션: 환경에서 사용 불가한 모드는 자동 비활성화
- 자동 폴백: auto 모드에서 composer 실행 불가 시 vendor-bundle.zip이 있으면 자동 전환
7. 무결성 검증 및 보안
7.1 SHA256 검증
VendorIntegrityChecker::verify($sourceDir) 가 다음을 검증합니다. $sourceDir 은 zip 과 manifest 가 놓인 디렉토리이며, 확장의 경우 _bundled 또는 그 복사본(_pending)입니다 (§1.1).
- vendor-bundle.zip 파일의 실제 SHA256 vs manifest 기록 값
- composer.json SHA256 (해당 디렉토리에 존재 시)
- composer.lock SHA256 (해당 디렉토리에 존재 시)
검증 실패 시 VendorInstallException 이 발생하며 설치가 중단됩니다.
7.2 Zip Slip 방지
추출 전 zip 내부 모든 파일 경로를 검증합니다:
../포함 경로 거부- 절대 경로 거부 (
/...,C:/...)
8. 자동 트리거
composer.json 또는 composer.lock 파일이 변경되면 번들이 stale 상태가 됩니다.
확장의 경우 판정 기준은 _bundled 디렉토리의 두 파일이다 (§1.1). 확장 버전을 올려
_bundled/composer.json 의 version 만 바뀐 경우에도 stale 이 되므로 재빌드가 필요하다.
다음 명령으로 재빌드해야 합니다:
# stale 여부 확인
php artisan vendor-bundle:build-all --check
# 일괄 재빌드
php artisan vendor-bundle:build-all
CI 통합 예시:
- name: Verify vendor bundles
run: php artisan vendor-bundle:build-all --check
9. 핵심 클래스
| 클래스 | 책임 |
|---|---|
| VendorMode | 모드 enum (Auto/Composer/Bundled) |
| VendorResolver | 모드 결정 + 전략 디스패치 |
| VendorBundler | zip 빌드 (개발 타임) |
| VendorBundleInstaller | zip 추출 (런타임) |
| VendorIntegrityChecker | SHA256 무결성 검증 |
| EnvironmentDetector | composer/zip 환경 감지 |
| VendorInstallException | vendor 관련 예외 |
10. 트러블슈팅
| 증상 | 원인 | 해결 |
|---|---|---|
bundle_zip_missing |
vendor-bundle.zip 파일 없음 | vendor-bundle:build 실행 |
zip_hash_mismatch |
zip 파일 손상 또는 변조 | vendor-bundle:build --force 재빌드 |
composer_json_sha_mismatch |
composer.json 변경 후 번들 미갱신 | vendor-bundle:build 재빌드 |
composer_not_available |
composer 모드 강제했으나 실행 불가 | --vendor-mode=auto 또는 bundled로 변경 |
no_vendor_strategy_available |
composer 불가 + 번들 없음 | vendor-bundle.zip 업로드 또는 호스팅 변경 |
bundle_contains_unsafe_path |
외부 zip 파일이 zip slip 시도 | 신뢰할 수 있는 소스에서만 다운로드 |
bundle_build_promote_failed |
빌드 산출물을 최종 경로로 원자적 교체 실패 | 디스크 공간/권한 확인 후 재빌드 (원본 번들은 promoteAtomic 으로 보존됨) |
빌드가 Generating optimized autoload files 직후 무한 대기 (Windows) |
composer autoload dump 의 async 손자 프로세스가 부모 파이프 핸들을 상속·점유 | 코어 반영됨 — stdout/stderr 를 파이프가 아닌 파일 descriptor 로 실행 (VendorBundler::composerOutputDescriptors). 최신 코어에서는 재발하지 않음 |
11. 참고
- 본 문서는 vendor 번들 시스템의 사용자 가이드입니다.
- 코어 및 번들 확장의
composer.json/composer.lock/vendor-bundle.json/vendor-bundle.zip은 Git 으로 추적합니다. 추출 결과인vendor/디렉토리만.gitignore대상입니다. - 확장 설치 모드는
modules.vendor_mode/plugins.vendor_mode컬럼에 기록되며, 업데이트 시 자동 상속됩니다.