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 BaseModel {
// 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 BaseModel {
// 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 BaseModel {
// ํ๋ผ๋ฏธํฐ๋ณ๋ก ๋ค๋ฅธ ์บ์ ์์ฑ
@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 BaseModel {
@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 BaseModel {
@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 BaseModel {
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 BaseModel {
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 BaseModel {
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 BaseModel {
// ์๋ฒ ์์ ์ ์ธ๊ธฐ ์ํ ์บ์ ์๋ฐ
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 BaseModel {
// ๋ชฉ๋ก 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 BaseModel {
@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 ์ค์ (๋ฌธ์์ด ํ์)
- ๋ช ํํ ์บ์ ํค ๋ค์ด๋ฐ
- ๋ฐ์ดํฐ ๋ณ๊ฒฝ ์ ์บ์ ๋ฌดํจํ
- ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ ๋ชจ๋ํฐ๋ง
- ์บ์ ์๋ฐ ์ ๋ต
- ์คํจ ๋์ ๋ก์ง