๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
@cache ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋Š” ๋ฉ”์„œ๋“œ์˜ ์‹คํ–‰ ๊ฒฐ๊ณผ๋ฅผ ์บ์‹œํ•˜์—ฌ ์„ฑ๋Šฅ์„ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. BentoCache๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ฉ”๋ชจ๋ฆฌ, Redis, DynamoDB ๋“ฑ ๋‹ค์–‘ํ•œ ์Šคํ† ๋ฆฌ์ง€๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.

๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ•

import { BaseModelClass, api, cache } from "sonamu";

class ProductModelClass extends BaseModelClass {
  @api({ httpMethod: "GET" })
  @cache({ ttl: "10m" })
  async findById(id: number) {
    const rdb = this.getPuri("r");
    return rdb.table("products").where("id", id).first();
  }

  @api({ httpMethod: "GET" })
  @cache({ ttl: "1h", tags: ["products"] })
  async list() {
    const rdb = this.getPuri("r");
    return rdb.table("products").select();
  }
}

์„ค์ • (sonamu.config.ts)

์บ์‹œ๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด sonamu.config.ts์—์„œ ์„ค์ •์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
import { bentostore } from "bentocache";
import { memoryDriver } from "bentocache/drivers/memory";
import { redisDriver } from "bentocache/drivers/redis";
import { defineConfig } from "sonamu";

export default defineConfig({
  cache: {
    default: "multi",  // ๊ธฐ๋ณธ ์Šคํ† ์–ด
    stores: {
      // ๋ฉ”๋ชจ๋ฆฌ ์ „์šฉ
      memory: bentostore()
        .useL1Layer(memoryDriver({ maxItems: 1000 })),
      
      // Redis ์ „์šฉ
      redis: bentostore()
        .useL1Layer(redisDriver({
          connection: { host: "127.0.0.1", port: 6379 }
        })),
      
      // Multi-tier (๋ฉ”๋ชจ๋ฆฌ + Redis)
      multi: bentostore()
        .useL1Layer(memoryDriver({ maxItems: 1000 }))
        .useL2Layer(redisDriver({
          connection: { host: "127.0.0.1", port: 6379 }
        }))
    }
  }
});
์บ์‹œ ์„ค์ •์ด ์—†์œผ๋ฉด @cache ์‚ฌ์šฉ ์‹œ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

์˜ต์…˜

ttl

์บ์‹œ ์œ ํšจ ์‹œ๊ฐ„์„ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.
type TTL = string | number;  // "10s", "5m", "1h", "1d" ๋˜๋Š” ๋ฐ€๋ฆฌ์ดˆ
@cache({ ttl: "10m" })     // 10๋ถ„
async getData() {}

@cache({ ttl: "1h" })      // 1์‹œ๊ฐ„
async getReport() {}

@cache({ ttl: "1d" })      // 1์ผ
async getDailySummary() {}

@cache({ ttl: 60000 })     // 60์ดˆ (๋ฐ€๋ฆฌ์ดˆ)
async getQuickData() {}
๋ฌธ์ž์—ด ํ˜•์‹์„ ์‚ฌ์šฉํ•˜๋ฉด ๊ฐ€๋…์„ฑ์ด ์ข‹์Šต๋‹ˆ๋‹ค: "10s", "5m", "1h", "1d"

key

์บ์‹œ ํ‚ค๋ฅผ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ๋ณธ๊ฐ’: "{ModelName}.{methodName}:{serializedArgs}"
@cache({ ttl: "10m" })
async findById(id: number) {
  // ์บ์‹œ ํ‚ค: "Product.findById:123"
}

@cache({ ttl: "10m" })
async search(query: string, page: number) {
  // ์บ์‹œ ํ‚ค: "Product.search:["search term",1]"
}

store

์‚ฌ์šฉํ•  ์บ์‹œ ์Šคํ† ์–ด๋ฅผ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ๋ณธ๊ฐ’: sonamu.config.ts์˜ default ์Šคํ† ์–ด
@cache({ ttl: "1h", store: "redis" })
async getSharedData() {
  // Redis ์Šคํ† ์–ด ์‚ฌ์šฉ
}

