Files
Gnuboard7/docs/extension/template-caching.md
T
HeuJung e819db9d42 fix(core): 공개 템플릿 config 캐시 무효화 누락과 확장 캐시 삭제 경로 동종 결함 정비
공개 config.json 의 고정 캐시 키(template.config.{identifier})가 어떤 갱신
경로에서도 forget 되지 않아 template:update/cache-clear/비활성화/삭제 후에도
최대 1시간(TTL) 이전 manifest 로 응답되던 결함(공개 )을 수정한다.

- TemplateManager::clearTemplateCache 공개화 — config/components_manifest(두
 변종)/현재 버전 routes·language 키 능동 forget, updateTemplate cleanup 호출
- forgetLayoutCacheKeys 가 편집기용 .with_source_meta 변종까지 삭제
- ext.cache_version 쓰기를 incrementExtensionCacheVersion 단일 지점으로 통일
- module/plugin:cache-clear 의 유령 키 forget 을 실재 캐시(레이아웃 서빙/해석
 캐시, 상태 키, 미들웨어 인덱스, 버전 bump) 무효화로 교체
- 공개 routes/language/layout API 의 ?v 생략 폴백을 0 → 현재 버전으로 교정
2026-08-17 13:22:48 +09:00

19 KiB

템플릿 캐싱 전략

관련 문서: cache-driver.md (공통 캐시 인터페이스 · 권장) | template-basics.md | template-security.md | index.md

⚠️ 중요: 본 문서는 템플릿 캐싱의 개념적 계층을 설명합니다. 실제 구현은 CacheInterface 를 사용해야 하며, Cache:: 파사드 직접 호출은 금지되었습니다. 모든 캐시 키는 드라이버 접두사(g7:core: / g7:module.{id}: / g7:plugin.{id}:)가 자동 적용됩니다. TTL 은 g7_core_settings('cache.*_ttl') 중앙 관리를 따릅니다.


목차

  1. 핵심 원칙
  2. 캐싱 계층
  3. 확장 기능 캐시 버전 (Cache Busting)
  4. 캐시 무효화 구현
  5. 서비스에서 캐시 무효화 호출
  6. 캐싱 모범 사례
  7. 성능 벤치마크

핵심 원칙

중요: 다층 캐싱으로 성능 최적화
✅ 필수: 캐시 무효화 규칙 준수

템플릿 시스템은 여러 계층의 캐싱을 통해 성능을 최적화합니다. 각 캐시는 적절한 TTL(Time To Live)을 가지며, 데이터 변경 시 정확한 무효화가 필수입니다.


캐싱 계층

1. 활성 템플릿 상태 캐시

// 키: ext.templates.active_identifiers / ext.templates.active_identifiers_{type} / ext.templates.installed_identifiers
// TTL: extension_status_ttl 설정값 (기본 86400초)
// 값: 템플릿 식별자 목록 (CachesTemplateStatus 트레이트가 관리)

$activeIdentifiers = $cache->remember('ext.templates.active_identifiers_admin', function () {
    return Template::where('type', 'admin')
        ->where('is_active', true)
        ->pluck('identifier')
        ->all();
}, $ttl);

무효화 시점:

  • 템플릿 설치/활성화/비활성화/삭제/업데이트 시 (TemplateManager::invalidateTemplateStatusCache() — 상태 키 4종 일괄 삭제)
  • template:cache-clear 실행 시

2. 레이아웃 JSON 캐시

// 키: template.layout.{layout_name}.{locale}
// TTL: 1800초 (30분)
// 값: 병합된 레이아웃 JSON 배열

Cache::remember("template.layout.dashboard.ko", 1800, function () use ($layoutName, $locale) {
    return $this->layoutService->getLayout($layoutName, $locale);
});

무효화 시점:

  • 레이아웃 생성/수정/삭제 시
  • 상위 레이아웃(extends) 변경 시
  • 로케일 변경 시

3. 병합된 레이아웃 캐시

상속 체인 처리 결과를 캐싱합니다.

// 키: template.layout.merged.{layout_name}.{locale}
// TTL: 3600초 (1시간)
// 값: extends/slots 병합 완료된 최종 레이아웃

