> ## Documentation Index
> Fetch the complete documentation index at: https://sonamu.cartanova.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# @cache 데코레이터

> 메서드에 캐시 적용하기

`@cache` 데코레이터를 사용하여 Model이나 Frame 메서드의 결과를 자동으로 캐싱할 수 있습니다.

## 기본 사용법

### 간단한 예제

```typescript theme={null}
import { BaseModelClass, cache, api } from "sonamu";

class UserModelClass extends BaseModelClass {
  @cache({ ttl: "10m" })
  @api()
  async findById(id: number) {
    // 이 메서드는 10분간 캐싱됨
    return this.findOne(['id', id]);
  }
}
```

**작동 방식**:

1. 첫 번째 호출: DB 조회 → 결과 캐싱
2. 두 번째 호출 (10분 내): 캐시에서 반환 (DB 조회 없음)
3. 10분 경과 후: 다시 DB 조회 → 캐시 갱신

## 데코레이터 옵션

### 전체 옵션

```typescript theme={null}
@cache({
  key?: string | ((...args: unknown[]) => string),  // 캐시 키
  store?: string,           // 사용할 스토어
  ttl?: string | number,    // 만료 시간
  grace?: string | number,  // Grace period (Stale-While-Revalidate)
  tags?: string[],          // 태그 목록
  forceFresh?: boolean,     // 캐시 무시하고 강제 갱신
})
```

### key: 캐시 키 설정

키를 지정하지 않으면 **자동으로 생성**됩니다.

<Tabs>
  <Tab title="자동 생성 (기본)" icon="wand-magic-sparkles">
    ```typescript theme={null}
    class UserModelClass extends BaseModelClass {
      @cache({ ttl: "10m" })
      async findById(id: number) {
        return this.findOne(['id', id]);
      }
    }

    // 호출: userModel.findById(123)
    // 자동 생성 키: "User.findById:123"
    ```

    **패턴**: `ModelName.methodName:serializedArgs`

    **인자 직렬화 규칙**:

    * 단일 primitive (string/number/boolean): 그대로 사용
    * 복잡한 객체: JSON.stringify 사용
    * 인자 없음: suffix 없이 `ModelName.methodName`
  </Tab>

  <Tab title="문자열 키" icon="text">
    ```typescript theme={null}
    @cache({ key: "user-list", ttl: "5m" })
    async findAll() {
      return this.findMany({});
    }

    // 호출: userModel.findAll()
    // 키: "user-list"

    @cache({ key: "user", ttl: "10m" })
    async findById(id: number) {
      return this.findOne(['id', id]);
    }

    // 호출: userModel.findById(123)
    // 키: "user:123" (인자가 suffix로 자동 추가)
    ```

    **인자가 있으면**: `key:args` 형태로 자동 생성
  </Tab>

  <Tab title="함수 키" icon="code">
    ```typescript theme={null}
    @cache({ 
      key: (userId: number, status: string) => `user:${userId}:${status}`,
      ttl: "10m"
    })
    async findByStatus(userId: number, status: string) {
      return this.findMany({ user_id: userId, status });
    }

    // 호출: model.findByStatus(123, "active")
    // 키: "user:123:active"
    ```

    **복잡한 키 생성 시 유용**
  </Tab>
</Tabs>

### ttl: 만료 시간

TTL(Time To Live)은 캐시가 유효한 시간입니다.

```typescript theme={null}
// 문자열 형식 (권장)
@cache({ ttl: "10s" })   // 10초
@cache({ ttl: "5m" })    // 5분
@cache({ ttl: "1h" })    // 1시간
@cache({ ttl: "1d" })    // 1일
@cache({ ttl: "1w" })    // 1주
@cache({ ttl: "forever" }) // 영구

// 숫자 형식 (밀리초)
@cache({ ttl: 60000 })  // 60000ms = 1분
```

**TTL 없이 사용하면**: BentoCache 기본값 적용 (일반적으로 무제한)

### grace: Stale-While-Revalidate

Grace period는 TTL 만료 후에도 **오래된 캐시(Stale)를 반환**하면서 백그라운드에서 갱신하는 기능입니다.