@cache({ ttl: "5m", store: "memory" })
async getLocalData() {
  // ๋ฉ”๋ชจ๋ฆฌ ์Šคํ† ์–ด ์‚ฌ์šฉ
}

tags

์บ์‹œ ํƒœ๊ทธ๋ฅผ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค. ํƒœ๊ทธ๋ณ„๋กœ ์บ์‹œ๋ฅผ ์ผ๊ด„ ๋ฌดํšจํ™”ํ•  ๋•Œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
@cache({ ttl: "1h", tags: ["products", "featured"] })
async getFeatured() {
  // tags๋กœ ๋ฌถ์–ด์„œ ๊ด€๋ฆฌ
}

@cache({ ttl: "10m", tags: ["products"] })
async findById(id: number) {
  // "products" ํƒœ๊ทธ๋กœ ๋ฌดํšจํ™” ๊ฐ€๋Šฅ
}

// ์บ์‹œ ๋ฌดํšจํ™”
await Sonamu.cache.deleteByTags(["products"]);

gracePeriod

์บ์‹œ๊ฐ€ ๋งŒ๋ฃŒ๋œ ํ›„์—๋„ ์ผ์ • ์‹œ๊ฐ„ ๋™์•ˆ ์ด์ „ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ์œ ์˜ˆ ๊ธฐ๊ฐ„์ž…๋‹ˆ๋‹ค.
@cache({
  ttl: "10m",
  gracePeriod: { enabled: true, duration: "5m" }
})
async getData() {
  // TTL ๋งŒ๋ฃŒ ํ›„ 5๋ถ„๊ฐ„์€ ๊ธฐ์กด ์บ์‹œ ๋ฐ˜ํ™˜
  // ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์ƒˆ๋กœ์šด ๊ฐ’ ๊ฐฑ์‹ 
}

timeouts

์บ์‹œ ์ž‘์—…์˜ ํƒ€์ž„์•„์›ƒ์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
@cache({
  ttl: "10m",
  timeouts: {
    soft: "100ms",   // ์ด ์‹œ๊ฐ„ ๋‚ด์— ์บ์‹œ ์—†์œผ๋ฉด factory ์‹คํ–‰
    hard: "1s"       // ์ตœ๋Œ€ ๋Œ€๊ธฐ ์‹œ๊ฐ„
  }
})
async getData() {
  // 100ms ๋‚ด์— ์บ์‹œ๊ฐ€ ์—†์œผ๋ฉด ์ฆ‰์‹œ factory ์‹คํ–‰
}

์บ์‹œ ๋ฌดํšจํ™”

ํƒœ๊ทธ๋กœ ๋ฌดํšจํ™”

class ProductModelClass extends BaseModelClass {
  @api({ httpMethod: "GET" })
  @cache({ ttl: "1h", tags: ["products"] })
  async list() {
    const rdb = this.getPuri("r");
    return rdb.table("products").select();
  }

  @api({ httpMethod: "POST" })
  @transactional()
  async create(data: ProductCreateParams) {
    const wdb = this.getDB("w");
    const result = await this.insert(wdb, data);
    
    // "products" ํƒœ๊ทธ์˜ ๋ชจ๋“  ์บ์‹œ ๋ฌดํšจํ™”
    await Sonamu.cache.deleteByTags(["products"]);
    
    return result;
  }
}

ํ‚ค๋กœ ๋ฌดํšจํ™”

// ํŠน์ • ํ‚ค ์‚ญ์ œ
await Sonamu.cache.delete("Product.findById:123");

// ํŒจํ„ด ๋งค์นญ์œผ๋กœ ์‚ญ์ œ
await Sonamu.cache.deleteMany("Product.findById:*");

์ „์ฒด ๋ฌดํšจํ™”

// ๋ชจ๋“  ์บ์‹œ ์‚ญ์ œ
await Sonamu.cache.clear();

๋‹ค๋ฅธ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ์™€ ํ•จ๊ป˜ ์‚ฌ์šฉ

@api์™€ ํ•จ๊ป˜

