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().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().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");
}
}
'5s': 5์ด'5m': 5๋ถ'1h': 1์๊ฐ'1d': 1์ผ0: ์๊ตฌ (๋ฌดํจํ ํ์)
ํ๋ผ๋ฏธํฐ ๊ธฐ๋ฐ ์บ์ฑ
class UserModelClass extends BaseModelClass {
// ํ๋ผ๋ฏธํฐ๋ณ๋ก ๋ค๋ฅธ ์บ์ ์์ฑ
@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 BaseModelClass {
@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 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()
.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()
.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 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 ์ค์ (๋ฌธ์์ด ํ์)
- ๋ช ํํ ์บ์ ํค ๋ค์ด๋ฐ
- ๋ฐ์ดํฐ ๋ณ๊ฒฝ ์ ์บ์ ๋ฌดํจํ
- ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ ๋ชจ๋ํฐ๋ง
- ์บ์ ์๋ฐ ์ ๋ต
- ์คํจ ๋์ ๋ก์ง