Cache::remember("template.layout.merged.dashboard.ko", 3600, function () use ($layoutName, $locale) {
    return $this->layoutService->mergeLayouts($layoutName, [], 0, $locale);
});

무효화 시점:

  • 레이아웃 JSON 캐시 무효화 시 함께 무효화
  • 상속 체인의 어느 레이아웃이든 변경 시

4. 컴포넌트 매니페스트 캐시

// 키: template.{templateId}.components_manifest  (숫자 id / identifier 두 변종)
// TTL: 3600초 (1시간)
// 값: components.json 파싱 결과 (레이아웃 컴포넌트 검증용)

$manifest = $cache->remember("template.{$templateId}.components_manifest", function () use ($identifier) {
    $componentsPath = base_path("templates/{$identifier}/components.json");
    return json_decode(file_get_contents($componentsPath), true);
});

무효화 시점:

  • 템플릿 라이프사이클 전 경로 (TemplateManager::clearTemplateCache() — update/deactivate/uninstall/template:cache-clear)
  • components.json 이 바뀌는 템플릿 업데이트 시 (위 경로에 포함)

확장 기능 캐시 버전 (Cache Busting)

개요

모듈/플러그인/템플릿 활성화/비활성화 시 프론트엔드가 최신 데이터를 즉시 반영하도록 버전 기반 캐시 버스팅을 구현합니다.

문제 상황: 브라우저 HTTP 캐시로 인해 모듈 활성화 후 새로고침(Ctrl+F5) 전까지
             다국어/라우트 데이터가 반영되지 않음
✅ 해결책: 캐시 버전(타임스탬프) 기반 쿼리 파라미터로 캐시 무효화

백엔드 구현

ClearsTemplateCaches Trait

모듈/플러그인/템플릿 매니저에서 사용하는 공통 trait입니다. 내부적으로 CacheInterface 를 lazy resolve 해서 사용합니다.

<?php

namespace App\Extension\Traits;

use App\Contracts\Extension\CacheInterface;
use App\Extension\Cache\CoreCacheDriver;

trait ClearsTemplateCaches
{
    // 드라이버 접두사 `g7:core:` 가 자동 적용되어 실제 저장 키는 `g7:core:ext.cache_version`
    private static string $extensionCacheVersionKey = 'ext.cache_version';

    /**
     * 확장 기능 캐시 버전을 증가시킵니다.
     */
    protected function incrementExtensionCacheVersion(): void
    {
        self::resolveExtensionCache()->put(self::$extensionCacheVersionKey, time());
    }

    /**
     * 현재 확장 기능 캐시 버전을 반환합니다.
     */
    public static function getExtensionCacheVersion(): int
    {
        return (int) self::resolveExtensionCache()->get(self::$extensionCacheVersionKey, 0);
    }

    private static function resolveExtensionCache(): CacheInterface
    {
        try {
            return app(CacheInterface::class);
        } catch (\Throwable $e) {
            return new CoreCacheDriver(config('cache.default', 'array'));
        }
    }

    /**
     * 모든 활성 템플릿의 언어 캐시를 무효화합니다.
     */
    protected function clearAllTemplateLanguageCaches(): void
    {
        $locales = config('app.supported_locales', ['ko', 'en']);
        $templates = Template::where('status', ExtensionStatus::Active->value)->get();

        foreach ($templates as $template) {
            foreach ($locales as $locale) {
                Cache::forget("template.language.{$template->identifier}.{$locale}");
            }
        }
    }

    /**
     * 모든 활성 템플릿의 routes 캐시를 무효화합니다.
     */
    protected function clearAllTemplateRoutesCaches(): void
    {
        $templates = Template::where('status', ExtensionStatus::Active->value)->get();

        foreach ($templates as $template) {
            Cache::forget("template.routes.{$template->identifier}");
        }
    }
}

캐시 버전 증가 트리거 시점

매니저 메서드 트리거 상황
ModuleManager activate() 모듈 활성화
ModuleManager deactivate() 모듈 비활성화
ModuleManager refreshModuleLayouts() 레이아웃 갱신 시 변경 발생
PluginManager activate() 플러그인 활성화
PluginManager deactivate() 플러그인 비활성화
TemplateManager activate() 템플릿 활성화
TemplateManager refreshTemplateLayouts() 레이아웃 갱신 시 변경 발생

