7.0.11 인스톨러·코어 업데이트 변경(e60a82d73)에 대해 과거 회귀 22건을 부류별로 대조한
결과, 신규 노출면 1건과 테스트 위생 1건이 나와 인터뷰 결정대로 조치했다.
1. argv 채널의 SAPI 게이트 — CGI/FPM 은 register_argc_argv=On 이면 $_SERVER['argv'] 를
쿼리스트링을 '+' 로 쪼개 채우므로(`GET /?x+core:update` → argv[1]==='core:update', php-cgi
실측) 비인증 웹 요청이 업데이트 트리로 판정되어 bootstrap/app.php 자가 치유가 요청마다
패키지 매니페스트를 지우고 다시 만들었다. CoreUpdateContext 와 bootstrap/app.php 복제본
모두 argv 를 cli·phpdbg 에서만 읽는다. env 플래그 채널은 웹에서 주입할 수 없으므로 그대로
두어 웹 요청 안에서 시작하는 업데이트 흐름(7.1.0)에 영향이 없다. 동형성 테스트에 SAPI 축을
더했다.
2. 매니페스트 삭제 실패 기록 — PackageManifestCacheHelper::clear 가 지우지 못한 파일의
경로를 돌려주고, spawn 직전 호출부가 업그레이드 로그·콘솔에 경고로 남긴다. 권한·소유권
불일치면 자식의 자가 치유도 같은 이유로 실패해 증상은 제보와 같은 「Class not found」 인데,
이 경고가 원인이 권한이라는 유일한 흔적이다.
3. 테스트 격리 — tests/bootstrap.php 가 APP_PACKAGES_CACHE/APP_SERVICES_CACHE 를 테스트
전용 경로로 돌린다. proc_open 으로 자식을 띄우는 기존 테스트 2종의 자식이 개발 클론의
실제 bootstrap/cache 매니페스트를 지우고 다시 쓰던 것(stat 실측)을 부모·자식 함께 막는다.
관리자 [시스템 최적화] 경로(withPreservedContainer 파사드 복원의 미실측 형제 호출처)는
임시 설치본에서 API 로 실측했다 — 200, 설정·라우트 캐시 재생성, 후속 요청 200, 로그 오류 0.
4. 트러블슈팅 사례 ↔ 회귀 테스트 앵커 계약 — 신규 사례는 헤딩에 <!-- case:{영역}-{번호} -->
앵커를 달고 같은 문자열을 그 사례를 잠그는 회귀 테스트에도 남겨야 한다. 사례 번호는
문서마다 1부터 재시작하고 병합으로 중복되므로(이번 리베이스에서도 우리 사례가 develop 과
같은 29 였다가 31 로 밀렸다), 개수만 대조하면 다른 사례를 덮는 테스트도 초록이 된다.
그런데 판정기 check-troubleshooting-test-coverage.cjs 를 부르는 지점이 저장소에 하나도
없었다 — 스크립트 자체 주석에만 실행법이 적혀 있어 아무도 부르지 않으면 영원히 돌지
않았고, 그 사이 위반이 9건 쌓였다(backend 26~31, cache 17~19). 돌지 않는 대조는 아무것도
잠그지 못하므로 위반 해소와 실행 지점 부여를 함께 한다.
9건 전부에 앵커를 부착하고(각 사례가 선언한 회귀 테스트 중 가장 구체적인 파일에 배치,
한 파일이 두 사례에 선언된 경우는 갈라 배치), stop-guard 7.2 에 앵커 계약 + 미커버
baseline ratchet 두 축으로 등록했다. 트러블슈팅 사례 추가 프로토콜에 6단계를
더하고 coverage 에 troubleshooting-case-anchor-contract(manual-only, 전용 판정기 위임)를
등재했다. 판정기 종료코드 1 → 0, 미커버 건수는 전 문서 baseline 그대로다.
Laravel PackageManifest 는 bootstrap/cache/packages.php 가 있으면 stale 여부를 검사하지
않고 그대로 읽어 등재된 provider 를 new 한다. 코어 업데이트는 Step 6/8 에서 vendor 를
--no-dev 로 교체하지만 그 파일은 Step 11 까지 이전 설치본의 것이 남으므로, 옵션 없는
`composer install` 로 깔린 사이트에서는 Step 10 spawn 자식이 새 vendor 에 없는 provider 를
찾다 부팅 단계에서 죽는다. 부팅 전이라 앱 로그에 흔적이 없고 부모에게는 자식의 비정상
종료로만 보여, 운영자에게는 「Class ... not found」 와 수동 재개 안내만 남는다.
(sir.kr 커뮤니티 제보, 7.0.9 → 7.0.10)
3계층으로 막는다.
1. 부모 — spawn 직전 PackageManifestCacheHelper::clear
2. 자식 — bootstrap/app.php 가 G7_UPDATE_IN_PROGRESS 를 보고 스스로 정리한다.
이미 배포된 7.0.9·7.0.10 부모는 고칠 수 없으므로 그 아래에서 도는 신버전
자식의 유일한 방어다. App\ 클래스를 참조하지 않고 실패는 무시한다.
3. 범위 — CoreVersionChecker 의 env APP_VERSION 우선을 CoreUpdateContext 트리 안으로
축소한다. 업데이트 전에 뜬 artisan serve·큐 워커가 옛 값을 물고 확장을
incompatible_core 로 끄던 경로를 닫는다(관리자 템플릿이 대상이면 복구 UI
자체에 도달할 수 없다).
업데이트 트리 판정은 App\Support\CoreUpdateContext 가 단독 소유하고
CoreServiceProvider::isCoreUpdateInProgress 는 위임으로 남는다. bootstrap/app.php 의
복제본은 부팅 전이라 불가피한 예외이며, 두 조건의 동형성을 테스트가 단언한다.
실측 중 드러난 결함 2건을 함께 고쳤다.
- ConfigCacheHelper::withPreservedContainer 가 파사드 애플리케이션을 되돌리지 않아
Step 11 이 `Target class [command.tinker] does not exist` 로 실패·롤백했다.
- updateVersionInEnv 가 프로세스 환경을 갱신하지 않아 config 캐시에 이전 버전이 구워졌다
(Laravel env 저장소가 불변이라 재부팅으로도 덮이지 않는다).
인스톨러는 재사용 vendor 의 개발용 패키지를 installed.json 으로 감지해 설치 환경 확인
카드·설치 로그로 알리되 설치를 차단하지 않고( 결정 D1), 재사용 경로에서도 컴파일 캐시를
정리한다. 실행되는 명령만이 아니라 실패 시 안내하는 수동 명령까지 --no-dev 로 맞췄다.
코어 업데이트 완료·핸드오프·단독 재개 사후 단계에서 queue:restart 신호를 보낸다(D3).
코어 7.0.10 → 7.0.11.
7.0.9 → 7.0.10 sudo 업데이트 실측 후 점검에서 드러난 3건.
- 정리 단계가 소스 경로(core_{ts}/extracted/{루트}) 안쪽만 지워 껍데기가
업데이트마다 남던 것을 격리 디렉토리 루트째 삭제로 바꾼다. 부모는 구버전
클래스로 돌아 다음 업데이트부터 효력이 있으므로, 신버전 코드로 도는 두 자식
(execute-upgrade-steps / execute-bundled-updates)이 파일이 없는 core_* 만
골라 치운다 — 구버전 부모에서 올라오는 업데이트도 껍데기 없이 끝난다.
- 항목별 소유권 스냅샷이 격리 디렉토리 생성 뒤에 찍혀 root 가 만든 추출본을
원본으로 기록하고 복원이 다시 root 로 되돌리던 것을, 이번 실행의 격리
디렉토리를 스냅샷에서 제외해 닫는다.
- 완료 안내문이 성공 경로에서는 동작하지 않는 hotfix:rollback-stale-files 를
가리키던 것을 --prune 재실행 안내만 남기도록 정정한다(ko/en/ja).
부수: ExecuteUpgradeStepsStandaloneTest 가 앞선 core:update 실행이 남긴
프로세스 env 플래그에 따라 결과가 갈리던 순서 의존을 setUp/tearDown 에서 제거.
7.0.9 클린 설치본에서 7.0.10 으로 올리자 업그레이드 스텝 단계가 "부모 메모리 stale" 로 중단됐다.
스텝 자식은 부모가 비우지 않은 이전 버전 config 캐시로 부팅해 env APP_VERSION 오버라이드와
신버전 config/app.php 의 update 목록을 모두 보지 못한다. 이전 릴리즈에서는 자식 진입부의
config:cache 가 전역 Container 를 일회용 앱으로 바꿔 놓는 부수효과로 가드가 우연히 침묵했고,
그 부수효과를 걷어낸 withPreservedContainer 가 잠복 결함을 드러냈다.
- stale 가드는 CoreVersionChecker::getCoreVersion(env 우선) 으로 판독
- 부모는 spawn 직전 config 캐시를 비우고, 자식은 캐시 파일이 있으면 디스크 config 를 직접 읽는다
(7.0.10 이 추가한 public/build/ext 쓰기 권한 정상화 누락 차단)
- route:cache 도 같은 부수효과를 남기므로 RouteCacheHelper·optimizeSystem 을 보존 래퍼로 감쌌다
(단독 스텝 재실행·시스템 최적화 뒤 정적 재게시 예약 소실)
- 과거 업그레이드 사고 14부류를 7.0.10 변경과 대조 — 재발 조건은 위 3건뿐
- audit 룰 fresh-app-artisan-call-outside-cache-helper + 픽스처, 회귀 테스트 6건(수정 전 red)
- 템플릿 update 는 레이아웃 변경 여부와 무관하게 캐시 버전을 올리고(실패 복원 뒤에도), 자산 주소 방식 전환 3경로도 bump 한다
- config:cache 가 컨테이너 인스턴스를 덮어 이후 terminating 재게시가 사라지던 결함을 복원 헬퍼로 차단
- custom/ 변경 감지와 게시가 같은 열거자(재귀·크기 포함)를 쓰고, 서명은 호스트별로 저장
- 캐시 버전·서명 키를 만료시키지 않는다(기본 TTL 24h 로 매일 전체 재생성되던 문제)
- 관리자 > 환경설정 > 일반 「초기 화면 정적 파일」 카드: 상태 조회 + 지금 다시 만들기(확인 모달), 대시보드 알림 버튼 연결, CLI 와 같은 statusReport 소비
- sudo core:update 경로의 root 소유 잔존(설정 디렉토리·임시 폴더·로그·업그레이드 마이그레이션 산출물) 상속·정합화, ext-static 명령 root 경고와 sudo -u 힌트
- 안내·문서·트러블슈팅 4건·audit 룰 ext-cache-version-raw-read·ja 언어팩 동기
클린 7.0.9→7.0.10 실서버 업그레이드에서 전면 500 재발. 치명점을 캐시 파일
내용 역산으로 확정 — sha1('g7:_idx:g7:core') = 모든 remember 가 갱신하는
G7 캐시 키 인덱스 파일이 업데이트 마지막 root 쓰기(cache:clear 직후 버전
bump·상태/훅 캐시 재생성)로 root 소유 생성되면, 웹 프로세스의 모든 캐시
쓰기가 인덱스 갱신에서 Permission denied 로 죽어 부팅 경로가 500 이 된다
(로거 도달 전이라 laravel.log 공백). 직전 커밋의 자식측 게시 게이트만으로
부족했던 이유: 마지막 root 쓰기의 주체가 구코드 부모라 그 이후에 신코드가
개입할 지점이 없다.
- 원인 수정: CoreUpdateService::normalizeRuntimeOwnershipAfterRootRun —
root 실행 시 storage/framework/cache·bootstrap/cache·storage/app/ext-bundles
를 디렉토리 소유자 기준 재귀 정상화 + 그룹 쓰기 동기(업그레이드 로그
정상화 선례 확장). 부모·자식 커맨드의 모든 흐름 종료부에 짝으로 배선 —
이후 업데이트는 root 산출물을 남기지 않고 과거 잔재도 재귀 자가 수복
- 안전망: AbstractCacheDriver 쓰기 fail-soft — put/forget/putMany 는 false,
remember 는 콜백 1회 실행 보장 후 저장 실패를 삼키고 결과 반환(무캐시
동작), 경고 로그는 조합당 프로세스 1회. 구코드가 남긴 오염처럼 신코드가
선제 개입 못 하는 상황에서도 화면이 죽지 않는다 — 캐시는 최적화다
- red→green: CacheDriverWriteFailSoftTest 4케이스, CoreUpdateRuntimeOwnershipTest
(비-root no-op + 종료부 짝 호출 소스 훑기)
합의되지 않은 구조 변경을 되돌리고, 계획서 전수조사에서 드러난 결함을 처리했다.
쇼핑 화면 호출 통합(F4)은 API 쿼리 튜닝이 아니라 화면 구조 변경이라 원복했다.
레이아웃 9개를 develop 상태로 복원하고 전용 엔드포인트·컨트롤러·문서·테스트를 걷어냈다.
되돌리면 함께 사라지는 비-F4 수정 둘(페이저 has_more_pages 판정 3곳, Icon 크기 클래스
23줄)은 재적용했다. 성능 개선분은 전부 잔존한다.
통합검색 화면 상태를 전역 상태에서 URL 쿼리로 옮겼다. 종전에는 탭·필터·정렬·페이지가
주소에 없어 새로고침·뒤로가기에서 초기화되고 결과를 공유할 수도 없었다. 화면은 정상으로
보이고 콘솔 에러도 없어 코드만으로는 드러나지 않는 형태였다.
확장이 라우트를 추가해도 라우트 캐시가 걸린 사이트에서는 그 주소가 404 가 되던 구멍을
막았다. 훅 캐시와 달리 라우트 캐시에는 스캔 폴백이 없어 예외도 경고도 남지 않는다.
같은 문제를 이미 푼 ConfigCacheHelper 를 미러링해 갱신 지점을 한 곳으로 모았고,
코어 업데이트가 캐시를 비우기만 하고 되살리지 않던 문제도 함께 고쳤다.
상품·페이지 검색이 커서 판정에 페이지 번호를 넘기지 않아, 커서 없이 깊은 페이지를
지목한 딥링크가 조용히 1 페이지를 돌려주던 결함을 고쳤다. 호출 지점을 손으로 열거하는
대신 저장소를 훑는 패리티 테스트로 다음 도메인의 재발을 막는다.
환경설정 고급 탭의 목록 상한값이 저장되지 않던 결함을 고쳤다. 화면·검증·읽기는 모두
있었는데 저장 시 카테고리 분류표가 손으로 열거돼 있어 그 값만 버려지고 있었다. 분류표를
설정 정의에서 도출하도록 바꿔 다음 카테고리가 합류해도 같은 일이 생기지 않게 했다.
범위를 벗어난 값의 안내에 내부 식별자가 노출되던 것도 함께 정리했다.
언어팩 재설치가 활성 팩을 자기 자신과의 슬롯 충돌로 오인해 강등하던 회귀와, CLI 설치가
HTTP 권한 컨텍스트 없이 자동 활성화되는지를 고정하는 테스트를 함께 담았다.
목록 조회 성능 축(깊은 OFFSET·정렬·색인)과 그 검증 계층에서 계획서 전수검수와 3회에 걸친
감사가 지적한 항목을 처리했다. 세부 경위는 의 각 회차 문서에 있다.
검증 계층 — 컨트롤러가 base Request 를 직접 주입받던 확장 13곳을 전용 FormRequest 로 옮겼다.
옮기면서 기존 동작 계약은 그대로 두었다: 상한 초과 limit 을 거부하지 않고 상한까지 반환하던
공개 API, per_page 를 범위로 조정하던 관리자 목록, 미지원 period 를 year 로 해석하던 인기글
목록 모두 종전과 같은 응답을 낸다. 상한/폴백을 rules 로 승격시키면 200 이던 응답이 422 가
되어 기존 링크가 깨지므로, 규칙은 타입만 닫고 클램프·폴백은 접근자가 맡는다. period 는
접근자가 닫힌 집합만 반환해 캐시 키 공간도 함께 닫힌다.
ckeditor5 이미지 업로드만 ResponseHelper 봉투를 쓰지 않는다. 응답을 파싱하는 주체가 CDN 으로
로드되는 상위 CKEditor5 43.3.1 의 SimpleUploadAdapter 라 규약을 바꿀 수 없어, 각 응답 지점에
사유를 명시한 면제를 부착하고 근거를 API 문서에 남겼다.
하네스 — 룰 5개의 대상 경로 패턴이 매처와 맞지 않아 번들 확장 컨트롤러가 검사 대상에서
통째로 빠져 있었다. 패턴을 고치자 확장 위반 17건이 드러나 전건 처리했다. severity 오타가
요약 집계 양쪽에 안 잡혀 "0 error" 로 보고되던 문제도 런너 사전 검증으로 막았다.
정렬 게이트 대조 하네스는 관계 정렬 변형만 쓰는 저장소를 탐지하지 못한 채 통과시키고 있었다.
탐지·제외·인자 파싱을 함께 고치고 단위 테스트를 신설했다.
--prune 은 릴리즈 소스에 없는 public/storage 런타임 symlink 를 orphan 으로
삭제해 업로드 파일이 404 되었다. 심층 방어 2층으로 차단한다.
- 층1(예방): FilePermissionHelper 에 preserveLinkPaths 화이트리스트 추가.
applyUpdate 가 public 타깃에 ['storage'] 전달 → 매칭 orphan 링크/junction 만
삭제 제외 (정밀 보호)
- 층2(복구): StorageLinkHelper::ensurePublicStorageLink 신규 정적 헬퍼.
CoreUpdateCommand 종료 시점(정상+핸드오프) 호출로 부재/손상 링크 멱등 재생성
(Windows junction 폴백). beta.5 migration 05 는 이 헬퍼로 위임(상위호환)
core:update Step 7 이 targets 전체를 무조건 재복사하고 orphan 을 삭제하여
사용자 .htaccess 커스텀 블록·_bundled 커스텀 확장이 소실되던 사고(공개 )를
차단한다. 백업(base)↔_pending(theirs) 3-way md5 비교로 코어가 실제 변경/추가한
파일만 산출해 적용하고, 코어 미변경 파일은 복사·권한·mtime 을 전부 스킵해 현재
디스크 상태(사용자 수정 포함)를 보존한다. 전체 덮어쓰기 + orphan 정리가 필요하면
--prune 으로 opt-in. 백업 부재(--no-backup) 시 base 가 없어 전체 덮어쓰기로 안전
회귀 + 안내를 출력한다.
기존 stale 정리 테스트는 새 기본(증분)에서 orphan 을 삭제하지 않으므로 prune
컨텍스트로 전환했고, 증분 3-way 판정·목록 적용·fallback 단위 테스트와 --prune
커맨드 표면 테스트를 추가했다.
번들 ja 언어팩은 코어 apply_mode 안내 키(g7-core-ja)와 함께, 같은 빌드에서
동기화된 관리자↔유저 크로스링크 라벨(board/ecommerce/admin_basic/basic)의 일본어
번역을 반영하고 각 패키지 버전·CHANGELOG 를 갱신했다.
sudo(root) 로 core:update 를 실행하면 업그레이드 스텝 spawn 자식이 실패할 때
운영자에게 core:execute-upgrade-steps 재실행을 안내하는데, 이 명령을 root 로
그대로 재실행하면 스텝이 만드는 파일이 root 소유가 되어 이후 웹서버(www-data)
요청이 그 경로에 쓰기 실패한다.
재실행 안내를 실행 환경 4가지로 분기한다:
- non_root: 일반 SSH 사용자 / 공유 호스팅(웹서버=PHP=실행 유저) / posix 미지원 → 명령만
- root_web_known: 웹서버 계정 식별 → sudo -u {계정} + 계정명 경고
- root_web_symmetric: root 서비스 구성 → 명령만
- root_web_unknown: 계정 추정 실패 → placeholder + 확인 방법 안내
웹서버 계정은 FilePermissionHelper::inferWebServerOwnership 로 추정한다.
출력 전담 renderResumeGuidance 를 분리해 4분기 출력 계약을 회귀 테스트로 고정.
매 요청 균일하게 발생하던 부팅 오버헤드를 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. 순수 내부 부팅 인프라 — 확장 공개 표면·훅 계약 불변.
- 출시일: CHANGELOG [7.0.1] 및 엔진 CHANGELOG(v1.52.0/v1.51.0) 를 2026-07-02(오늘)로 조정
- 공개 release 포함 소스의 내부 이슈번호 정리:
- 공개 이슈는 gnuboard/g7 명시 형식으로 정규화
- 내부 이슈번호는 서술문으로 일반화
- 코드 주석의 내부 역할 호칭("")을 중립 표현으로 일반화
Windows JUNCTION 복사 실패:
- public/storage 는 storage:link 가 Windows 에서 생성하는 JUNCTION 으로,
PHP is_link/is_dir 이 모두 false 를 반환해 copyDirectory 가 파일로
오판 → copy(디렉토리) 예외로 코어 업데이트가 중단됐다.
- isReparsePoint 로 junction 을 정확 감지(link/file/dir 모두 false +
readlink 성공)하고, copyLink 가 symlink 실패 시 mklink /J 로 폴백 복원.
removeOrphanItems 도 junction orphan 을 rmdir 로 링크만 제거해 target
재귀 삭제 사고를 차단.
실행할 스텝이 없는 업데이트를 실패로 오판:
- spawn 자식이 exit=0 + step 0건으로 종료하면 부모 가드가 from<to 만으로
fail-fast 를 발동해, 스텝이 필요 없는 패치 릴리즈(예: 7.0.0→7.0.1)를
실패로 처리하고 확장 업데이트 단계까지 도달하지 못했다.
- [STEPS_EXECUTED] 신호에 discovered(범위 내 발견된 스텝 파일 수)를 추가하고,
handleSpawnExit 가 executed=0 && discovered=0 은 정상 통과, discovered>0
일 때만 fail-fast 하도록 구분. 구버전 자식(discovered 부재)은 레거시 판정 유지.
정상 통과 시 확장 일괄 업데이트 단계까지 진행된다.
테스트: FilePermissionHelperSymlinkTest / CoreUpdateCommandSpawnFailureTest /
CoreUpdateCommandHandoffTest / MultiVersionUpgradePathTest 전부 green.