๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
ํšจ๊ณผ์ ์ธ ์บ์‹ฑ์„ ์œ„ํ•ด์„œ๋Š” ๋ฐ์ดํ„ฐ ํŠน์„ฑ์— ๋งž๋Š” ์ „๋žต์„ ์„ ํƒํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ฐ€์ด๋“œ์—์„œ๋Š” ๋‹ค์–‘ํ•œ ์บ์‹œ ์ „๋žต๊ณผ ํ™œ์šฉ ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ด…๋‹ˆ๋‹ค.

TTL (Time To Live) ์ „๋žต

TTL์ด๋ž€?

TTL์€ ์บ์‹œ๊ฐ€ ์œ ํšจํ•œ ์‹œ๊ฐ„์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. TTL์ด ์ง€๋‚˜๋ฉด ์บ์‹œ๊ฐ€ ๋งŒ๋ฃŒ๋˜์–ด ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
@cache({ ttl: "10m" })  // 10๋ถ„ ํ›„ ๋งŒ๋ฃŒ
async getData() {
  return await this.expensiveQuery();
}

๋ฐ์ดํ„ฐ ํŠน์„ฑ๋ณ„ TTL ์„ค์ •

๋ณ€๊ฒฝ์ด ๊ฑฐ์˜ ์—†๋Š” ๋ฐ์ดํ„ฐ
// ์„ค์ • ๋ฐ์ดํ„ฐ (๊ฑฐ์˜ ๋ณ€๊ฒฝ ์•ˆ๋จ)
@cache({ ttl: "forever", tags: ["config"] })
async getConfig() {
  return this.findOne(['key', 'app_config']);
}

// ์นดํ…Œ๊ณ ๋ฆฌ (๊ฐ€๋” ๋ณ€๊ฒฝ)
@cache({ ttl: "1d", tags: ["category"] })
async getCategoryTree() {
  return this.buildCategoryTree();
}
TTL: "forever", "1d", "1w"

TTL ๋‹จ์œ„

// ์ดˆ (seconds)
@cache({ ttl: "30s" })

// ๋ถ„ (minutes)
@cache({ ttl: "10m" })

// ์‹œ๊ฐ„ (hours)
@cache({ ttl: "2h" })

// ์ผ (days)
@cache({ ttl: "7d" })

// ์ฃผ (weeks)
@cache({ ttl: "2w" })

// ์˜๊ตฌ
@cache({ ttl: "forever" })

// ๋ฐ€๋ฆฌ์ดˆ (์ˆซ์ž)
@cache({ ttl: 60000 })  // 60000ms = 1๋ถ„

Grace Period (Stale-While-Revalidate)

Grace๋ž€?

Grace๋Š” TTL ๋งŒ๋ฃŒ ํ›„์—๋„ ์˜ค๋ž˜๋œ ์บ์‹œ(Stale)๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉด์„œ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๊ฐฑ์‹ ํ•˜๋Š” ์ „๋žต์ž…๋‹ˆ๋‹ค.
@cache({ 
  ttl: "5m",     // 5๋ถ„ ํ›„ ๋งŒ๋ฃŒ
  grace: "1h"    // ๋งŒ๋ฃŒ ํ›„ 1์‹œ๊ฐ„ ๋™์•ˆ Stale ๊ฐ’ ์‚ฌ์šฉ
})
async getExpensiveData() {
  return await this.heavyComputation();
}

์ž‘๋™ ๋ฐฉ์‹

Grace ์‚ฌ์šฉ ์‹œ๊ธฐ

Grace ์‚ฌ์šฉ โœ…

๋ฌด๊ฑฐ์šด ๊ณ„์‚ฐ/์ฟผ๋ฆฌ
  • ์ง‘๊ณ„ ํ†ต๊ณ„
  • ๋ณต์žกํ•œ Join ์ฟผ๋ฆฌ
  • ์™ธ๋ถ€ API ํ˜ธ์ถœ
  • ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ
@cache({ 
  ttl: "10m", 
  grace: "1h" 
})
async getDashboard() {
  return await this.complexAggregation();
}

Grace ๋ถˆํ•„์š” โŒ

๋น ๋ฅธ ์กฐํšŒ
  • ๋‹จ์ˆœ SELECT
  • ์ธ๋ฑ์Šค ์กฐํšŒ
  • ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ
@cache({ ttl: "5m" })
// grace ์—†์Œ
async getUser(id: number) {
  return this.findOne(['id', id]);
}

Grace ์‹ค์ „ ์˜ˆ์ œ

class AnalyticsModelClass extends BaseModel {
  // ๋Œ€์‹œ๋ณด๋“œ ํ†ต๊ณ„ (๋ฌด๊ฑฐ์šด ์ง‘๊ณ„)
  @cache({ 
    ttl: "5m",      // 5๋ถ„๋งˆ๋‹ค ๊ฐฑ์‹ 
    grace: "2h",    // 2์‹œ๊ฐ„ ๋™์•ˆ Stale ํ—ˆ์šฉ
    tags: ["analytics"]
  })
  @api()
  async getDashboardStats() {
    const [userCount, orderCount, revenue] = await Promise.all([
      this.countUsers(),
      this.countOrders(),
      this.calculateRevenue(),
    ]);
    
    return { userCount, orderCount, revenue };
  }
  
  // ์‹ค์‹œ๊ฐ„ ์ˆœ์œ„ (๋งค์šฐ ๋ฌด๊ฑฐ์›€)
  @cache({ 
    ttl: "1m",      // 1๋ถ„๋งˆ๋‹ค ๊ฐฑ์‹ 
    grace: "10m",   // 10๋ถ„ ๋™์•ˆ Stale ํ—ˆ์šฉ
    tags: ["ranking"]
  })
  @api()
  async getRealTimeRanking() {
    // ์ˆ˜๋ฐฑ๋งŒ ๊ฑด์˜ ๋ฐ์ดํ„ฐ ์ง‘๊ณ„
    return await this.calculateRankingWithHeavyComputation();
  }
}

Grace vs ๊ธด TTL

Grace ์ „๋žต:
@cache({ ttl: "5m", grace: "1h" })
  • ๋Œ€๋ถ€๋ถ„ ์‹ ์„ ํ•œ ๋ฐ์ดํ„ฐ (5๋ถ„ ์ด๋‚ด)
  • ๋งŒ๋ฃŒ ์‹œ Stale ์ฆ‰์‹œ ๋ฐ˜ํ™˜ (๋น ๋ฆ„)
  • ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๊ฐฑ์‹ 
๊ธด TTL:
@cache({ ttl: "1h" })
  • ์ตœ๋Œ€ 1์‹œ๊ฐ„ ์˜ค๋ž˜๋œ ๋ฐ์ดํ„ฐ
  • ๋งŒ๋ฃŒ ์‹œ ์ƒˆ๋กœ ๊ณ„์‚ฐ (๋А๋ฆผ)
๊ถŒ์žฅ: ๋ฌด๊ฑฐ์šด ์ž‘์—…์€ Grace ์‚ฌ์šฉ

Namespace ์ „๋žต

Namespace๋ž€?

Namespace๋Š” ์บ์‹œ๋ฅผ ๋…ผ๋ฆฌ์ ์œผ๋กœ ๋ถ„๋ฆฌํ•˜๋Š” ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค.
const userCache = Sonamu.cache.namespace("user:123");
const adminCache = Sonamu.cache.namespace("admin");

// ๊ฐ™์€ ํ‚ค๋ผ๋„ ๋‹ค๋ฅธ namespace๋ฉด ๋‹ค๋ฅธ ์บ์‹œ
await userCache.set({ key: "data", value: "user data" });
await adminCache.set({ key: "data", value: "admin data" });