프론트엔드 구현

캐시 버전 저장 및 관리

// LayoutLoader.ts
private cacheVersion: number = 0;

constructor() {
    // localStorage에서 캐시 버전 복원
    const stored = localStorage.getItem('g7_extension_cache_version');
    this.cacheVersion = stored ? parseInt(stored, 10) : 0;
}

setCacheVersion(version: number): void {
    const oldVersion = this.cacheVersion;
    this.cacheVersion = version;
    localStorage.setItem('g7_extension_cache_version', String(version));

    // 버전 변경 시 캐시 무효화
    if (oldVersion !== 0 && oldVersion !== version) {
        this.clearCache();
    }
}

API 요청 시 버전 파라미터 추가

// LayoutLoader.ts
async loadLayout(layoutName: string): Promise<LayoutConfig> {
    let url = `/api/layout/${layoutName}`;
    if (this.cacheVersion > 0) {
        url += `?v=${this.cacheVersion}`;
    }
    // ...
}

// TranslationEngine.ts
async loadTranslations(identifier: string, locale: string): Promise<void> {
    let url = `/api/template/${identifier}/language/${locale}`;
    const params: string[] = [];

    if (this.cacheVersion > 0) {
        params.push(`v=${this.cacheVersion}`);
    }

    if (params.length > 0) {
        url += `?${params.join('&')}`;
    }
    // ...
}

앱 초기화 시 캐시 버전 설정

// TemplateApp.ts
async init(): Promise<void> {
    // 서버에서 캐시 버전 조회
    const response = await fetch('/api/extension/cache-version');
    const { version } = await response.json();

    // 각 엔진에 버전 설정
    this.layoutLoader.setCacheVersion(version);
    this.translationEngine.setCacheVersion(version);
}

캐시 버전 API 엔드포인트

// routes/api.php
Route::get('/extension/cache-version', function () {
    return response()->json([
        'version' => ClearsTemplateCaches::getExtensionCacheVersion(),
    ]);
});

동작 흐름

1. 사용자가 모듈 활성화 버튼 클릭
2. 백엔드: ModuleManager->activate() 실행
3. 백엔드: incrementExtensionCacheVersion() 호출 → 캐시 버전 = 현재 타임스탬프
4. 백엔드: clearAllTemplateLanguageCaches() 호출 → 서버 캐시 무효화
5. 프론트엔드: 페이지 새로고침 또는 SPA 라우트 이동
6. 프론트엔드: /api/extension/cache-version 조회
7. 프론트엔드: 버전 변경 감지 → localStorage 업데이트 + 내부 캐시 클리어
8. 프론트엔드: API 요청 시 ?v={version} 파라미터 추가
9. 브라우저: 새로운 URL로 인식하여 캐시 우회 → 최신 데이터 수신

캐시 버전 키 요약

저장소 키 값 용도
Laravel Cache g7:core:ext.cache_version 타임스탬프 서버 측 버전 관리 (CacheInterface 경유)
localStorage g7_extension_cache_version 타임스탬프 클라이언트 측 버전 유지

캐시 무효화 구현

TemplateCacheService

<?php

namespace App\Services\Template;

use Illuminate\Support\Facades\Cache;

class TemplateCacheService
{
    /**
     * 템플릿 상태 캐시 무효화
     */
    public function invalidateActiveTemplate(string $type): void
    {
        // 상태 키(ext.templates.active_identifiers* / installed_identifiers)는
        // CachesTemplateStatus 가 관리 — 단일 지점에 위임한다
        TemplateManager::invalidateTemplateStatusCache();
    }

    /**
     * 레이아웃 캐시 무효화
     */
    public function invalidateLayout(string $layoutName): void
    {
        $locales = config('app.supported_locales', ['ko', 'en']);

        foreach ($locales as $locale) {
            Cache::forget("template.layout.{$layoutName}.{$locale}");
            Cache::forget("template.layout.merged.{$layoutName}.{$locale}");
        }
    }