```typescript theme={null}
@cache({ 
  ttl: "1m",      // 1분 후 만료
  grace: "10m"    // 만료 후 10분간 Stale 값 반환
})
async getExpensiveData() {
  // 시간이 오래 걸리는 작업
  return await this.heavyComputation();
}
```

**작동 방식**:

1. **0\~1분**: 신선한 캐시 반환
2. **1\~11분**: Stale 캐시 즉시 반환 + 백그라운드 갱신
3. **11분 이후**: 캐시 미스, 새로 계산

**장점**:

* 사용자는 항상 빠른 응답 (Stale이라도 즉시 반환)
* 백그라운드에서 갱신되어 다음 사용자는 신선한 데이터 받음

### tags: 태그 기반 무효화

태그를 사용하여 관련 캐시를 그룹으로 무효화할 수 있습니다.

```typescript theme={null}
class ProductModelClass extends BaseModelClass {
  @cache({ ttl: "1h", tags: ["product", "list"] })
  @api()
  async findAll() {
    return this.findMany({});
  }
  
  @cache({ ttl: "1h", tags: ["product"] })
  @api()
  async findById(id: number) {
    return this.findOne(['id', id]);
  }
  
  @api()
  async update(id: number, data: Partial<Product>) {
    const result = await this.updateOne(['id', id], data);
    
    // product 태그가 있는 모든 캐시 무효화
    await Sonamu.cache.deleteByTag({ tags: ["product"] });
    
    return result;
  }
}
```

**무효화 패턴**:

```typescript theme={null}
// update() 호출 시
await Sonamu.cache.deleteByTag({ tags: ["product"] });

// 결과: findAll()과 findById() 캐시 모두 삭제됨
```

자세한 내용은 [캐시 무효화](/ko/advanced-features/caching/cache-invalidation)를 참고하세요.

### store: 특정 스토어 사용

여러 스토어를 설정한 경우, 특정 스토어를 지정할 수 있습니다.

```typescript theme={null}
// sonamu.config.ts에 여러 스토어 정의
export const config: SonamuConfig = {
  server: {
    cache: {
      default: "api",
      stores: {
        api: store().useL1Layer(drivers.memory({ maxSize: "200mb" })),
        database: store()
          .useL1Layer(drivers.memory({ maxSize: "100mb" }))
          .useL2Layer(drivers.redis({ connection: redis })),
      },
    },
  },
};

// Model에서 스토어 지정
class UserModelClass extends BaseModelClass {
  // api 스토어 사용 (기본)
  @cache({ ttl: "5m" })
  @api()
  async findAll() {
    return this.findMany({});
  }
  
  // database 스토어 사용 (Redis 공유)
  @cache({ store: "database", ttl: "1h" })
  @api()
  async findById(id: number) {
    return this.findOne(['id', id]);
  }
}
```

### forceFresh: 캐시 무시

항상 새로운 데이터를 가져오고 싶을 때 사용합니다.

```typescript theme={null}
@cache({ 
  ttl: "1h",
  forceFresh: true  // 캐시를 무시하고 항상 factory 실행
})
async getRealTimeData() {
  return await this.fetchFromExternalAPI();
}
```

**용도**: 디버깅이나 특수한 경우에만 사용 (일반적으로 불필요)

## 실전 예제

### 1. API 응답 캐싱

```typescript theme={null}
class PostModelClass extends BaseModelClass {
  // 게시글 목록 (5분 캐시)
  @cache({ ttl: "5m", tags: ["post", "list"] })
  @api()
  async findAll(page: number = 1) {
    return this.findMany({ 
      num: 20, 
      page,
      order: [['created_at', 'desc']]
    });
  }
  
  // 게시글 상세 (10분 캐시)
  @cache({ ttl: "10m", tags: ["post"] })
  @api()
  async findById(id: number) {
    return this.findOne(['id', id]);
  }
  
  // 인기 게시글 (1시간 캐시)
  @cache({ 
    key: "popular-posts",
    ttl: "1h", 
    tags: ["post", "popular"] 
  })
  @api()
  async findPopular() {
    return this.findMany({
      num: 10,
      where: [['view_count', '>', 1000]],
      order: [['view_count', 'desc']]
    });
  }
}
```

### 2. 데이터 변경 시 캐시 무효화

