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");
}
}
'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μ΄
체ν¬λ¦¬μ€νΈ
ν¨κ³Όμ μΈ μΊμ±μ μν΄:- μμ£Ό μ‘°νλλ λ°μ΄ν° νμ
- μ μ ν TTL μ€μ (λ¬Έμμ΄ νμ)
- λͺ νν μΊμ ν€ λ€μ΄λ°
- λ°μ΄ν° λ³κ²½ μ μΊμ 무ν¨ν
- λ©λͺ¨λ¦¬ μ¬μ©λ λͺ¨λν°λ§
- μΊμ μλ° μ λ΅
- μ€ν¨ λμ λ‘μ§