@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 μ¬μ© μ μλ¬κ° λ°μν©λλ€.
μΊμ μ ν¨ μκ°μ μ§μ ν©λλ€.
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"
μΊμ ν€λ₯Ό μ§μ ν©λλ€.
κΈ°λ³Έκ°: "{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]"
}
@cache({ ttl: "1h", key: "products:featured" })
async getFeatured() {
// μΊμ ν€: "products:featured"
// μΈμκ° μμΌλ―λ‘ νμ κ°μ ν€
}
@cache({ ttl: "10m", key: "user:profile" })
async getProfile(userId: number) {
// μΊμ ν€: "user:profile:123"
// μΈμκ° μλμΌλ‘ suffixλ‘ μΆκ°λ¨
}
@cache({
ttl: "10m",
key: (subset: string, id: number) => `product:${subset}:${id}`
})
async findById(subset: string, id: number) {
// μΊμ ν€: "product:A:123"
}
@cache({
ttl: "1h",
key: (filters: FilterParams) => {
const sorted = Object.keys(filters).sort();
return `search:${sorted.map(k => `${k}=${filters[k]}`).join(":")}`;
}
})
async search(filters: FilterParams) {
// μΊμ ν€: "search:category=electronics:price=100"
}
μ¬μ©ν μΊμ μ€ν μ΄λ₯Ό μ§μ ν©λλ€.
κΈ°λ³Έκ°: sonamu.config.tsμ default μ€ν μ΄
@cache({ ttl: "1h", store: "redis" })
async getSharedData() {
// Redis μ€ν μ΄ μ¬μ©
}
@cache({ ttl: "5m", store: "memory" })
async getLocalData() {
// λ©λͺ¨λ¦¬ μ€ν μ΄ μ¬μ©
}
μΊμ νκ·Έλ₯Ό μ§μ ν©λλ€. νκ·Έλ³λ‘ μΊμλ₯Ό μΌκ΄ 무ν¨νν λ μ¬μ©ν©λλ€.
@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, 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}`);
},
},
}),
},
},
});
μμ λͺ¨μ
API μλ΅ μΊμ±
μ¬μ©μ νλ‘ν
μ§κ³ λ°μ΄ν°
κ²μ κ²°κ³Ό
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;
}
}
class UserModelClass extends BaseModelClass {
@api({ httpMethod: "GET" })
@cache({
ttl: "10m",
key: (id: number) => `user:profile:${id}`,
tags: ["users"]
})
async getProfile(id: number) {
const rdb = this.getPuri("r");
const user = await rdb.table("users")
.where("id", id)
.first();
const posts = await rdb.table("posts")
.where("user_id", id)
.limit(5)
.select();
return { user, posts };
}
@api({ httpMethod: "PUT" })
@transactional()
async updateProfile(id: number, data: UserUpdateParams) {
const wdb = this.getDB("w");
await this.upsert(wdb, { id, ...data });
// ν΄λΉ μ¬μ©μ μΊμλ§ λ¬΄ν¨ν
await Sonamu.cache.delete(`user:profile:${id}`);
return this.getProfile(id);
}
}
class AnalyticsModelClass extends BaseModelClass {
@api({ httpMethod: "GET" })
@cache({ ttl: "1h", key: "analytics:dashboard" })
async getDashboard() {
const rdb = this.getPuri("r");
const [users, orders, revenue] = await Promise.all([
rdb.table("users").count("* as count"),
rdb.table("orders").count("* as count"),
rdb.table("orders").sum("total_amount as total")
]);
return {
totalUsers: users[0].count,
totalOrders: orders[0].count,
totalRevenue: revenue[0].total
};
}
@api({ httpMethod: "GET" })
@cache({
ttl: "1d",
key: (date: string) => `analytics:daily:${date}`
})
async getDailyStats(date: string) {
const rdb = this.getPuri("r");
return rdb.table("orders")
.whereRaw("DATE(created_at) = ?", [date])
.select(
rdb.raw("COUNT(*) as order_count"),
rdb.raw("SUM(total_amount) as revenue"),
rdb.raw("AVG(total_amount) as avg_order_value")
)
.first();
}
}
class SearchModelClass extends BaseModelClass {
@api({ httpMethod: "GET" })
@cache({
ttl: "15m",
key: (query: string, filters: SearchFilters) => {
const filterStr = Object.entries(filters)
.sort(([a], [b]) => a.localeCompare(b))
.map(([k, v]) => `${k}:${v}`)
.join(",");
return `search:${query}:${filterStr}`;
},
tags: ["search"]
})
async search(query: string, filters: SearchFilters) {
const rdb = this.getPuri("r");
let qb = rdb.table("products")
.where("name", "like", `%${query}%`);
if (filters.category) {
qb = qb.where("category", filters.category);
}
if (filters.minPrice) {
qb = qb.where("price", ">=", filters.minPrice);
}
if (filters.maxPrice) {
qb = qb.where("price", "<=", filters.maxPrice);
}
return qb.limit(20).select();
}
@api({ httpMethod: "DELETE" })
async clearSearchCache() {
// κ²μ μΊμ μ 체 무ν¨ν
await Sonamu.cache.deleteByTags(["search"]);
return { success: true };
}
}
λ€μ λ¨κ³
@api
API μλν¬μΈνΈ λ§λ€κΈ°
@transactional
νΈλμμ
μ¬μ©νκΈ°
BentoCache
BentoCache 곡μ λ¬Έμ
μ±λ₯ μ΅μ ν
μ±λ₯ μ΅μ ν κ°μ΄λ