```typescript theme={null}
class ProductModelClass extends BaseModelClass {
  @cache({ ttl: "1h", tags: ["product"] })
  @api()
  async findById(id: number) {
    return this.findOne(['id', id]);
  }
  
  @cache({ ttl: "30m", tags: ["product", "list"] })
  @api()
  async findAll() {
    return this.findMany({});
  }
  
  @api()
  async create(data: ProductSave) {
    const result = await this.saveOne(data);
    
    // list 태그만 무효화 (목록에 새 항목 추가됨)
    await Sonamu.cache.deleteByTag({ tags: ["list"] });
    
    return result;
  }
  
  @api()
  async update(id: number, data: Partial<ProductSave>) {
    const result = await this.updateOne(['id', id], data);
    
    // product 태그 전체 무효화 (상세 + 목록)
    await Sonamu.cache.deleteByTag({ tags: ["product"] });
    
    return result;
  }
}
```

### 3. 복잡한 키 생성

```typescript theme={null}
class OrderModelClass extends BaseModelClass {
  @cache({ 
    key: (userId: number, status: string, startDate: string) => 
      `order:user:${userId}:${status}:${startDate}`,
    ttl: "10m",
    tags: ["order"]
  })
  @api()
  async findByUserAndStatus(
    userId: number,
    status: string,
    startDate: string
  ) {
    return this.findMany({
      where: [
        ['user_id', userId],
        ['status', status],
        ['created_at', '>=', startDate]
      ]
    });
  }
}

// 호출: model.findByUserAndStatus(123, "pending", "2025-01-01")
// 키: "order:user:123:pending:2025-01-01"
```

### 4. Stale-While-Revalidate 활용

```typescript theme={null}
class AnalyticsModelClass extends BaseModelClass {
  @cache({ 
    ttl: "5m",      // 5분 후 만료
    grace: "1h",    // 1시간 동안 Stale 값 사용
    tags: ["analytics"]
  })
  @api()
  async getDashboardStats() {
    // 무거운 집계 쿼리
    const [users, orders, revenue] = await Promise.all([
      this.countUsers(),
      this.countOrders(),
      this.calculateRevenue(),
    ]);
    
    return { users, orders, revenue };
  }
}
```

**시나리오**:

* 0\~5분: 신선한 캐시
* 5\~65분: Stale 캐시 즉시 반환 + 백그라운드 재계산
* 65분 이후: 캐시 미스, 새로 계산

### 5. 설정값 영구 캐싱

```typescript theme={null}
class ConfigModelClass extends BaseModelClass {
  @cache({ 
    key: "app-config",
    ttl: "forever",  // 영구 캐싱
    tags: ["config"]
  })
  @api()
  async getAppConfig() {
    return this.findOne(['key', 'app_config']);
  }
  
  @api()
  async updateConfig(data: ConfigSave) {
    const result = await this.saveOne(data);
    
    // 설정 변경 시 캐시 무효화
    await Sonamu.cache.deleteByTag({ tags: ["config"] });
    
    return result;
  }
}
```

## 내부 메서드 호출과 캐시 공유

Model 내부에서 다른 메서드를 호출할 때도 캐시가 공유됩니다.

```typescript theme={null}
class UserModelClass extends BaseModelClass {
  // findMany에 캐시 적용
  @cache({ ttl: "10m", tags: ["user"] })
  @api()
  async findMany(params: FindManyParams<User>) {
    return super.findMany(params);
  }
  
  // findById는 내부적으로 findMany를 호출
  async findById(id: number) {
    const { rows } = await this.findMany({ 
      where: [['id', id]], 
      num: 1 
    });
    return rows[0];
  }
}
```

**작동 방식**:

```typescript theme={null}
// 첫 번째 호출
await userModel.findById(123);
// → findMany({ where: [['id', 123]], num: 1 }) 호출
// → DB 조회 및 캐싱

// 두 번째 호출 (같은 파라미터)
await userModel.findById(123);
// → findMany({ where: [['id', 123]], num: 1 }) 호출
// → 캐시에서 반환 (DB 조회 없음)

// findMany를 직접 호출해도 같은 캐시 사용
await userModel.findMany({ where: [['id', 123]], num: 1 });
// → 캐시에서 반환
```

## 캐시 키 생성 로직

