메인 μ½˜ν…μΈ λ‘œ κ±΄λ„ˆλ›°κΈ°
@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 { defineConfig } from "sonamu";
import { drivers, store } from "sonamu/cache";

export default defineConfig({
  server: {
    cache: {
      default: "multi", // κΈ°λ³Έ μŠ€ν† μ–΄
      stores: {
        // λ©”λͺ¨λ¦¬ μ „μš©
        memory: store().useL1Layer(drivers.memory({ maxSize: "100mb" })),

        // Redis μ „μš©
        redis: store().useL1Layer(
          drivers.redis({
            connection: { host: "127.0.0.1", port: 6379 },
          }),
        ),

        // Multi-tier (λ©”λͺ¨λ¦¬ + Redis)
        multi: store()
          .useL1Layer(drivers.memory({ maxSize: "100mb" }))
          .useL2Layer(
            drivers.redis({
              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"]);

grace

grace, timeouts λ“±μ˜ κ³ κΈ‰ μ˜΅μ…˜μ€ BentoCache의 RawCommonOptionsμ—μ„œ μ œκ³΅λ©λ‹ˆλ‹€. μžμ„Έν•œ λ‚΄μš©μ€ BentoCache λ¬Έμ„œλ₯Ό μ°Έκ³ ν•˜μ„Έμš”.
μΊμ‹œκ°€ 만료된 후에도 일정 μ‹œκ°„ λ™μ•ˆ 이전 κ°’(Stale)을 λ°˜ν™˜ν•˜λŠ” 유예 κΈ°κ°„μž…λ‹ˆλ‹€ (Stale-While-Revalidate νŒ¨ν„΄).
@cache({
  ttl: "10m",
  grace: "5m"  // TTL 만료 ν›„ 5λΆ„ λ™μ•ˆ Stale κ°’ λ°˜ν™˜
})
async getData() {
  // 0~10λΆ„: μ‹ μ„ ν•œ μΊμ‹œ λ°˜ν™˜
  // 10~15λΆ„: Stale μΊμ‹œ μ¦‰μ‹œ λ°˜ν™˜ + λ°±κ·ΈλΌμš΄λ“œ κ°±μ‹ 
  // 15λΆ„ 이후: μΊμ‹œ 미슀, μƒˆλ‘œ 계산
}
μž‘λ™ 방식:
  • TTL λ‚΄: μ‹ μ„ ν•œ μΊμ‹œ λ°˜ν™˜
  • TTL 만료 ν›„ grace κΈ°κ°„ λ‚΄: Stale μΊμ‹œ μ¦‰μ‹œ λ°˜ν™˜, λ°±κ·ΈλΌμš΄λ“œμ—μ„œ κ°±μ‹ 
  • grace κΈ°κ°„ κ²½κ³Ό ν›„: μΊμ‹œ 미슀
Stale-While-RevalidateλŠ” μ‚¬μš©μžμ—κ²Œ λΉ λ₯Έ 응닡(Stale이라도 μ¦‰μ‹œ)을 μ œκ³΅ν•˜λ©΄μ„œ, λ°±κ·ΈλΌμš΄λ“œμ—μ„œ κ°±μ‹ ν•˜μ—¬ λ‹€μŒ μ‚¬μš©μžλŠ” μ‹ μ„ ν•œ 데이터λ₯Ό λ°›κ²Œ ν•˜λŠ” νŒ¨ν„΄μž…λ‹ˆλ‹€.

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;
  }
}

λ‹€μŒ 단계

@api

API μ—”λ“œν¬μΈνŠΈ λ§Œλ“€κΈ°

@transactional

νŠΈλžœμž­μ…˜ μ‚¬μš©ν•˜κΈ°

BentoCache

BentoCache 곡식 λ¬Έμ„œ

μ„±λŠ₯ μ΅œμ ν™”

μ„±λŠ₯ μ΅œμ ν™” κ°€μ΄λ“œ