await userCache.get({ key: "data" });   // "user data"
await adminCache.get({ key: "data" });  // "admin data"

์‚ฌ์šฉ์ž๋ณ„ ๊ฒฉ๋ฆฌ

class UserDataModelClass extends BaseModel {
  @api()
  async getMyData(ctx: Context) {
    const userId = ctx.user.id;
    
    // ์‚ฌ์šฉ์ž๋ณ„ namespace
    const userCache = Sonamu.cache.namespace(`user:${userId}`);
    
    return userCache.getOrSet({
      key: "preferences",
      ttl: "1h",
      factory: async () => {
        return this.getUserPreferences(userId);
      }
    });
  }
  
  @api()
  async updateMyData(ctx: Context, data: any) {
    const userId = ctx.user.id;
    const result = await this.saveUserData(userId, data);
    
    // ํ•ด๋‹น ์‚ฌ์šฉ์ž์˜ ์บ์‹œ๋งŒ ์‚ญ์ œ
    const userCache = Sonamu.cache.namespace(`user:${userId}`);
    await userCache.clear();
    
    return result;
  }
}
์žฅ์ :
  • ์‚ฌ์šฉ์ž A์˜ ๋ณ€๊ฒฝ์ด ์‚ฌ์šฉ์ž B์— ์˜ํ–ฅ ์—†์Œ
  • ์„ ํƒ์  ๋ฌดํšจํ™” ๊ฐ€๋Šฅ

๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ

class TenantServiceModelClass extends BaseModel {
  @api()
  async getData(tenantId: number) {
    // ํ…Œ๋„ŒํŠธ๋ณ„ namespace
    const tenantCache = Sonamu.cache.namespace(`tenant:${tenantId}`);
    
    return tenantCache.getOrSet({
      key: "service-data",
      ttl: "1h",
      tags: ["service"],
      factory: async () => {
        return this.findMany({ 
          where: [['tenant_id', tenantId]] 
        });
      }
    });
  }
  
  @api()
  async clearTenantCache(tenantId: number) {
    const tenantCache = Sonamu.cache.namespace(`tenant:${tenantId}`);
    
    // ํŠน์ • ํ…Œ๋„ŒํŠธ ์บ์‹œ๋งŒ ์‚ญ์ œ
    await tenantCache.clear();
  }
}

๊ธฐ๋Šฅ๋ณ„ ๊ฒฉ๋ฆฌ

// ์ธ์ฆ ๊ด€๋ จ
const authCache = Sonamu.cache.namespace("auth");
await authCache.set({ key: `session:${sessionId}`, value: session, ttl: "1h" });

// ํ†ต๊ณ„ ๊ด€๋ จ
const statsCache = Sonamu.cache.namespace("stats");
await statsCache.set({ key: "daily", value: stats, ttl: "1d" });

// API ๋ ˆ์ดํŠธ ๋ฆฌ๋ฐ‹
const rateLimitCache = Sonamu.cache.namespace("ratelimit");
await rateLimitCache.set({ key: `user:${userId}`, value: count, ttl: "1m" });

์บ์‹œ ํŒจํ„ด

1. Cache-Aside (Lazy Loading)

๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ํŒจํ„ด: ํ•„์š”ํ•  ๋•Œ ์กฐํšŒํ•˜๊ณ  ์บ์‹ฑ
async getData(id: number) {
  // 1. ์บ์‹œ ํ™•์ธ
  const cached = await Sonamu.cache.get({ key: `data:${id}` });
  if (cached) return cached;
  
  // 2. DB ์กฐํšŒ
  const data = await this.findOne(['id', id]);
  
  // 3. ์บ์‹ฑ
  await Sonamu.cache.set({ 
    key: `data:${id}`, 
    value: data, 
    ttl: "10m" 
  });
  
  return data;
}
@cache ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๊ฐ€ ์ด ํŒจํ„ด์„ ์ž๋™ ๊ตฌํ˜„

