๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
Sonamu์˜ BentoCache๋ฅผ ํ™œ์šฉํ•œ ํšจ๊ณผ์ ์ธ ์บ์‹ฑ ์ „๋žต์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

BentoCache๋ž€?

Sonamu๋Š” BentoCache๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ•๋ ฅํ•œ ์บ์‹ฑ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ฃผ์š” ํŠน์ง•:
  • ๋‹ค์–‘ํ•œ ๋“œ๋ผ์ด๋ฒ„ (๋ฉ”๋ชจ๋ฆฌ, Redis, PostgreSQL ๋“ฑ)
  • TTL (Time To Live) ์„ค์ •
  • ๋„ค์ž„์ŠคํŽ˜์ด์Šค ๊ธฐ๋ฐ˜ ๊ด€๋ฆฌ
  • ์บ์‹œ ์›Œ๋ฐ (Cache Warming)
  • ํƒœ๊ทธ ๊ธฐ๋ฐ˜ ๋ฌดํšจํ™”

์บ์‹œ ์„ค์ •

sonamu.config.ts ์„ค์ •

import { bentostore } from "bentocache";
import { memoryDriver } from "bentocache/drivers/memory";

export default {
  server: {
    cache: {
      default: "memory",
      stores: {
        memory: bentostore()
          .useL1Layer(memoryDriver({ 
            maxItems: 10_000,
            maxSize: 100_000_000  // 100MB
          }))
      }
    }
  }
} satisfies SonamuConfig;

Redis ๋“œ๋ผ์ด๋ฒ„

import { bentostore } from "bentocache";
import { redisDriver } from "bentocache/drivers/redis";

export default {
  server: {
    cache: {
      default: "redis",
      stores: {
        redis: bentostore()
          .useL1Layer(redisDriver({
            connection: {
              host: process.env.REDIS_HOST || "localhost",
              port: 6379,
              password: process.env.REDIS_PASSWORD
            }
          }))
      }
    }
  }
} satisfies SonamuConfig;

๋‹ค์ค‘ ๋“œ๋ผ์ด๋ฒ„

export default {
  server: {
    cache: {
      default: "memory",
      stores: {
        memory: bentostore()
          .useL1Layer(memoryDriver({ maxSize: 50_000_000 })),
        redis: bentostore()
          .useL1Layer(redisDriver({
            connection: { host: "localhost", port: 6379 }
          }))
      }
    }
  }
} satisfies SonamuConfig;

@cache ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ

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

import { cache, Puri } from "sonamu";

class UserModelClass extends BaseModel {
  // 5๋ถ„๊ฐ„ ์บ์‹ฑ
  @cache({ ttl: '5m' })
  async getActiveUsersCount(): Promise<number> {
    const result = await this.getPuri()
      .select({ count: Puri.count() })
      .where({ status: "active" });
    return result[0].count;
  }
}

TTL ์„ค์ •

class ProductModelClass extends BaseModel {
  // 1์‹œ๊ฐ„ ์บ์‹ฑ
  @cache({ ttl: '1h' })
  async getFeaturedProducts() {
    return this.getPuri()
      .select("*")
      .where({ featured: true });
  }
  
  // 1์ผ ์บ์‹ฑ
  @cache({ ttl: '1d' })
  async getCategoryCounts() {
    return this.getPuri()
      .select("category", knex.raw("COUNT(*) as count"))
      .groupBy("category");
  }
  
  // ์˜๊ตฌ ์บ์‹ฑ (์ˆ˜๋™ ๋ฌดํšจํ™” ํ•„์š”)
  @cache({ ttl: 0 })
  async getProductCategories() {
    return this.getPuri()
      .select("DISTINCT category");
  }
}
TTL ํ˜•์‹:
  • '5s': 5์ดˆ
  • '5m': 5๋ถ„
  • '1h': 1์‹œ๊ฐ„
  • '1d': 1์ผ
  • 0: ์˜๊ตฌ (๋ฌดํšจํ™” ํ•„์š”)

ํŒŒ๋ผ๋ฏธํ„ฐ ๊ธฐ๋ฐ˜ ์บ์‹ฑ