    /**
     * 레이아웃 상속 체인 캐시 무효화
     */
    public function invalidateLayoutChain(string $layoutName): void
    {
        // 현재 레이아웃 무효화
        $this->invalidateLayout($layoutName);

        // 이 레이아웃을 extends하는 모든 자식 레이아웃 무효화
        $children = TemplateLayout::where('extends', $layoutName)->get();

        foreach ($children as $child) {
            $this->invalidateLayoutChain($child->layout_name);
        }
    }

    /**
     * 컴포넌트 매니페스트 캐시 무효화 (숫자 id / identifier 두 변종)
     */
    public function invalidateComponentRegistry(int $templateId, string $identifier): void
    {
        Cache::forget("template.{$templateId}.components_manifest");
        Cache::forget("template.{$identifier}.components_manifest");
    }

    /**
     * 템플릿 전체 캐시 무효화
     */
    public function invalidateAllTemplateCache(string $identifier): void
    {
        // 활성 템플릿 캐시 무효화
        $template = Template::where('identifier', $identifier)->first();
        if ($template) {
            $this->invalidateActiveTemplate($template->type);
        }

        // 모든 레이아웃 캐시 무효화
        $layouts = TemplateLayout::where('template_id', $template->id)->get();
        foreach ($layouts as $layout) {
            $this->invalidateLayoutChain($layout->layout_name);
        }

        // 컴포넌트 매니페스트 캐시 무효화
        $this->invalidateComponentRegistry($template->id, $identifier);
    }
}

서비스에서 캐시 무효화 호출

LayoutService 예시

<?php

namespace App\Services\Template;

class LayoutService
{
    public function __construct(
        private TemplateCacheService $cacheService
    ) {}

    /**
     * 레이아웃 수정
     */
    public function updateLayout(int $id, array $data): TemplateLayout
    {
        $layout = TemplateLayout::findOrFail($id);
        $oldExtends = $layout->extends;

        $layout->update($data);

        // 캐시 무효화
        $this->cacheService->invalidateLayoutChain($layout->layout_name);

        // extends 변경 시 이전 부모의 자식들도 무효화
        if ($oldExtends && $oldExtends !== $layout->extends) {
            $this->cacheService->invalidateLayoutChain($oldExtends);
        }

        return $layout->fresh();
    }

    /**
     * 템플릿 활성화
     */
    public function activateTemplate(string $identifier): Template
    {
        $template = Template::where('identifier', $identifier)->firstOrFail();

        // 동일 타입의 기존 활성 템플릿 비활성화
        Template::where('type', $template->type)
            ->where('is_active', true)
            ->update(['is_active' => false]);

        $template->update(['is_active' => true]);

        // 캐시 무효화
        $this->cacheService->invalidateActiveTemplate($template->type);

        return $template->fresh();
    }
}

레이아웃 편집(관리자 커스터마이징) 시 캐시 무효화

관리자 UI에서 레이아웃을 직접 편집/복원할 때도 캐시 무효화가 필수입니다.

LayoutService의 저장(updateLayoutContent) 및 버전 복원(restoreVersion) 메서드에서:

  1. clearDependentLayoutsCache() 호출 — 해당 레이아웃 + 이를 extends하는 자식 레이아웃의 병합 캐시 + PublicLayoutController 서빙 캐시(layout.{identifier}.{name}.v{version})를 재귀적으로 무효화
  2. ext.cache_version 증가 ($this->incrementExtensionCacheVersion() — ClearsTemplateCaches 트레이트) — CDN/브라우저 HTTP 캐시 busting. PublicLayoutController가 ?v={version} 기반 캐싱을 사용하므로, 버전 변경 시 브라우저가 새 URL로 인식하여 캐시를 우회합니다. 버전 키 쓰기는 반드시 트레이트를 경유합니다 — 캐시 드라이버에 직접 put 하면 스토어/네임스페이스가 어긋날 때 bump 가 읽기 경로에서 보이지 않습니다.
// LayoutService::updateLayoutContent() 내부
$layout = $this->layoutRepository->update($layout->id, $updateData);

// 캐시 무효화 (현재 레이아웃 + 자식 레이아웃 + PublicLayoutController 서빙 캐시)
$this->clearDependentLayoutsCache($templateId, $name);

