메인 μ½˜ν…μΈ λ‘œ κ±΄λ„ˆλ›°κΈ°
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 BaseModelClass {
  // 5λΆ„κ°„ 캐싱
  @cache({ ttl: "5m" })
  async getActiveUsersCount(): Promise<number> {
    const result = await this.getPuri("r").table("users").select({ count: Puri.count() }).where({ status: "active" });
    return result[0].count;
  }
}

TTL μ„€μ •

class ProductModelClass extends BaseModelClass {
  // 1μ‹œκ°„ 캐싱
  @cache({ ttl: "1h" })
  async getFeaturedProducts() {
    return this.getPuri("r").table("products").select("*").where({ featured: true });
  }

  // 1일 캐싱
  @cache({ ttl: "1d" })
  async getCategoryCounts() {
    return this.getPuri("r").table("products").select({ category: "category", count: Puri.count() }).groupBy("category");
  }

  // 영ꡬ 캐싱 (μˆ˜λ™ λ¬΄νš¨ν™” ν•„μš”)
  @cache({ ttl: 0 })
  async getProductCategories() {
    return this.getPuri("r").table("products").select("DISTINCT category");
  }
}
TTL ν˜•μ‹:
  • '5s': 5초
  • '5m': 5λΆ„
  • '1h': 1μ‹œκ°„
  • '1d': 1일
  • 0: 영ꡬ (λ¬΄νš¨ν™” ν•„μš”)

νŒŒλΌλ―Έν„° 기반 캐싱

class UserModelClass extends BaseModelClass {
  // νŒŒλΌλ―Έν„°λ³„λ‘œ λ‹€λ₯Έ μΊμ‹œ 생성
  @cache({ ttl: "5m" })
  async getUsersByStatus(status: string) {
    return this.getPuri("r").table("users").select("id", "name", "email").where({ status });
  }
}

// 각각 λ‹€λ₯Έ μΊμ‹œ ν‚€λ‘œ μ €μž₯됨
await UserModel.getUsersByStatus("active"); // UserModel.getUsersByStatus:active
await UserModel.getUsersByStatus("inactive"); // UserModel.getUsersByStatus:inactive

μΊμ‹œ ν‚€ μ»€μŠ€ν„°λ§ˆμ΄μ§•

class PostModelClass extends BaseModelClass {
  @cache({
    ttl: "10m",
    key: (userId: number, page: number) => `posts:user:${userId}:page:${page}`,
  })
  async getUserPosts(userId: number, page: number = 1) {
    return this.getPuri("r")
      .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("r")
        .table("users")
        .select({ count: Puri.count() })
        .then((r) => r[0].count),
      OrderModel.getPuri("r")
        .table("orders")
        .select({ count: Puri.count() })
        .where("created_at", ">=", date)
        .then((r) => r[0].count),
      OrderModel.getPuri("r")
        .table("orders")
        .select({ total: Puri.sum("total") })
        .where("created_at", ">=", date)
        .then((r) => r[0].total),
    ]);

    return {
      date,
      userCount,
      orderCount,
      revenue,
    };
  }
}

μΊμ‹œ λ¬΄νš¨ν™”

class UserModelClass extends BaseModelClass {
  @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 BaseModelClass {
  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 BaseModelClass {
  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 BaseModelClass {
  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 BaseModelClass {
  // μ„œλ²„ μ‹œμž‘ μ‹œ 인기 μƒν’ˆ μΊμ‹œ μ›Œλ°
  async warmPopularProducts() {
    const popularProducts = await this.getPuri("r")
      .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 BaseModelClass {
  // λͺ©λ‘ API - 10λΆ„ 캐싱
  @cache({ ttl: "10m" })
  @api({ httpMethod: "GET" })
  async listPosts(page: number = 1) {
    return this.getPuri("r")
      .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("r")
            .table("users")
            .select({ count: Puri.count() })
            .then((r) => r[0].count),
          UserModel.getPuri("r")
            .table("users")
            .select({ count: Puri.count() })
            .where({ status: "active" })
            .then((r) => r[0].count),
          OrderModel.getPuri("r")
            .table("orders")
            .select({ count: Puri.count() })
            .then((r) => r[0].count),
          OrderModel.getPuri("r")
            .table("orders")
            .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("r");
}

2. Event-Based

class ProductModelClass extends BaseModelClass {
  @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 μ„€μ • (λ¬Έμžμ—΄ ν˜•μ‹)
  • λͺ…ν™•ν•œ μΊμ‹œ ν‚€ 넀이밍
  • 데이터 λ³€κ²½ μ‹œ μΊμ‹œ λ¬΄νš¨ν™”
  • λ©”λͺ¨λ¦¬ μ‚¬μš©λŸ‰ λͺ¨λ‹ˆν„°λ§
  • μΊμ‹œ μ›Œλ° μ „λž΅
  • μ‹€νŒ¨ λŒ€μ‘ 둜직

κ΄€λ ¨ λ¬Έμ„œ