class UserModelClass extends BaseModel {
  // ํŒŒ๋ผ๋ฏธํ„ฐ๋ณ„๋กœ ๋‹ค๋ฅธ ์บ์‹œ ์ƒ์„ฑ
  @cache({ ttl: '5m' })
  async getUsersByStatus(status: string) {
    return this.getPuri()
      .select("id", "name", "email")
      .where({ status });
  }
}

// ๊ฐ๊ฐ ๋‹ค๋ฅธ ์บ์‹œ ํ‚ค๋กœ ์ €์žฅ๋จ
await UserModel.getUsersByStatus("active");   // UserModel.getUsersByStatus:active
await UserModel.getUsersByStatus("inactive"); // UserModel.getUsersByStatus:inactive

์บ์‹œ ํ‚ค ์ปค์Šคํ„ฐ๋งˆ์ด์ง•

class PostModelClass extends BaseModel {
  @cache({ 
    ttl: '10m',
    key: (userId: number, page: number) => `posts:user:${userId}:page:${page}`
  })
  async getUserPosts(userId: number, page: number = 1) {
    return this.getPuri()
      .select("*")
      .where({ user_id: userId })
      .offset((page - 1) * 20)
      .limit(20);
  }
}

์ˆ˜๋™ ์บ์‹œ ๊ด€๋ฆฌ

Sonamu.cache ์‚ฌ์šฉ

import { Sonamu, Puri } from "sonamu";

class StatisticsService {
  async getDailyStats(date: string) {
    const cacheKey = `stats:daily:${date}`;
    
    // ์บ์‹œ ํ™•์ธ
    const cached = await Sonamu.cache.get(cacheKey);
    if (cached) {
      console.log("์บ์‹œ ํžˆํŠธ!");
      return cached;
    }
    
    // ์บ์‹œ ๋ฏธ์Šค - ๊ณ„์‚ฐ
    console.log("์บ์‹œ ๋ฏธ์Šค, ๊ณ„์‚ฐ ์ค‘...");
    const stats = await this.calculateDailyStats(date);
    
    // ์บ์‹œ ์ €์žฅ (24์‹œ๊ฐ„)
    await Sonamu.cache.set(cacheKey, stats, { ttl: '24h' });
    
    return stats;
  }
  
  private async calculateDailyStats(date: string) {
    // ๋ณต์žกํ•œ ํ†ต๊ณ„ ๊ณ„์‚ฐ
    const [userCount, orderCount, revenue] = await Promise.all([
      UserModel.getPuri()
        .select({ count: Puri.count() })
        .then(r => r[0].count),
      OrderModel.getPuri()
        .select({ count: Puri.count() })
        .where("created_at", ">=", date)
        .then(r => r[0].count),
      OrderModel.getPuri()
        .select({ total: Puri.sum("total") })
        .where("created_at", ">=", date)
        .then(r => r[0].total)
    ]);
    
    return {
      date,
      userCount,
      orderCount,
      revenue
    };
  }
}

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

class UserModelClass extends BaseModel {
  @api({ httpMethod: "POST" })
  async createUser(params: UserSaveParams) {
    const user = await this.save(params);
    
    // ๊ด€๋ จ ์บ์‹œ ๋ฌดํšจํ™”
    await Sonamu.cache.delete("users:active:count");
    await Sonamu.cache.delete("users:stats");
    
    return user;
  }
  
  @api({ httpMethod: "PUT" })
  async updateUserStatus(id: number, status: string) {
    await this.save({ id, status });
    
    // ํŒจํ„ด ๋งค์นญ์œผ๋กœ ๊ด€๋ จ ์บ์‹œ ๋ฌดํšจํ™”
    await Sonamu.cache.clear();  // ์ฃผ์˜: ์ „์ฒด ์บ์‹œ ์‚ญ์ œ
  }
}

getOrSet ํŒจํ„ด

class CacheService {
  async getWithCache(key: string, factory: () => Promise<any>) {
    return Sonamu.cache.getOrSet({
      key,
      ttl: '1h',
      factory
    });
  }
  