2. Write-Through

์“ฐ๊ธฐ ์‹œ ์บ์‹œ๋„ ๊ฐฑ์‹ : ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ๊ณผ ๋™์‹œ์— ์บ์‹œ ์—…๋ฐ์ดํŠธ
@api()
async update(id: number, data: Partial<DataSave>) {
  // 1. DB ์—…๋ฐ์ดํŠธ
  const result = await this.updateOne(['id', id], data);
  
  // 2. ์บ์‹œ ๊ฐฑ์‹ 
  await Sonamu.cache.set({ 
    key: `data:${id}`, 
    value: result, 
    ttl: "10m",
    tags: ["data"]
  });
  
  return result;
}
์žฅ์ : ์บ์‹œ ํ•ญ์ƒ ์ตœ์‹  ์ƒํƒœ ๋‹จ์ : ์“ฐ๊ธฐ ์„ฑ๋Šฅ ์ €ํ•˜

3. Write-Behind (Write-Back)

์“ฐ๊ธฐ๋ฅผ ์บ์‹œ์— ๋จผ์ €: ์บ์‹œ ์—…๋ฐ์ดํŠธ ํ›„ ๋น„๋™๊ธฐ๋กœ DB ์ €์žฅ
@api()
async updateCounter(id: number) {
  const key = `counter:${id}`;
  
  // 1. ์บ์‹œ ์ฆ๊ฐ€
  let count = await Sonamu.cache.get({ key });
  count = (count ?? 0) + 1;
  await Sonamu.cache.set({ key, value: count, ttl: "10m" });
  
  // 2. ๋น„๋™๊ธฐ DB ์ €์žฅ (ํ์— ๋„ฃ๊ธฐ)
  await this.queueCounterUpdate(id, count);
  
  return count;
}
์žฅ์ : ๋น ๋ฅธ ์‘๋‹ต ๋‹จ์ : ๊ตฌํ˜„ ๋ณต์žก, ๋ฐ์ดํ„ฐ ์œ ์‹ค ๊ฐ€๋Šฅ

4. Refresh-Ahead

๋งŒ๋ฃŒ ์ „ ๋ฏธ๋ฆฌ ๊ฐฑ์‹ : TTL์ด ๊ฑฐ์˜ ๋๋‚  ๋•Œ ๋ฏธ๋ฆฌ ์ƒˆ ๋ฐ์ดํ„ฐ ์ค€๋น„
@cache({ 
  ttl: "10m",
  grace: "1h"  // Grace๊ฐ€ Refresh-Ahead์™€ ์œ ์‚ฌ
})
async getData() {
  return await this.expensiveQuery();
}
Grace period๊ฐ€ ์ด ์—ญํ• ์„ ์ˆ˜ํ–‰

์กฐํ•ฉ ์ „๋žต

๋ ˆ์ด์–ด๋ณ„ TTL

class ProductModelClass extends BaseModel {
  // L1 (๋ฉ”๋ชจ๋ฆฌ): ์งง์€ TTL
  // L2 (Redis): ๊ธด TTL
  @cache({ 
    ttl: "5m",      // L1์€ 5๋ถ„
    // L2๋Š” ์ž๋™์œผ๋กœ ๋” ๊ธธ๊ฒŒ ์œ ์ง€๋จ
    tags: ["product"] 
  })
  @api()
  async findById(id: number) {
    return this.findOne(['id', id]);
  }
}

์ค‘์š”๋„๋ณ„ ์ „๋žต

ํ•ญ์ƒ ์ •ํ™•ํ•ด์•ผ ํ•˜๋Š” ๋ฐ์ดํ„ฐ
// ๊ฒฐ์ œ ์ •๋ณด (์บ์‹ฑ ์—†์Œ ๋˜๋Š” ์งง์€ TTL)
@cache({ ttl: "30s", grace: false })
async getPaymentInfo(id: number) {
  return this.findOne(['id', id]);
}