// 프론트엔드 브라우저 캐시 무효화 (ClearsTemplateCaches 트레이트)
$this->incrementExtensionCacheVersion();
// LayoutService::restoreVersion() 내부
$newVersion = $this->versionRepository->restoreVersion($layout->id, $versionId);

// 동일하게 캐시 무효화
$this->clearDependentLayoutsCache($templateId, $name);
$this->incrementExtensionCacheVersion();

이 과정이 누락되면 편집/복원 내용이 실제 사이트에 반영되지 않습니다.

참고: clearDependentLayoutsCache()는 내부적으로 clearLayoutCache()를 호출하며, 이 메서드는 병합 캐시 삭제(일반 + .with_source_meta 편집기 변종) + clearPublicServingCache()(현재 ext.cache_version 버전 키 삭제) + 모듈 레이아웃인 경우 LayoutResolverService 캐시도 무효화합니다.


캐싱 모범 사례

DO (권장)

  • ✅ 읽기 빈도가 높은 데이터는 캐싱 필수 (활성 템플릿, 레이아웃 JSON)
  • ✅ TTL은 데이터 변경 빈도에 따라 조정
  • ✅ 상속 체인 무효화는 재귀적으로 처리
  • ✅ 로케일별 캐싱으로 다국어 지원
  • ✅ 캐시 키에 의미 있는 이름 사용

DON'T (금지)

  • ❌ 캐시 무효화 누락 금지 (데이터 불일치 발생)
  • ❌ 과도한 캐싱으로 메모리 낭비 금지
  • ❌ TTL 없이 무제한 캐싱 남용 금지 (컴포넌트 레지스트리 제외)

성능 벤치마크

참고용 성능 비교 데이터입니다.

상태 응답 시간 개선율
캐싱 없음 120ms -
캐싱 적용 5ms 95% ↓

캐시 키 요약

서버 사이드 캐시는 모두 CacheInterface(g7:core: 네임스페이스) 에 저장됩니다. 무효화 방식은 두 갈래입니다 — 버전 키(.v{v} 접미사)는 ext.cache_version bump 로 다음 요청부터 새 키를 쓰고 이전 키는 TTL 로 자연 만료되며, 고정 키(버전 접미사 없음)는 라이프사이클에서 능동 forget 해야 합니다.

캐시 타입 키 패턴 TTL 무효화
공개 템플릿 설정 (config.json) template.config.{identifier} 3600초 고정 키 — 라이프사이클 능동 삭제
공개 라우트 (routes.json) template.routes.{identifier}.v{v} 3600초 버전 키 + 현재 버전 키 능동 삭제
공개 다국어 (lang/{locale}.json) template.language.{identifier}.{locale}.v{v} 3600초 버전 키 + 현재 버전 키 능동 삭제
공개 레이아웃 서빙 layout.{identifier}.{name}.v{v} (+ 편집기 .meta 변종) layout_ttl (기본 3600초) 버전 키 + 현재 버전 키 능동 삭제
병합 레이아웃 (내부) template.{templateId}.layout.{name} (+ .{sourceHash} / .with_source_meta 변종) layout_ttl 고정 키 — 능동 삭제
컴포넌트 매니페스트 (검증) template.{templateId}.components_manifest (identifier 변종 포함) 3600초 고정 키 — 능동 삭제
템플릿 상태 ext.templates.active_identifiers* / ext.templates.installed_identifiers 86400초 라이프사이클 능동 삭제
확장 캐시 버전 좌표 ext.cache_version 없음 bump 전용 (ClearsTemplateCaches)

template:cache-clear 가 무효화하는 대상

template:cache-clear {identifier?} 는 템플릿 라이프사이클(update/deactivate/uninstall)과 동일한 무효화 단일 지점을 경유합니다.

대상 단일 모드 전체 모드
template.config.{identifier} ✅ ✅ (설치된 전 템플릿)
template.{id}.components_manifest (identifier 변종 포함) ✅ ✅
현재 버전 routes/language 키 ✅ ✅
병합 레이아웃 캐시 (변종 포함) ✅ ✅
템플릿 상태 키 + ext.cache_version bump ✅ ✅ (1회)
확장 프론트엔드 병합 번들 파일 — ✅

관련 문서