  async getUser(userId: number) {
    return this.getWithCache(
      `user:${userId}`,
      () => UserModel.findById(userId)
    );
  }
}

์บ์‹ฑ ์ „๋žต

1. Read-Through ํŒจํ„ด

class ProductModelClass extends BaseModel {
  async getProductWithCache(id: number): Promise<Product> {
    const cacheKey = `product:${id}`;
    
    return Sonamu.cache.getOrSet({
      key: cacheKey,
      ttl: '1h',
      factory: () => this.findById(id)
    });
  }
}

2. Write-Through ํŒจํ„ด

class ProductModelClass extends BaseModel {
  async updateProduct(id: number, params: ProductSaveParams) {
    // DB ์—…๋ฐ์ดํŠธ
    const product = await this.save({ id, ...params });
    
    // ์บ์‹œ๋„ ํ•จ๊ป˜ ์—…๋ฐ์ดํŠธ
    await Sonamu.cache.set(`product:${id}`, product, { ttl: '1h' });
    
    return product;
  }
}

3. Cache-Aside ํŒจํ„ด

class ProductModelClass extends BaseModel {
  async getProduct(id: number) {
    // 1. ์บ์‹œ ํ™•์ธ
    const cached = await Sonamu.cache.get(`product:${id}`);
    if (cached) return cached;
    
    // 2. DB ์กฐํšŒ
    const product = await this.findById(id);
    
    // 3. ์บ์‹œ ์ €์žฅ (์‘๋‹ต ํ›„ ๋น„๋™๊ธฐ)
    setImmediate(async () => {
      await Sonamu.cache.set(`product:${id}`, product, { ttl: '1h' });
    });
    
    return product;
  }
}

4. Cache Warming

class ProductModelClass extends BaseModel {
  // ์„œ๋ฒ„ ์‹œ์ž‘ ์‹œ ์ธ๊ธฐ ์ƒํ’ˆ ์บ์‹œ ์›Œ๋ฐ
  async warmPopularProducts() {
    const popularProducts = await this.getPuri()
      .select("*")
      .where({ featured: true })
      .orWhere("view_count", ">", 1000);
    
    for (const product of popularProducts) {
      await Sonamu.cache.set(
        `product:${product.id}`,
        product,
        { ttl: '1h' }
      );
    }
    
    console.log(`${popularProducts.length}๊ฐœ ์ƒํ’ˆ ์บ์‹œ ์›Œ๋ฐ ์™„๋ฃŒ`);
  }
}

์‹ค์ „ ์˜ˆ์ œ

API ์‘๋‹ต ์บ์‹ฑ

class PostModelClass extends BaseModel {
  // ๋ชฉ๋ก API - 10๋ถ„ ์บ์‹ฑ
  @cache({ ttl: '10m' })
  @api({ httpMethod: "GET" })
  async listPosts(page: number = 1) {
    return this.getPuri()
      .select("id", "title", "author_id", "created_at")
      .orderBy("created_at", "desc")
      .offset((page - 1) * 20)
      .limit(20);
  }
  
  // ์ƒ์„ธ API - 5๋ถ„ ์บ์‹ฑ
  @cache({ ttl: '5m' })
  @api({ httpMethod: "GET" })
  async getPost(id: number) {
    const { qb } = this.getSubsetQueries("Detail");
    const result = await this.executeSubsetQuery({
      subset: "Detail",
      qb: qb.where({ id }),
      params: { num: 1, page: 1 }
    });
    return result.rows[0];
  }
  
  // ์ƒ์„ฑ API - ๊ด€๋ จ ์บ์‹œ ๋ฌดํšจํ™”
  @api({ httpMethod: "POST" })
  async createPost(params: PostSaveParams) {
    const post = await this.save(params);
    
    // ๋ชฉ๋ก ์บ์‹œ ๋ฌดํšจํ™”๋Š” ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌ๋จ (ํ‚ค๊ฐ€ ๋‹ค๋ฅด๋ฏ€๋กœ)
    // ํŠน์ • ์บ์‹œ๋งŒ ๋ฌดํšจํ™”ํ•˜๋ ค๋ฉด ์ˆ˜๋™์œผ๋กœ ์ฒ˜๋ฆฌ
    
    return post;
  }
}