@api({ httpMethod: "GET" })
@cache({ ttl: "10m" })
async getData() {
  // API ์—”๋“œํฌ์ธํŠธ + ์บ์‹ฑ
}

@transactional๊ณผ ํ•จ๊ป˜

@api({ httpMethod: "GET" })
@cache({ ttl: "10m" })
@transactional({ readOnly: true })
async getComplexData() {
  // ์ฝ๊ธฐ ์ „์šฉ ํŠธ๋žœ์žญ์…˜ + ์บ์‹ฑ
  const rdb = this.getPuri("r");
  
  const users = await rdb.table("users").select();
  const posts = await rdb.table("posts").select();
  
  return { users, posts };
}
๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ์ˆœ์„œ: @api โ†’ @cache โ†’ @transactional

์บ์‹œ ์ž‘๋™ ๋ฐฉ์‹

1. ์บ์‹œ ํžˆํŠธ

@cache({ ttl: "10m" })
async findById(id: number) {
  // ์ฒซ ๋ฒˆ์งธ ํ˜ธ์ถœ: DB ์กฐํšŒ + ์บ์‹œ ์ €์žฅ
  // ๋‘ ๋ฒˆ์งธ ํ˜ธ์ถœ: ์บ์‹œ์—์„œ ์ฆ‰์‹œ ๋ฐ˜ํ™˜ (DB ์กฐํšŒ ์—†์Œ)
}

2. ์บ์‹œ ๋ฏธ์Šค

@cache({ ttl: "10m" })
async findById(id: number) {
  // ์บ์‹œ์— ์—†์Œ โ†’ factory ์‹คํ–‰ (๋ฉ”์„œ๋“œ ์‹คํ–‰)
  const result = await this.queryDB(id);
  // ๊ฒฐ๊ณผ๋ฅผ ์บ์‹œ์— ์ €์žฅ
  return result;
}

3. ์บ์‹œ ๊ฐฑ์‹ 

@cache({ ttl: "10m" })
async getData() {
  // TTL ๋งŒ๋ฃŒ ํ›„ ์ฒซ ํ˜ธ์ถœ: factory ์‹คํ–‰ + ์ƒˆ๋กœ์šด ๊ฐ’ ์บ์‹œ
  // ์ดํ›„ 10๋ถ„๊ฐ„: ์บ์‹œ๋œ ๊ฐ’ ๋ฐ˜ํ™˜
}

์ฃผ์˜์‚ฌํ•ญ

1. CacheManager ์ดˆ๊ธฐํ™” ํ•„์š”

// โŒ ์—๋Ÿฌ ๋ฐœ์ƒ
@cache({ ttl: "10m" })
async getData() {}
// CacheManager is not initialized
ํ•ด๊ฒฐ: sonamu.config.ts์—์„œ cache ์„ค์ • ์ถ”๊ฐ€

2. ์ธ์ž ์ง๋ ฌํ™”

๋ณต์žกํ•œ ๊ฐ์ฒด๋Š” ์ž๋™์œผ๋กœ JSON ์ง๋ ฌํ™”๋ฉ๋‹ˆ๋‹ค:
@cache({ ttl: "10m" })
async search(params: { name: string; age: number }) {
  // ์บ์‹œ ํ‚ค: "User.search:{"name":"Alice","age":30}"
}
์ง๋ ฌํ™”๊ฐ€ ์–ด๋ ค์šด ๊ฐ์ฒด๋Š” key ํ•จ์ˆ˜๋กœ ์ง์ ‘ ํ‚ค๋ฅผ ์ƒ์„ฑํ•˜์„ธ์š”.

3. null/undefined ์บ์‹ฑ

null๊ณผ undefined๋„ ์บ์‹œ๋ฉ๋‹ˆ๋‹ค:
@cache({ ttl: "10m" })
async findById(id: number) {
  const result = await this.queryDB(id);
  // result๊ฐ€ null์ด์–ด๋„ ์บ์‹œ๋จ
  return result;  // null
}

4. ๋ถ€์ž‘์šฉ์ด ์žˆ๋Š” ๋ฉ”์„œ๋“œ