### 인자 직렬화

```typescript theme={null}
// decorator.ts 내부
function serializeArgs(args: unknown[]): string {
  if (args.length === 0) return "";

  // 단일 primitive 값
  if (args.length === 1) {
    const arg = args[0];
    if (arg === null || arg === undefined) return "";
    if (typeof arg === "string" || typeof arg === "number" || typeof arg === "boolean") {
      return String(arg);
    }
  }

  // 복잡한 값은 JSON 직렬화
  try {
    return JSON.stringify(args);
  } catch {
    // 직렬화 실패 시 toString 사용
    return args.map((arg) => String(arg)).join(":");
  }
}
```

**예시**:

```typescript theme={null}
// 단일 primitive
serializeArgs([123]) → "123"
serializeArgs(["hello"]) → "hello"

// 복잡한 객체
serializeArgs([{ id: 1, name: "test" }]) → '[{"id":1,"name":"test"}]'

// 여러 인자
serializeArgs([123, "active"]) → '[123,"active"]'
```

### 전체 키 생성

```typescript theme={null}
function generateCacheKey(
  modelName: string,
  methodName: string,
  args: unknown[],
  keyOption?: string | Function,
): string {
  // 1. 커스텀 키 함수
  if (typeof keyOption === "function") {
    return keyOption(...args);
  }

  // 2. 문자열 키 + args suffix
  if (typeof keyOption === "string") {
    const argsSuffix = serializeArgs(args);
    return argsSuffix ? `${keyOption}:${argsSuffix}` : keyOption;
  }

  // 3. 자동 생성
  const baseKey = `${modelName}.${methodName}`;
  const argsSuffix = serializeArgs(args);
  return argsSuffix ? `${baseKey}:${argsSuffix}` : baseKey;
}
```

### 복잡한 캐시 키 생성 예제

실전에서 마주치는 복잡한 캐시 키 생성 시나리오와 해결 방법입니다.

#### 예제 1: 중첩된 객체 파라미터

```typescript theme={null}
interface SearchParams {
  filters: {
    category?: string;
    priceRange?: { min: number; max: number };
    tags?: string[];
  };
  sort?: { field: string; order: "asc" | "desc" };
  pagination: { page: number; pageSize: number };
}

class ProductModelClass extends BaseModelClass {
  // ❌ 문제: 객체 순서가 바뀌면 다른 키 생성
  @cache({ ttl: "5m" })
  async search(params: SearchParams) {
    // JSON.stringify({ page: 1, pageSize: 20 }) !== JSON.stringify({ pageSize: 20, page: 1 })
    // ...
  }

  // ✅ 해결책 1: 정렬된 키 생성 함수
  @cache({
    key: (params: SearchParams) => {
      // 객체를 정규화하여 일관된 키 생성
      const normalized = {
        category: params.filters.category || "",
        priceMin: params.filters.priceRange?.min || 0,
        priceMax: params.filters.priceRange?.max || Infinity,
        tags: (params.filters.tags || []).sort().join(","),
        sortField: params.sort?.field || "id",
        sortOrder: params.sort?.order || "asc",
        page: params.pagination.page,
        pageSize: params.pagination.pageSize,
      };

      return `products:search:${JSON.stringify(normalized)}`;
    },
    ttl: "5m",
  })
  async searchNormalized(params: SearchParams) {
    // ...
  }

  // ✅ 해결책 2: 주요 필드만 사용
  @cache({
    key: (params: SearchParams) => {
      const { category } = params.filters;
      const { page, pageSize } = params.pagination;
      return `products:search:${category}:${page}:${pageSize}`;
    },
    ttl: "5m",
  })
  async searchSimple(params: SearchParams) {
    // ...
  }
}
```

#### 예제 2: 배열 파라미터

```typescript theme={null}
class PostModelClass extends BaseModelClass {
  // ❌ 문제: 배열 순서가 바뀌면 다른 키
  @cache({ ttl: "5m" })
  async findByTags(tags: string[]) {
    // cache key: "Post.findByTags:[\"react\",\"typescript\"]"
    // cache key: "Post.findByTags:[\"typescript\",\"react\"]"  ← 다른 키!
    // ...
  }

  // ✅ 해결책: 배열 정렬
  @cache({
    key: (tags: string[]) => {
      const sortedTags = [...tags].sort().join(",");
      return `posts:tags:${sortedTags}`;
    },
    ttl: "5m",
  })
  async findByTagsSorted(tags: string[]) {
    // cache key: "posts:tags:react,typescript" (항상 동일)
    // ...
  }
}
```