์ง‘๊ณ„ ๋ฐ์ดํ„ฐ ์บ์‹ฑ

class DashboardService {
  // ๋Œ€์‹œ๋ณด๋“œ ํ†ต๊ณ„ - 5๋ถ„ ์บ์‹ฑ
  async getDashboardStats() {
    const cacheKey = "dashboard:stats";
    
    return Sonamu.cache.getOrSet({
      key: cacheKey,
      ttl: '5m',
      factory: async () => {
        // ์—ฌ๋Ÿฌ ์ง‘๊ณ„ ์ฟผ๋ฆฌ ๋ณ‘๋ ฌ ์‹คํ–‰
        const [
          totalUsers,
          activeUsers,
          totalOrders,
          todayRevenue
        ] = await Promise.all([
          UserModel.getPuri()
            .select({ count: Puri.count() })
            .then(r => r[0].count),
          UserModel.getPuri()
            .select({ count: Puri.count() })
            .where({ status: "active" })
            .then(r => r[0].count),
          OrderModel.getPuri()
            .select({ count: Puri.count() })
            .then(r => r[0].count),
          OrderModel.getPuri()
            .select({ total: Puri.sum("total") })
            .where("created_at", ">=", new Date().toISOString().split("T")[0])
            .then(r => r[0].total)
        ]);
        
        return {
          totalUsers,
          activeUsers,
          totalOrders,
          todayRevenue,
          cachedAt: new Date()
        };
      }
    });
  }
}

์‚ฌ์šฉ์ž ์„ธ์…˜ ์บ์‹ฑ

class AuthService {
  // ์„ธ์…˜ ์ €์žฅ (Redis ๊ถŒ์žฅ)
  async saveSession(userId: number, token: string) {
    const sessionData = {
      userId,
      token,
      createdAt: new Date()
    };
    
    await Sonamu.cache.set(
      `session:${token}`,
      sessionData,
      { ttl: '24h' }
    );
  }
  
  // ์„ธ์…˜ ์กฐํšŒ
  async getSession(token: string) {
    return Sonamu.cache.get(`session:${token}`);
  }
  
  // ๋กœ๊ทธ์•„์›ƒ
  async logout(token: string) {
    await Sonamu.cache.delete(`session:${token}`);
  }
}

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

1. Time-Based (TTL)

// ๊ฐ€์žฅ ๊ฐ„๋‹จ - TTL๋กœ ์ž๋™ ๋งŒ๋ฃŒ
@cache({ ttl: '5m' })  // 5๋ถ„ ํ›„ ์ž๋™ ๋งŒ๋ฃŒ
async getData() {
  return this.getPuri();
}

2. Event-Based

class ProductModelClass extends BaseModel {
  @api({ httpMethod: "POST" })
  async createProduct(params: ProductSaveParams) {
    const product = await this.save(params);
    
    // ๊ด€๋ จ ์บ์‹œ ๋ฌดํšจํ™”
    await this.invalidateProductCaches();
    
    return product;
  }
  
  private async invalidateProductCaches() {
    // ๊ฐœ๋ณ„ ์‚ญ์ œ
    await Sonamu.cache.delete("featured:products");
    await Sonamu.cache.delete("stats:products");
  }
}

Best Practices

1. ์ ์ ˆํ•œ TTL ์„ค์ •

// โœ… ์ž์ฃผ ๋ณ€๊ฒฝ๋˜๋Š” ๋ฐ์ดํ„ฐ - ์งง์€ TTL
@cache({ ttl: '1m' })  // 1๋ถ„
async getCurrentOnlineUsers() { }

// โœ… ๊ฑฐ์˜ ๋ณ€๊ฒฝ๋˜์ง€ ์•Š๋Š” ๋ฐ์ดํ„ฐ - ๊ธด TTL
@cache({ ttl: '1d' })  // 1์ผ
async getCountries() { }

// โœ… ์ •์  ๋ฐ์ดํ„ฐ - ์˜๊ตฌ
@cache({ ttl: 0 })
async getAppConfig() { }