// ์žฌ๊ณ  ์ •๋ณด (์งง์€ TTL + ์ฆ‰์‹œ ๋ฌดํšจํ™”)
@cache({ ttl: "1m", tags: ["inventory"] })
async getInventory(productId: number) {
  return this.getStock(productId);
}

์‹œ๊ฐ„๋Œ€๋ณ„ ์ „๋žต

class DynamicCacheModelClass extends BaseModel {
  @api()
  async getData() {
    const hour = new Date().getHours();
    
    // ํ”ผํฌ ์‹œ๊ฐ„ (์˜ค์ „ 9์‹œ~์˜คํ›„ 6์‹œ): ์งง์€ TTL
    const ttl = (hour >= 9 && hour <= 18) ? "5m" : "30m";
    
    return Sonamu.cache.getOrSet({
      key: "dynamic-data",
      ttl,
      tags: ["data"],
      factory: async () => {
        return this.expensiveQuery();
      }
    });
  }
}

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

์บ์‹œ ์›Œ๋ฐ

์„œ๋ฒ„ ์‹œ์ž‘ ์‹œ ๋ฏธ๋ฆฌ ์บ์‹œ๋ฅผ ์ฑ„์›๋‹ˆ๋‹ค.
class CacheWarmerModelClass extends BaseModel {
  async warmupCache() {
    console.log("Warming up cache...");
    
    // ์ž์ฃผ ์‚ฌ์šฉ๋˜๋Š” ๋ฐ์ดํ„ฐ ๋ฏธ๋ฆฌ ์บ์‹ฑ
    await Promise.all([
      this.getCategoryTree(),  // @cache ์ ์šฉ๋จ
      this.getPopularProducts(),
      this.getConfig(),
    ]);
    
    console.log("Cache warmed up!");
  }
  
  @cache({ ttl: "1d", tags: ["category"] })
  async getCategoryTree() {
    return this.buildTree();
  }
  
  @cache({ ttl: "1h", tags: ["popular"] })
  async getPopularProducts() {
    return this.findPopular();
  }
}

// ์„œ๋ฒ„ ์‹œ์ž‘ ์‹œ ์‹คํ–‰
await cacheWarmerModel.warmupCache();

๋ฐฐ์น˜ ์บ์‹ฑ

์—ฌ๋Ÿฌ ํ•ญ๋ชฉ์„ ํ•œ ๋ฒˆ์— ์บ์‹ฑํ•ฉ๋‹ˆ๋‹ค.
class BatchCacheModelClass extends BaseModel {
  async batchCache(ids: number[]) {
    // 1. ์บ์‹œ๋˜์ง€ ์•Š์€ ID ์ฐพ๊ธฐ
    const cacheKeys = ids.map(id => `item:${id}`);
    const cached = await Promise.all(
      cacheKeys.map(key => Sonamu.cache.get({ key }))
    );
    
    const uncachedIds = ids.filter((_, i) => !cached[i]);
    
    if (uncachedIds.length === 0) {
      return cached.filter(Boolean);
    }
    
    // 2. DB์—์„œ ์กฐํšŒ
    const items = await this.findMany({ 
      where: [['id', 'in', uncachedIds]] 
    });
    
    // 3. ๋ฐฐ์น˜ ์บ์‹ฑ
    await Promise.all(
      items.rows.map(item =>
        Sonamu.cache.set({ 
          key: `item:${item.id}`, 
          value: item, 
          ttl: "10m" 
        })
      )
    );
    
    // 4. ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜
    return [...cached.filter(Boolean), ...items.rows];
  }
}

์„ ํƒ์  ์บ์‹ฑ