#### 예제 3: 날짜 파라미터

```typescript theme={null}
class ReportModelClass extends BaseModelClass {
  // ❌ 문제: Date 객체가 다르게 직렬화될 수 있음
  @cache({ ttl: "1h" })
  async generateReport(startDate: Date, endDate: Date) {
    // Date 객체는 ISO 문자열로 변환되지만 시간대 문제 가능
    // ...
  }

  // ✅ 해결책: ISO 문자열로 정규화
  @cache({
    key: (startDate: Date, endDate: Date) => {
      const start = startDate.toISOString().split("T")[0];  // "2024-01-01"
      const end = endDate.toISOString().split("T")[0];
      return `reports:${start}:${end}`;
    },
    ttl: "1h",
  })
  async generateReportNormalized(startDate: Date, endDate: Date) {
    // ...
  }
}
```

#### 예제 4: 사용자별 캐싱

```typescript theme={null}
class DashboardModelClass extends BaseModelClass {
  // ✅ 사용자 ID를 키에 포함
  @cache({
    key: (userId: number) => `dashboard:user:${userId}`,
    ttl: "10m",
  })
  async getUserDashboard(userId: number) {
    const context = Sonamu.getContext();

    // 권한 체크
    if (context.user?.id !== userId && context.user?.role !== "admin") {
      throw new ForbiddenError();
    }

    // 대시보드 데이터 조회
    // ...
  }

  // ✅ Context 기반 캐싱
  @cache({
    key: () => {
      const context = Sonamu.getContext();
      return `dashboard:user:${context.user?.id || "guest"}`;
    },
    ttl: "10m",
  })
  async getMyDashboard() {
    // Context에서 자동으로 사용자 ID 가져옴
    // ...
  }
}
```

#### 예제 5: 순환 참조 객체

```typescript theme={null}
interface Node {
  id: number;
  name: string;
  parent?: Node;  // 순환 참조 가능
}

class TreeModelClass extends BaseModelClass {
  // ❌ 문제: 순환 참조 객체는 JSON.stringify 실패
  @cache({ ttl: "5m" })
  async buildTree(node: Node) {
    // TypeError: Converting circular structure to JSON
    // ...
  }

  // ✅ 해결책: ID만 사용
  @cache({
    key: (node: Node) => `tree:${node.id}`,
    ttl: "5m",
  })
  async buildTreeSafe(node: Node) {
    // ...
  }

  // ✅ 해결책: 직렬화 가능한 데이터만 사용
  @cache({
    key: (nodeId: number, depth: number) => `tree:${nodeId}:${depth}`,
    ttl: "5m",
  })
  async buildTreeById(nodeId: number, depth: number = 3) {
    // ...
  }
}
```

#### 예제 6: 다중 필터 조건

```typescript theme={null}
interface ListFilters {
  status?: "active" | "inactive" | "pending";
  category?: string;
  minPrice?: number;
  maxPrice?: number;
  createdAfter?: Date;
  searchQuery?: string;
}

class ProductModelClass extends BaseModelClass {
  // ✅ 필터를 정규화하여 일관된 키 생성
  @cache({
    key: (filters: ListFilters, page: number, pageSize: number) => {
      // undefined/null 값 제거
      const normalized: Record<string, string> = {};

      if (filters.status) normalized.status = filters.status;
      if (filters.category) normalized.category = filters.category;
      if (filters.minPrice !== undefined) normalized.minPrice = String(filters.minPrice);
      if (filters.maxPrice !== undefined) normalized.maxPrice = String(filters.maxPrice);
      if (filters.createdAfter) {
        normalized.createdAfter = filters.createdAfter.toISOString().split("T")[0];
      }
      if (filters.searchQuery) normalized.q = filters.searchQuery;

      // 정렬된 키=값 쌍으로 변환
      const filterStr = Object.keys(normalized)
        .sort()
        .map((key) => `${key}=${normalized[key]}`)
        .join("&");

      return `products:list:${filterStr}:${page}:${pageSize}`;
    },
    ttl: "5m",
  })
  async list(filters: ListFilters, page: number, pageSize: number) {
    // cache key 예: "products:list:category=electronics&status=active:1:20"
    // ...
  }
}
```

