@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 { bentostore } from "bentocache";
import { memoryDriver } from "bentocache/drivers/memory";
import { redisDriver } from "bentocache/drivers/redis";
import { defineConfig } from "sonamu";
export default defineConfig({
cache: {
default: "multi", // ๊ธฐ๋ณธ ์คํ ์ด
stores: {
// ๋ฉ๋ชจ๋ฆฌ ์ ์ฉ
memory: bentostore()
.useL1Layer(memoryDriver({ maxItems: 1000 })),
// Redis ์ ์ฉ
redis: bentostore()
.useL1Layer(redisDriver({
connection: { host: "127.0.0.1", port: 6379 }
})),
// Multi-tier (๋ฉ๋ชจ๋ฆฌ + Redis)
multi: bentostore()
.useL1Layer(memoryDriver({ maxItems: 1000 }))
.useL2Layer(redisDriver({
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"]);
gracePeriod
์บ์๊ฐ ๋ง๋ฃ๋ ํ์๋ ์ผ์ ์๊ฐ ๋์ ์ด์ ๊ฐ์ ๋ฐํํ๋ ์ ์ ๊ธฐ๊ฐ์
๋๋ค.
@cache({
ttl: "10m",
gracePeriod: { enabled: true, duration: "5m" }
})
async getData() {
// TTL ๋ง๋ฃ ํ 5๋ถ๊ฐ์ ๊ธฐ์กด ์บ์ ๋ฐํ
// ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์๋ก์ด ๊ฐ ๊ฐฑ์
}
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 };
}
}
๋ค์ ๋จ๊ณ