์กฐ๊ฑด์— ๋”ฐ๋ผ ์บ์‹ฑ ์—ฌ๋ถ€๋ฅผ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค.
class ConditionalCacheModelClass extends BaseModel {
  @api()
  async getData(id: number, useCache: boolean = true) {
    const cacheKey = `data:${id}`;
    
    // ์บ์‹œ ์‚ฌ์šฉ ์—ฌ๋ถ€ ์„ ํƒ
    if (!useCache) {
      return this.findOne(['id', id]);
    }
    
    return Sonamu.cache.getOrSet({
      key: cacheKey,
      ttl: "10m",
      factory: async () => {
        return this.findOne(['id', id]);
      }
    });
  }
}

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

์บ์‹œ ์ „๋žต ์„ ํƒ ์‹œ ์ฃผ์˜์‚ฌํ•ญ:
  1. TTL์ด ๋„ˆ๋ฌด ๊ธธ๋ฉด: ์˜ค๋ž˜๋œ ๋ฐ์ดํ„ฐ ์ œ๊ณต
    // โŒ ์ž์ฃผ ๋ณ€๊ฒฝ๋˜๋Š”๋ฐ TTL์ด ๋„ˆ๋ฌด ๊น€
    @cache({ ttl: "1d" })  
    async getLiveStock() { ... }
    
    // โœ… ์ ์ ˆํ•œ TTL
    @cache({ ttl: "1m" })
    async getLiveStock() { ... }
    
  2. TTL์ด ๋„ˆ๋ฌด ์งง์œผ๋ฉด: ์บ์‹œ ํšจ๊ณผ ๊ฐ์†Œ
    // โŒ ๊ฑฐ์˜ ๋ณ€๊ฒฝ ์•ˆ ๋˜๋Š”๋ฐ TTL์ด ๋„ˆ๋ฌด ์งง์Œ
    @cache({ ttl: "10s" })
    async getConfig() { ... }
    
    // โœ… ์ ์ ˆํ•œ TTL
    @cache({ ttl: "1d", tags: ["config"] })
    async getConfig() { ... }
    
  3. Grace ์˜ค๋‚จ์šฉ: ๋น ๋ฅธ ์ฟผ๋ฆฌ์— Grace ๋ถˆํ•„์š”
    // โŒ ๋ถˆํ•„์š”ํ•œ Grace
    @cache({ ttl: "5m", grace: "1h" })
    async getUser(id: number) {
      return this.findOne(['id', id]);  // ๋น ๋ฅธ ์กฐํšŒ
    }
    
    // โœ… Grace ์—†์ด ์ถฉ๋ถ„
    @cache({ ttl: "5m" })
    async getUser(id: number) { ... }
    
  4. Namespace ๋‚จ์šฉ: ๋„ˆ๋ฌด ๋งŽ์€ namespace๋Š” ๊ด€๋ฆฌ ์–ด๋ ค์›€
    // โŒ ๊ณผ๋„ํ•œ namespace
    const cache1 = Sonamu.cache.namespace("a:b:c:d:e");
    
    // โœ… ์ ์ ˆํ•œ namespace
    const userCache = Sonamu.cache.namespace(`user:${userId}`);
    

์ „๋žต ์š”์•ฝํ‘œ

๋ฐ์ดํ„ฐ ์œ ํ˜•TTLGraceTags๋ฌดํšจํ™”
์ •์  ์„ค์ •foreverโŒ["config"]์ˆ˜๋™
์‚ฌ์šฉ์ž ํ”„๋กœํ•„1hโŒ["user"]๋ณ€๊ฒฝ ์‹œ
์ƒํ’ˆ ์ •๋ณด30m1h["product"]๋ณ€๊ฒฝ ์‹œ
๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก5m30m["post", "list"]์ƒ์„ฑ/์ˆ˜์ • ์‹œ
์‹ค์‹œ๊ฐ„ ํ†ต๊ณ„1m10m["stats"]TTL ์˜์กด
์ง‘๊ณ„ ๋Œ€์‹œ๋ณด๋“œ10m2h["dashboard"]TTL ์˜์กด

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