클린 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 + 종료부 짝 호출 소스 훑기)
11 KiB
캐시 드라이버 시스템 (CacheInterface)
코어/모듈/플러그인에서 캐시를 관리하기 위한 표준화된 인터페이스
TL;DR (5초 요약)
1. 모든 캐시 저장은 CacheInterface 사용 (Cache:: 직접 호출 금지)
2. BaseModuleServiceProvider에서 cacheServices 배열에 Service 클래스 등록
3. 접두사로 캐시 키 격리: g7:core:{key}, g7:module.{id}:{key}, g7:plugin.{id}:{key}
4. TTL은 g7_core_settings('cache.*')로 중앙 관리 (하드코딩 금지)
5. Service 생성자에서 CacheInterface 타입힌트하면 자동 주입
6. 쓰기 실패는 fail-soft — put/forget/putMany 는 false, remember 는 콜백 결과 반환 + 경고 로그 (캐시는 최적화라 페이지를 죽이지 않는다)
목차
개요
G7 캐시 드라이버 시스템은 StorageDriver 패턴을 참고하여 설계되었습니다. 코어, 모듈, 플러그인별로 캐시 키를 접두사로 격리하고, DI를 통해 자동 주입됩니다.
핵심 원칙
- 접두사 격리: 각 확장의 캐시 키가 충돌하지 않음
- DI 자동 주입: Service 생성자에서
CacheInterface타입힌트만으로 해당 확장의 캐시 드라이버를 받음 - TTL 중앙 관리: 모든 TTL은
g7_core_settings('cache.*')에서 관리 - 확장 라이프사이클 통합: 모듈/플러그인 비활성화/삭제 시 캐시 자동 정리
아키텍처
CacheInterface (인터페이스 — app/Contracts/Extension/)
↑ implements
AbstractCacheDriver (공통 구현 — app/Extension/Cache/)
↑ extends
├── CoreCacheDriver 접두사: g7:core:{key}
├── ModuleCacheDriver 접두사: g7:module.{identifier}:{key}
└── PluginCacheDriver 접두사: g7:plugin.{identifier}:{key}
CacheInvalidatable (트레이트 — app/Extension/Traits/)
→ 모델에 적용, saved/deleted 시 관련 태그 캐시 자동 무효화
키 접두사 체계
| 드라이버 | 접두사 패턴 | 예시 |
|---|---|---|
| CoreCacheDriver | g7:core:{key} |
g7:core:system_settings |
| ModuleCacheDriver | g7:module.{id}:{key} |
g7:module.sirsoft-ecommerce:products:list |
| PluginCacheDriver | g7:plugin.{id}:{key} |
g7:plugin.sirsoft-payment:gateways |
모듈에서 사용하기
1단계: ServiceProvider에 cacheServices 등록
class EcommerceServiceProvider extends BaseModuleServiceProvider
{
protected string $moduleIdentifier = 'sirsoft-ecommerce';
protected array $cacheServices = [
ProductService::class,
CategoryService::class,
];
}
2단계: Service 생성자에서 CacheInterface 주입
class ProductService
{
public function __construct(
private readonly ProductRepositoryInterface $productRepository,
private readonly CacheInterface $cache,
) {}
public function getProducts(array $filters): array
{
$queryHash = md5(json_encode($filters));
return $this->cache->rememberQuery(
$queryHash,
fn () => $this->productRepository->list($filters),
ttl: 3600,
tags: ['products'],
);
}
public function getProduct(int $id): ?Product
{
return $this->cache->remember(
"product:{$id}",
fn () => $this->productRepository->find($id),
ttl: 3600,
tags: ['products', "product:{$id}"],
);
}
}
3단계 (선택): 캐시 스토어 오버라이드
모듈 클래스에서 getCacheStore()를 오버라이드하면 다른 캐시 드라이버를 사용할 수 있습니다.
class EcommerceModule extends AbstractModule
{
public function getCacheStore(): string
{
return 'redis'; // 기본값(config('cache.default')) 대신 redis 사용
}
}
확장에서 cache 바인딩
플러그인 / 모듈 ServiceProvider 는 글로벌 CacheInterface / StorageInterface 바인딩을 덮어쓰지 않는다. 글로벌 재바인딩은 다음 결함을 만든다:
- ServiceProvider 등록 순서가 늦은 확장이 코어
app(CacheInterface::class)를 자신의 도메인으로 누수 - 코어 LayoutService 등의
$this->cache->put('ext.cache_version', ...)가 플러그인 도메인으로 작성됨 → 변경이 화면에 반영 안 됨 - 코어/타 확장이 같은 글로벌 바인딩을 공유하면서 상호 간섭
금지 패턴
// plugins/_bundled/foo/src/Providers/FooServiceProvider.php — 금지
public function register(): void
{
$this->app->singleton(CacheInterface::class, function () {
return new PluginCacheDriver('foo');
});
}
정적 검사가 자동 차단한다. 면제 표지 불허.
표준 패턴 — contextual binding
BasePluginServiceProvider / BaseModuleServiceProvider 의 $cacheServices 배열에 캐시 의존 서비스 클래스만 등록하면, 해당 클래스 생성자의 CacheInterface 인자에 본 확장 도메인의 PluginCacheDriver / ModuleCacheDriver 가 자동 주입된다. 글로벌 코어 바인딩은 그대로 보존된다.
플러그인에서 사용하기
플러그인 1단계: BasePluginServiceProvider 상속 + cacheServices 등록
class FooPluginServiceProvider extends BasePluginServiceProvider
{
protected string $pluginIdentifier = 'vendor-foo_plugin';
protected array $cacheServices = [
FooService::class,
FooListener::class,
];
}
플러그인 2단계: Service 생성자에서 CacheInterface 주입
모듈과 동일하다 (§"모듈에서 사용하기" §2단계 참조). 키 prefix 만 g7:plugin.{identifier}: 로 다르다.
플러그인 3단계 (선택): 캐시 스토어 오버라이드
class FooPlugin extends AbstractPlugin
{
public function getCacheStore(): string
{
return 'redis';
}
}
Listener / 동적 인스턴스 생성 시
Listener 가 직접 new MyService(cache: app(CacheInterface::class), ...) 로 생성하면 글로벌 바인딩 (코어 도메인) 이 주입되어 도메인이 누수된다. 컨테이너 해석에 위임해야 contextual binding 이 적용된다:
// 금지 — 글로벌 코어 캐시가 주입됨
$service = new MyService(
cache: app(CacheInterface::class),
);
// 표준 — $cacheServices 의 contextual binding 적용
$service = app()->makeWith(MyService::class, [
'config' => $dynamicConfig,
// cache 인자는 생략 → contextual binding 으로 자동 주입
]);
코어 서비스에서 사용하기
코어 서비스는 CoreServiceProvider에서 CacheInterface를 바인딩합니다.
// CoreServiceProvider
$this->app->bind(CacheInterface::class, function () {
return new CoreCacheDriver(config('cache.default'));
});
코어 서비스에서도 동일하게 생성자 주입으로 사용합니다.
class LayoutService
{
public function __construct(
private readonly CacheInterface $cache,
) {}
public function loadAndMergeLayout(string $name, int $templateId): array
{
$ttl = (int) g7_core_settings('cache.layout_ttl', 3600);
return $this->cache->remember(
"layout.{$templateId}.{$name}",
fn () => $this->mergeLayout($name, $templateId),
$ttl,
['layout'],
);
}
}
자동 무효화 (CacheInvalidatable)
모델에 CacheInvalidatable 트레이트를 적용하면 saved/deleted 이벤트 시 관련 태그 캐시를 자동 삭제합니다.
use App\Extension\Traits\CacheInvalidatable;
class Product extends Model
{
use CacheInvalidatable;
protected function getCacheInvalidationTags(): array
{
return ['products', 'product:' . $this->id];
}
protected function getCacheDriver(): ?CacheInterface
{
return app(ModuleManager::class)
->getModule('sirsoft-ecommerce')?->getCache();
}
}
흐름: Product 수정 → CacheInvalidatable이 saved 이벤트 감지 → flushTags(['products', 'product:123']) → 해당 태그 캐시 삭제 → 다음 조회 시 remember() 콜백 실행으로 재생성
TTL 중앙 관리
모든 캐시 TTL은 g7_core_settings('cache.*')로 중앙 관리합니다. 하드코딩 금지.
| 설정 키 | 기본값 | 용도 |
|---|---|---|
cache.default_ttl |
86400 | 기본 TTL (24h) |
cache.layout_ttl |
3600 | 레이아웃 캐시 TTL |
cache.seo_ttl |
7200 | SEO 페이지 캐시 TTL |
cache.seo_sitemap_ttl |
86400 | Sitemap 캐시 TTL |
cache.notification_ttl |
3600 | 알림 정의/템플릿 캐시 TTL |
cache.extension_status_ttl |
86400 | 확장 상태 캐시 TTL |
cache.geoip_ttl |
86400 | GeoIP 캐시 TTL |
cache.version_check_ttl |
3600 | 버전 검증 캐시 TTL |
// 사용 예시
$ttl = (int) g7_core_settings('cache.layout_ttl', 3600);
$this->cache->remember($key, $callback, $ttl, $tags);
API 레퍼런스
기본 CRUD
| 메서드 | 설명 |
|---|---|
get(key, default) |
캐시 조회 |
put(key, value, ttl) |
캐시 저장 |
has(key) |
존재 확인 |
forget(key) |
삭제 |
Remember 패턴
| 메서드 | 설명 |
|---|---|
remember(key, callback, ttl, tags) |
캐시 미스 시 콜백 실행 + 저장 |
rememberQuery(queryHash, callback, ttl, tags) |
쿼리 해시 기반 캐싱 (query: 접두사) |
refresh(key, callback, ttl, tags) |
forget + remember 원자적 실행 |
벌크 연산
| 메서드 | 설명 |
|---|---|
many(keys) |
여러 키 한 번에 조회 |
putMany(values, ttl) |
여러 키-값 한 번에 저장 |
무효화
| 메서드 | 설명 |
|---|---|
flush() |
이 드라이버 소속 전체 캐시 삭제 |
flushTags(tags) |
특정 태그의 캐시만 삭제 |
메타
| 메서드 | 설명 |
|---|---|
supportsTags() |
태그 지원 여부 |
getStore() |
현재 스토어 이름 |
withStore(store) |
스토어 변경 (불변 복제) |
resolveKey(key) |
전체 키 확인 (디버깅용) |
StorageDriver와의 대응
| StorageDriver | CacheDriver | 비고 |
|---|---|---|
StorageInterface |
CacheInterface |
계약 |
CoreStorageDriver |
CoreCacheDriver |
코어용 |
ModuleStorageDriver |
ModuleCacheDriver |
모듈용 |
PluginStorageDriver |
PluginCacheDriver |
플러그인용 |
getStorage() |
getCache() |
추상 클래스 메서드 |
getStorageDisk() |
getCacheStore() |
오버라이드 가능 |
withDisk() |
withStore() |
불변 복제 |
$storageServices |
$cacheServices |
ServiceProvider 배열 |
registerStorageBindings() |
registerCacheBindings() |
DI 자동 바인딩 |