세 갈래의 결함을 한 브랜치에서 정리한다.
## 목록 컨텍스트 왕복 시 URL 상태 소실 ( @jiwonpapa 님께서 제보해주셨습니다.)
목록에서 상세·형제 상세(이전/다음)·작성/수정 폼에 다녀오면 보고 있던
page/search/category/filters 가 사라지던 문제를 전 도메인에서 수정했다.
- 엔진(engine-v1.54.2): `mergeQuery: true` 만 적고 `query` 를 생략하면 병합이
통째로 건너뛰어지던 함정을 교정 — `ActionDispatcher.handleNavigate`/`handleReplaceUrl`.
- 게시판·이커머스·페이지·회원·마이페이지·gdpr 등 9개 확장 레이아웃의 왕복 leg 전수
적용(mergeQuery: true). 의도적 리셋(검색/필터 초기화·탭 전환·프리셋)은 면제 주석으로 구분.
- 무한스크롤 목록(브랜드·상품 공통정보·고시정보)의 새로고침이 URL 검색·정렬을 떨구던
결함 수정.
- 재발 차단: audit 룰 `layout-list-context-navigate-merge-query`(목록 클러스터 자동 도출,
page/필터 URL 신호 4종) + `layout-navigate-path-absolute`(navigate path 동작 키워드 금지).
## cellChildren 등 반복 렌더에서 단일 바인딩 파이프 미적용 ( @glitter-gim 님께서 제보해주셨습니다.)
목록 표의 각 칸에 넣은 날짜·숫자 서식(`{{row.x | datetime(...)}}`)이 빈 값이 되거나
서식 없는 원본으로 나오던 문제를, 표현식 판정 로직이 엔진 전역에 복제되며 갈라진
구조적 결함으로 진단하고 판정 경로를 단일화했다(engine-v1.54.3).
- `RenderHelpers`(renderItemChildren·evaluateIfCondition)·`ConditionEvaluator`·
`DataBindingEngine.resolveObject`·`DynamicRenderer` props 5곳에 단일 바인딩 파이프 분기 추가.
- 계획: `g7-scalable-lobster.md`(렌더 경로 비대칭 결함 일괄 수정).
## 공개 문서 내부 도구 귀속 제거
release 에 포함되는 공개 문서(`docs/**`)에서 내부 audit 룰 ID 귀속 서술을
도구 비귀속 표현("정적 검사")으로 정리. 재발 차단 룰 `public-no-internal-audit-reference` 신설.
전 계층 테스트(PHPUnit·Vitest·Playwright)·회귀 테스트 동반, 버전/CHANGELOG/활성 디렉토리 동기 완료.
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 타입힌트하면 자동 주입
목차
개요
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 자동 바인딩 |