2. ์บ์‹œ ํ‚ค ๋„ค์ด๋ฐ

// โœ… ๋ช…ํ™•ํ•˜๊ณ  ๊ณ„์ธต์ ์ธ ํ‚ค
"user:123"
"user:123:posts"
"stats:daily:2025-01-11"
"product:category:electronics"

// โŒ ๋ถˆ๋ช…ํ™•ํ•œ ํ‚ค
"u123"
"data"
"temp"

3. ์บ์‹œ ํฌ๊ธฐ ๊ด€๋ฆฌ

// ํฐ ๊ฐ์ฒด๋Š” ํ•„์š”ํ•œ ํ•„๋“œ๋งŒ ์บ์‹ฑ
async cacheUser(user: User) {
  const lightUser = {
    id: user.id,
    name: user.name,
    email: user.email
    // ๋Œ€์šฉ๋Ÿ‰ ํ•„๋“œ ์ œ์™ธ
  };
  
  await Sonamu.cache.set(`user:${user.id}`, lightUser, { ttl: '1h' });
}

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

// ์„œ๋ฒ„ ์‹œ์ž‘ ์‹œ ์ค‘์š” ๋ฐ์ดํ„ฐ ๋ฏธ๋ฆฌ ์บ์‹ฑ
export default {
  server: {
    lifecycle: {
      async onStart() {
        await ProductModel.warmPopularProducts();
        await CategoryModel.warmAllCategories();
        console.log("์บ์‹œ ์›Œ๋ฐ ์™„๋ฃŒ");
      }
    }
  }
} satisfies SonamuConfig;

5. ์‹คํŒจ ๋Œ€์‘

async getDataWithFallback(id: number) {
  try {
    // ์บ์‹œ ์‹œ๋„
    const cached = await Sonamu.cache.get(`data:${id}`);
    if (cached) return cached;
  } catch (error) {
    console.error("์บ์‹œ ์˜ค๋ฅ˜:", error);
    // ์บ์‹œ ์‹คํŒจํ•ด๋„ ๊ณ„์† ์ง„ํ–‰
  }
  
  // DB ์กฐํšŒ (์บ์‹œ ์—†๊ฑฐ๋‚˜ ์‹คํŒจ ์‹œ)
  return this.findById(id);
}

์„ฑ๋Šฅ ๋น„๊ต

์บ์‹ฑ ์ „

์š”์ฒญ 100ํšŒ:
- ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„: 150ms
- DB ์ฟผ๋ฆฌ: 100ํšŒ
- ์ด ์ฒ˜๋ฆฌ ์‹œ๊ฐ„: 15์ดˆ

์บ์‹ฑ ํ›„

์š”์ฒญ 100ํšŒ:
- ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„: 5ms (์บ์‹œ ํžˆํŠธ)
- DB ์ฟผ๋ฆฌ: 1ํšŒ (์ฒซ ์š”์ฒญ)
- ์ด ์ฒ˜๋ฆฌ ์‹œ๊ฐ„: 0.5์ดˆ
30๋ฐฐ ์ด์ƒ ์„ฑ๋Šฅ ํ–ฅ์ƒ!

์ฒดํฌ๋ฆฌ์ŠคํŠธ

ํšจ๊ณผ์ ์ธ ์บ์‹ฑ์„ ์œ„ํ•ด:
  • ์ž์ฃผ ์กฐํšŒ๋˜๋Š” ๋ฐ์ดํ„ฐ ํŒŒ์•…
  • ์ ์ ˆํ•œ TTL ์„ค์ • (๋ฌธ์ž์—ด ํ˜•์‹)
  • ๋ช…ํ™•ํ•œ ์บ์‹œ ํ‚ค ๋„ค์ด๋ฐ
  • ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ ์บ์‹œ ๋ฌดํšจํ™”
  • ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ๋ชจ๋‹ˆํ„ฐ๋ง
  • ์บ์‹œ ์›Œ๋ฐ ์ „๋žต
  • ์‹คํŒจ ๋Œ€์‘ ๋กœ์ง

๊ด€๋ จ ๋ฌธ์„œ