์บ์‹œ๋Š” ์ˆœ์ˆ˜ ํ•จ์ˆ˜์—๋งŒ ์‚ฌ์šฉํ•˜์„ธ์š”:
// โŒ ๋‚˜์œ ์˜ˆ: ๋ถ€์ž‘์šฉ์ด ์žˆ์Œ
@cache({ ttl: "10m" })
async incrementCounter() {
  this.counter++;  // ๋ถ€์ž‘์šฉ
  return this.counter;
}

// โœ… ์ข‹์€ ์˜ˆ: ์ˆœ์ˆ˜ ํ•จ์ˆ˜
@cache({ ttl: "10m" })
async getCounter() {
  return this.counter;  // ์ฝ๊ธฐ๋งŒ
}

์„ฑ๋Šฅ ์ตœ์ ํ™”

Multi-tier ์บ์‹ฑ

๋น ๋ฅธ ๋ฉ”๋ชจ๋ฆฌ ์บ์‹œ + ์˜์†์ ์ธ Redis ์บ์‹œ:
// sonamu.config.ts
export default defineConfig({
  cache: {
    stores: {
      multi: bentostore()
        .useL1Layer(memoryDriver({ maxItems: 1000 }))  // ๋น ๋ฆ„
        .useL2Layer(redisDriver({ /* ... */ }))        // ์˜์†์ 
    }
  }
});

// L1 (๋ฉ”๋ชจ๋ฆฌ)์— ์žˆ์œผ๋ฉด ์ฆ‰์‹œ ๋ฐ˜ํ™˜
// L1์— ์—†์œผ๋ฉด L2 (Redis) ์กฐํšŒ
// L2์— ์—†์œผ๋ฉด factory ์‹คํ–‰
@cache({ ttl: "1h", store: "multi" })
async getData() {}

์บ์‹œ ์›œ์—…

๋ฏธ๋ฆฌ ์บ์‹œ๋ฅผ ์ฑ„์›Œ๋‘๊ธฐ:
class ProductModelClass extends BaseModelClass {
  async warmupCache() {
    // ์ธ๊ธฐ ์ƒํ’ˆ ๋ฏธ๋ฆฌ ์บ์‹ฑ
    const popularIds = [1, 2, 3, 4, 5];
    await Promise.all(
      popularIds.map(id => this.findById(id))
    );
  }

  @cache({ ttl: "1h" })
  async findById(id: number) {
    const rdb = this.getPuri("r");
    return rdb.table("products").where("id", id).first();
  }
}

๋กœ๊น…

์บ์‹œ ํžˆํŠธ/๋ฏธ์Šค๋ฅผ ๋กœ๊น…ํ•˜๋ ค๋ฉด BentoCache ์„ค์ • ์‚ฌ์šฉ:
export default defineConfig({
  cache: {
    stores: {
      memory: bentostore()
        .useL1Layer(memoryDriver({ maxItems: 1000 }))
        .options({
          logger: {
            log: (level, message) => {
              console.log(`[${level}] ${message}`);
            }
          }
        })
    }
  }
});

์˜ˆ์‹œ ๋ชจ์Œ

class ProductModelClass extends BaseModelClass {
  @api({ httpMethod: "GET" })
  @cache({ ttl: "5m", tags: ["products"] })
  async list(params: ProductListParams) {
    const rdb = this.getPuri("r");
    return rdb.table("products")
      .where("active", true)
      .paginate(params);
  }

  @api({ httpMethod: "GET" })
  @cache({ ttl: "1h", tags: ["products"] })
  async getFeatured() {
    const rdb = this.getPuri("r");
    return rdb.table("products")
      .where("featured", true)
      .limit(10);
  }

  @api({ httpMethod: "POST" })
  @transactional()
  async create(data: ProductCreateParams) {
    const wdb = this.getDB("w");
    const result = await this.insert(wdb, data);
    
    // ์บ์‹œ ๋ฌดํšจํ™”
    await Sonamu.cache.deleteByTags(["products"]);
    
    return result;
  }
}

๋‹ค์Œ ๋‹จ๊ณ„