#### 예제 7: 큰 객체 최적화

```typescript theme={null}
class AnalyticsModelClass extends BaseModelClass {
  // ❌ 문제: 큰 객체는 캐시 키가 너무 길어짐
  @cache({ ttl: "1h" })
  async analyze(config: LargeConfigObject) {
    // cache key: "Analytics.analyze:[{...매우 긴 JSON...}]"
    // ...
  }

  // ✅ 해결책: 해시 사용
  @cache({
    key: (config: LargeConfigObject) => {
      const hash = crypto
        .createHash("md5")
        .update(JSON.stringify(config))
        .digest("hex");
      return `analytics:${hash}`;
    },
    ttl: "1h",
  })
  async analyzeWithHash(config: LargeConfigObject) {
    // cache key: "analytics:a3f2b9c8d1e0..."
    // ...
  }

  // ✅ 해결책: 주요 속성만 사용
  @cache({
    key: (config: LargeConfigObject) => {
      const { dataSource, dateRange, metrics } = config;
      return `analytics:${dataSource}:${dateRange}:${metrics.join(",")}`;
    },
    ttl: "1h",
  })
  async analyzeOptimized(config: LargeConfigObject) {
    // ...
  }
}
```

### 캐시 키 설계 Best Practices

| 시나리오        | 권장 방법               | 이유            |
| ----------- | ------------------- | ------------- |
| **객체 파라미터** | 주요 필드만 추출 또는 정규화    | 키 길이 최소화, 일관성 |
| **배열 파라미터** | 정렬 후 join           | 순서 무관하게 동일 키  |
| **날짜 파라미터** | ISO 문자열 (날짜만)       | 시간대 문제 방지     |
| **사용자별 캐싱** | `user:${userId}` 포함 | 권한 분리         |
| **순환 참조**   | ID만 사용              | 직렬화 오류 방지     |
| **큰 객체**    | 해시 또는 주요 속성만        | 키 길이 제한       |
| **다중 필터**   | 정렬된 key=value 쌍     | 필터 순서 무관      |

<Warning>
  **캐시 키 설계 주의사항**:

  1. **키 길이**: 너무 긴 키는 성능 저하 (권장: 250자 이내)
  2. **직렬화 오류**: 순환 참조, Date, Function 등 주의
  3. **키 충돌**: 서로 다른 데이터가 같은 키를 가지지 않도록
  4. **정규화**: 객체/배열 순서가 달라도 동일한 키 생성
</Warning>

<Info>
  **캐시 키 디버깅 팁**:

  * 생성된 키를 로깅하여 확인
  * 예상치 못한 캐시 미스는 키 정규화 확인
  * 캐시 통계로 히트율 모니터링
</Info>

## 직접 캐시 조작

데코레이터 없이 직접 캐시를 조작할 수도 있습니다.

```typescript theme={null}
import { Sonamu } from "sonamu";

class UserModelClass extends BaseModelClass {
  @api()
  async findById(id: number) {
    const cacheKey = `user:${id}`;
    
    // 캐시 확인
    const cached = await Sonamu.cache.get({ key: cacheKey });
    if (cached) {
      return cached;
    }
    
    // DB 조회
    const user = await this.findOne(['id', id]);
    
    // 캐싱
    await Sonamu.cache.set({ 
      key: cacheKey, 
      value: user,
      ttl: "10m",
      tags: ["user"]
    });
    
    return user;
  }
}
```

**데코레이터 vs 직접 조작**:

* **데코레이터**: 간결, 선언적, 자동 키 생성
* **직접 조작**: 복잡한 로직, 조건부 캐싱, 세밀한 제어

## 주의사항

