Files
Gnuboard7/docs/extension/cache-driver.md
T
HeuJung b505ac7ba7 fix(core,board,ecommerce,page,gdpr): 목록 컨텍스트 왕복·엔진 렌더 파이프 결함 일괄 수정 + 공개문서 정리
세 갈래의 결함을 한 브랜치에서 정리한다.

## 목록 컨텍스트 왕복 시 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/활성 디렉토리 동기 완료.
2026-07-26 15:17:56 +09:00

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 자동 바인딩