<Warning>
  **@cache 데코레이터 사용 시 주의사항**:

  1. **캐시 매니저 초기화 필수**: `sonamu.config.ts`에 캐시 설정이 없으면 에러 발생

     **에러 메시지**:

     ```
     CacheManager is not initialized. Please configure 'cache' in sonamu.config.ts.
     ```

     **발생 시점**:

     * @cache 데코레이터가 적용된 메서드를 **처음 호출할 때** 발생
     * 서버 시작 시가 아니라 실제 메서드 호출 시점에 발생

     **원인**:

     * `sonamu.config.ts`의 `server.cache` 설정이 없음
     * 또는 설정이 잘못됨

     **해결 방법**:

     `sonamu.config.ts`에 최소한의 캐시 설정을 추가하세요:

     ```typescript theme={null}
     import { store, drivers } from "sonamu/cache";

     export const config: SonamuConfig = {
       server: {
         // ... 기타 설정

         cache: {
           default: "main",
           stores: {
             main: store().useL1Layer(drivers.memory({ maxSize: "50mb" })),
           },
         },
       },
     };
     ```

     **최소 설정 설명**:

     * `default`: 기본 스토어 이름 (여기서는 "main")
     * `stores`: 스토어 객체 정의
       * `main`: 스토어 이름 (원하는 이름 사용 가능)
       * `store().useL1Layer(...)`: 메모리 드라이버를 L1 캐시로 사용
       * `maxSize: "50mb"`: 메모리 캐시 최대 크기

       <Info>
         **테스트 환경에서는**: `bootstrap(vi)` 호출 시 자동으로 메모리 드라이버가 설정되므로 별도 설정이 불필요합니다.
       </Info>

     **일반적인 실수**:

     | 실수                     | 증상             | 해결                                          |
     | ---------------------- | -------------- | ------------------------------------------- |
     | cache 설정 자체가 없음        | 메서드 호출 시 에러 발생 | 위의 최소 설정 추가                                 |
     | default와 stores 이름 불일치 | 에러 발생          | default 값이 stores 키와 일치하는지 확인               |
     | drivers import 누락      | 빌드 에러          | `import { drivers } from "sonamu/cache"` 추가 |
     | 메모리 크기 설정 누락           | maxSize 기본값 사용 | 명시적으로 maxSize 지정 권장                         |

  2. **비동기 메서드만 가능**: 동기 메서드에는 사용 불가
     ```typescript theme={null}
     // ❌ 에러
     @cache({ ttl: "10m" })
     findByIdSync(id: number) { ... }

     // ✅ 정상
     @cache({ ttl: "10m" })
     async findById(id: number) { ... }
     ```

  3. **스토어 이름 일치**: `store` 옵션은 설정에 정의된 이름과 일치해야 함
     ```typescript theme={null}
     // sonamu.config.ts
     stores: {
       myStore: store()...
     }

     // ❌ 에러
     @cache({ store: "wrongName", ttl: "10m" })

     // ✅ 정상
     @cache({ store: "myStore", ttl: "10m" })
     ```

  4. **직렬화 가능한 값만**: 함수, Symbol 등은 캐싱 불가
     ```typescript theme={null}
     // ❌ 잘못된 예
     @cache({ ttl: "10m" })
     async getProcessor() {
       return {
         process: () => { ... }  // 함수는 직렬화 안됨
       };
     }

     // ✅ 올바른 예
     @cache({ ttl: "10m" })
     async getData() {
       return {
         id: 1,
         name: "test",
         values: [1, 2, 3]
       };
     }
     ```

  5. **인자 순서 중요**: 같은 값이라도 순서가 다르면 다른 키
     ```typescript theme={null}
     @cache({ ttl: "10m" })
     async find(name: string, age: number) { ... }

     find("John", 30)  // 키: "Model.find:["John",30]"
     find(30, "John")  // 키: "Model.find:[30,"John"]" (다른 키!)
     ```
</Warning>

## 다음 단계

<CardGroup cols={2}>
  <Card title="캐시 설정" icon="gear" href="/ko/advanced-features/caching/cache-configuration">
    Stores와 Drivers 설정하기
  </Card>

  <Card title="캐시 무효화" icon="trash" href="/ko/advanced-features/caching/cache-invalidation">
    Tag 기반 캐시 무효화
  </Card>

  <Card title="캐시 전략" icon="chess" href="/ko/advanced-features/caching/cache-strategies">
    TTL, Grace, Namespace 활용
  </Card>
</CardGroup>
