๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
Sonamu๋Š” BentoCache๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ฐ•๋ ฅํ•œ ์บ์‹ฑ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ฐ€์ด๋“œ์—์„œ๋Š” sonamu.config.ts์—์„œ ์บ์‹œ๋ฅผ ์„ค์ •ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ด…๋‹ˆ๋‹ค.

BentoCache๋ž€?

BentoCache๋Š” Multi-tier ์บ์‹ฑ์„ ์ง€์›ํ•˜๋Š” TypeScript ์บ์‹œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ž…๋‹ˆ๋‹ค. ์ฃผ์š” ํŠน์ง•:
  • L1/L2 ๋ ˆ์ด์–ด: ๋ฉ”๋ชจ๋ฆฌ(๋น ๋ฆ„) + ์˜๊ตฌ ์ €์žฅ์†Œ(๋А๋ฆผํ•˜์ง€๋งŒ ๊ณต์œ  ๊ฐ€๋Šฅ)
  • ๋‹ค์–‘ํ•œ ๋“œ๋ผ์ด๋ฒ„: Memory, Redis, File, Knex ์ง€์›
  • Bus ์‹œ์Šคํ…œ: ๋ถ„์‚ฐ ์บ์‹œ ๋ฌดํšจํ™”
  • Tag ๊ธฐ๋ฐ˜ ๋ฌดํšจํ™”: ์—ฌ๋Ÿฌ ์บ์‹œ๋ฅผ ๊ทธ๋ฃน์œผ๋กœ ๊ด€๋ฆฌ
  • TTL & Grace Period: ๋งŒ๋ฃŒ ๋ฐ Stale-While-Revalidate

๊ธฐ๋ณธ ์„ค์ •

sonamu.config.ts

sonamu.config.ts์˜ server.cache ํ•„๋“œ์— ์บ์‹œ ์„ค์ •์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค:
import { drivers, store, type SonamuConfig } from "sonamu";

export const config: SonamuConfig = {
  // ... ๊ธฐํƒ€ ์„ค์ •
  server: {
    cache: {
      default: "main",
      stores: {
        main: store()
          .useL1Layer(drivers.memory({ maxSize: "100mb" }))
      },
    },
  },
};
ํ•„์ˆ˜ ํ•„๋“œ:
  • default: ๊ธฐ๋ณธ์œผ๋กœ ์‚ฌ์šฉํ•  ์Šคํ† ์–ด ์ด๋ฆ„
  • stores: ์Šคํ† ์–ด ์„ค์ • ๊ฐ์ฒด (์ตœ์†Œ 1๊ฐœ ์ด์ƒ ํ•„์š”)

๋“œ๋ผ์ด๋ฒ„ ์ข…๋ฅ˜

Sonamu๋Š” 5๊ฐ€์ง€ ๋“œ๋ผ์ด๋ฒ„๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค:

memory

๋ฉ”๋ชจ๋ฆฌ ๊ธฐ๋ฐ˜ ์บ์‹œ (๋น ๋ฅด์ง€๋งŒ ํ”„๋กœ์„ธ์Šค ์žฌ์‹œ์ž‘ ์‹œ ์‚ญ์ œ)

redis

Redis ๊ธฐ๋ฐ˜ ์บ์‹œ (์—ฌ๋Ÿฌ ํ”„๋กœ์„ธ์Šค ๊ฐ„ ๊ณต์œ  ๊ฐ€๋Šฅ)

file

ํŒŒ์ผ ์‹œ์Šคํ…œ ๊ธฐ๋ฐ˜ ์บ์‹œ (์˜๊ตฌ ์ €์žฅ)

knex

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ธฐ๋ฐ˜ ์บ์‹œ (๊ธฐ์กด DB ํ™œ์šฉ)

redisBus

๋ถ„์‚ฐ ์บ์‹œ ๋ฌดํšจํ™” ๋ฒ„์Šค (์—ฌ๋Ÿฌ ์„œ๋ฒ„ ๊ฐ„ ๋™๊ธฐํ™”)

Import ๋ฐฉ๋ฒ•

// 1. drivers ๊ฐ์ฒด ์‚ฌ์šฉ (๊ถŒ์žฅ)
import { drivers } from "sonamu/cache";
drivers.memory({ maxSize: "100mb" });
drivers.redis({ connection: {...} });

// 2. ๊ฐœ๋ณ„ import
import { memoryDriver, redisDriver } from "sonamu/cache";
memoryDriver({ maxSize: "100mb" });
redisDriver({ connection: {...} });

Store ๊ตฌ์„ฑ

L1 ๋ ˆ์ด์–ด (๋ฉ”๋ชจ๋ฆฌ ์บ์‹œ)

L1์€ ๋กœ์ปฌ ๋ฉ”๋ชจ๋ฆฌ์— ์ €์žฅ๋˜์–ด ๊ฐ€์žฅ ๋น ๋ฆ…๋‹ˆ๋‹ค.
store().useL1Layer(drivers.memory({ 
  maxSize: "100mb",  // ์ตœ๋Œ€ ํฌ๊ธฐ
  maxItems: 1000,    // ์ตœ๋Œ€ ํ•ญ๋ชฉ ์ˆ˜
}))
ํŠน์ง•:
  • ํ”„๋กœ์„ธ์Šค ๋‚ด์—์„œ๋งŒ ์œ ํšจ
  • ์„œ๋ฒ„ ์žฌ์‹œ์ž‘ ์‹œ ์‚ญ์ œ๋จ
  • ๋„คํŠธ์›Œํฌ I/O ์—†์Œ (๊ฐ€์žฅ ๋น ๋ฆ„)

L2 ๋ ˆ์ด์–ด (์˜๊ตฌ ์ €์žฅ์†Œ)

L2๋Š” ์—ฌ๋Ÿฌ ํ”„๋กœ์„ธ์Šค/์„œ๋ฒ„ ๊ฐ„ ๊ณต์œ  ๊ฐ€๋Šฅํ•œ ์ €์žฅ์†Œ์ž…๋‹ˆ๋‹ค.
import { drivers, store } from "sonamu/cache";
import Redis from "ioredis";

const redis = new Redis({
  host: "localhost",
  port: 6379,
});

store()
  .useL1Layer(drivers.memory({ maxSize: "100mb" }))
  .useL2Layer(drivers.redis({ connection: redis }))
Redis ์žฅ์ :
  • ์—ฌ๋Ÿฌ ์„œ๋ฒ„ ๊ฐ„ ์บ์‹œ ๊ณต์œ 
  • ์˜๊ตฌ ์ €์žฅ (์žฌ์‹œ์ž‘ ํ›„์—๋„ ์œ ์ง€)
  • ๋น ๋ฅธ ๋„คํŠธ์›Œํฌ ์•ก์„ธ์Šค

Bus ๋ ˆ์ด์–ด (๋ถ„์‚ฐ ๋ฌดํšจํ™”)

์—ฌ๋Ÿฌ ์„œ๋ฒ„๊ฐ€ ์žˆ์„ ๋•Œ, ํ•œ ์„œ๋ฒ„์—์„œ ์บ์‹œ๋ฅผ ์‚ญ์ œํ•˜๋ฉด ๋‹ค๋ฅธ ์„œ๋ฒ„์—๋„ ์•Œ๋ฆฝ๋‹ˆ๋‹ค.
import Redis from "ioredis";

const redis = new Redis({ host: "localhost", port: 6379 });

store()
  .useL1Layer(drivers.memory({ maxSize: "100mb" }))
  .useL2Layer(drivers.redis({ connection: redis }))
  .useBus(drivers.redisBus({ connection: redis }))
Bus ์—†์ด:
Server 1: deleteByTag("product") โ†’ L1/L2 ์‚ญ์ œ
Server 2: ์—ฌ์ „ํžˆ ์˜ค๋ž˜๋œ ์บ์‹œ ์‚ฌ์šฉ โŒ
Bus ์‚ฌ์šฉ:
Server 1: deleteByTag("product") โ†’ L1/L2 ์‚ญ์ œ + Bus ์•Œ๋ฆผ
Server 2: Bus ๋ฉ”์‹œ์ง€ ์ˆ˜์‹  โ†’ L1 ์‚ญ์ œ โœ…

๋ฉ€ํ‹ฐ ์Šคํ† ์–ด ์„ค์ •

์šฉ๋„์— ๋”ฐ๋ผ ์—ฌ๋Ÿฌ ์Šคํ† ์–ด๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:
export const config: SonamuConfig = {
  server: {
    cache: {
      default: "api",  // ๊ธฐ๋ณธ ์Šคํ† ์–ด
      stores: {
        // API ์‘๋‹ต ์บ์‹ฑ (์งง์€ TTL, ๋ฉ”๋ชจ๋ฆฌ๋งŒ)
        api: store()
          .useL1Layer(drivers.memory({ maxSize: "200mb" })),
        
        // ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ ์บ์‹ฑ (๊ธด TTL, Redis ๊ณต์œ )
        database: store()
          .useL1Layer(drivers.memory({ maxSize: "100mb" }))
          .useL2Layer(drivers.redis({ connection: redis }))
          .useBus(drivers.redisBus({ connection: redis })),
        
        // ์ •์  ์„ค์ • ์บ์‹ฑ (์˜๊ตฌ, ํŒŒ์ผ ์ €์žฅ)
        config: store()
          .useL1Layer(drivers.memory({ maxItems: 100 }))
          .useL2Layer(drivers.file({ directory: ".config-cache" })),
      },
    },
  },
};

์Šคํ† ์–ด ์‚ฌ์šฉํ•˜๊ธฐ

// ๊ธฐ๋ณธ ์Šคํ† ์–ด ์‚ฌ์šฉ
await Sonamu.cache.set({ key: "user:1", value: {...} });

// ํŠน์ • ์Šคํ† ์–ด ์‚ฌ์šฉ
await Sonamu.cache.use("database").set({ key: "query:1", value: {...} });
await Sonamu.cache.use("config").set({ key: "settings", value: {...} });
๋ฐ์ฝ”๋ ˆ์ดํ„ฐ์—์„œ๋„ ์Šคํ† ์–ด ์ง€์ • ๊ฐ€๋Šฅ:
class UserModelClass extends BaseModel {
  // database ์Šคํ† ์–ด ์‚ฌ์šฉ
  @cache({ store: "database", ttl: "1h" })
  @api()
  async findById(id: number) {
    return this.findOne(['id', id]);
  }
  
  // config ์Šคํ† ์–ด ์‚ฌ์šฉ
  @cache({ store: "config", ttl: "forever" })
  async getSettings() {
    return this.findOne(['key', 'settings']);
  }
}

์‹ค์ „ ์˜ˆ์ œ

1. ๋‹จ์ผ ์„œ๋ฒ„ (๋ฉ”๋ชจ๋ฆฌ๋งŒ)

export const config: SonamuConfig = {
  server: {
    cache: {
      default: "main",
      stores: {
        main: store().useL1Layer(
          drivers.memory({ 
            maxSize: "500mb",
            maxItems: 10000,
          })
        ),
      },
    },
  },
};
์ ํ•ฉํ•œ ๊ฒฝ์šฐ:
  • ๋‹จ์ผ ์„œ๋ฒ„ ์šด์˜
  • ์บ์‹œ ์žฌ์ƒ์„ฑ์ด ๋น ๋ฆ„
  • ์„œ๋ฒ„ ์žฌ์‹œ์ž‘์ด ๋“œ๋ฌผ์Œ

2. ๋‹ค์ค‘ ์„œ๋ฒ„ (Redis ๊ณต์œ )

import Redis from "ioredis";

const redis = new Redis({
  host: process.env.REDIS_HOST ?? "localhost",
  port: Number(process.env.REDIS_PORT) ?? 6379,
  password: process.env.REDIS_PASSWORD,
});

export const config: SonamuConfig = {
  server: {
    cache: {
      default: "main",
      stores: {
        main: store()
          .useL1Layer(drivers.memory({ maxSize: "200mb" }))
          .useL2Layer(drivers.redis({ connection: redis }))
          .useBus(drivers.redisBus({ connection: redis })),
      },
    },
  },
};
์ ํ•ฉํ•œ ๊ฒฝ์šฐ:
  • ๋กœ๋“œ ๋ฐธ๋Ÿฐ์„œ + ์—ฌ๋Ÿฌ ์„œ๋ฒ„
  • ์„œ๋ฒ„ ๊ฐ„ ์บ์‹œ ๊ณต์œ  ํ•„์š”
  • ์„œ๋ฒ„ ์žฌ์‹œ์ž‘ ํ›„์—๋„ ์บ์‹œ ์œ ์ง€

3. ๊ณ„์ธต๋ณ„ ์„ค์ • (์„ฑ๋Šฅ ์ตœ์ ํ™”)

import Redis from "ioredis";

const redis = new Redis({ /* ... */ });

export const config: SonamuConfig = {
  server: {
    cache: {
      default: "main",
      stores: {
        // ๋น ๋ฅธ ์กฐํšŒ (API ์‘๋‹ต)
        main: store()
          .useL1Layer(drivers.memory({ maxSize: "300mb" })),
        
        // ๊ณต์œ  ํ•„์š” (์‚ฌ์šฉ์ž ์„ธ์…˜)
        shared: store()
          .useL1Layer(drivers.memory({ maxSize: "100mb" }))
          .useL2Layer(drivers.redis({ connection: redis }))
          .useBus(drivers.redisBus({ connection: redis })),
        
        // ์˜๊ตฌ ์ €์žฅ (์„ค์ •, ํ…œํ”Œ๋ฆฟ)
        persistent: store()
          .useL1Layer(drivers.memory({ maxItems: 100 }))
          .useL2Layer(drivers.file({ directory: ".persistent-cache" })),
      },
    },
  },
};

๋“œ๋ผ์ด๋ฒ„ ์˜ต์…˜ ์ƒ์„ธ

Memory Driver

drivers.memory({
  maxSize: "100mb",     // ์ตœ๋Œ€ ํฌ๊ธฐ (๋ฌธ์ž์—ด ๋˜๋Š” ๋ฐ”์ดํŠธ ์ˆ˜)
  maxItems: 1000,       // ์ตœ๋Œ€ ํ•ญ๋ชฉ ์ˆ˜
  maxItemSize: "10mb",  // ํ•ญ๋ชฉ๋‹น ์ตœ๋Œ€ ํฌ๊ธฐ
})
ํฌ๊ธฐ ๋‹จ์œ„: "10kb", "5mb", "1gb" ๋˜๋Š” ๋ฐ”์ดํŠธ ์ˆ˜(1024)

Redis Driver

import Redis from "ioredis";

const redis = new Redis({
  host: "localhost",
  port: 6379,
  password: "secret",
  db: 0,
});

drivers.redis({
  connection: redis,
  keyPrefix: "cache:",  // ํ‚ค ์ ‘๋‘์‚ฌ (์„ ํƒ)
})

File Driver

drivers.file({
  directory: ".cache",  // ์ €์žฅ ๋””๋ ‰ํ† ๋ฆฌ
  prefix: "sonamu",     // ํŒŒ์ผ๋ช… ์ ‘๋‘์‚ฌ (์„ ํƒ)
})

Knex Driver

import { DB } from "sonamu";

drivers.knex({
  connection: DB.getDB("w"),
  tableName: "cache_items",  // ํ…Œ์ด๋ธ”๋ช… (๊ธฐ๋ณธ๊ฐ’)
})
ํ…Œ์ด๋ธ” ์Šคํ‚ค๋งˆ (์ž๋™ ์ƒ์„ฑ):
CREATE TABLE cache_items (
  key TEXT PRIMARY KEY,
  value TEXT,
  expires_at INTEGER
);

Redis Bus Driver

drivers.redisBus({
  connection: redis,
  channelPrefix: "cache:",  // ์ฑ„๋„ ์ ‘๋‘์‚ฌ (์„ ํƒ)
})

ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ

ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ๋Š” ์ž๋™์œผ๋กœ ๋ฉ”๋ชจ๋ฆฌ ๋“œ๋ผ์ด๋ฒ„๊ฐ€ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค:
// ํ…Œ์ŠคํŠธ์—์„œ๋Š” ๋ณ„๋„ ์„ค์ • ๋ถˆํ•„์š”
import { Sonamu } from "sonamu";

test("์บ์‹œ ํ…Œ์ŠคํŠธ", async () => {
  // ์ž๋™์œผ๋กœ ๋ฉ”๋ชจ๋ฆฌ ์บ์‹œ ์‚ฌ์šฉ
  await Sonamu.cache.set({ key: "test", value: "value" });
  
  const result = await Sonamu.cache.get({ key: "test" });
  expect(result).toBe("value");
});
๋‚ด๋ถ€ ๊ตฌํ˜„:
// sonamu/src/api/sonamu.ts
private async initializeCache(config: CacheConfig | undefined, forTesting: boolean) {
  if (forTesting) {
    const { createTestCacheManager } = await import("../cache/cache-manager");
    this._cache = createTestCacheManager();  // ๋ฉ”๋ชจ๋ฆฌ ๋“œ๋ผ์ด๋ฒ„
    return;
  }
  // ...
}

์ฃผ์˜์‚ฌํ•ญ

์บ์‹œ ์„ค์ • ์‹œ ์ฃผ์˜์‚ฌํ•ญ:
  1. ๋ฉ”๋ชจ๋ฆฌ ์ œํ•œ: maxSize๋ฅผ ๋„ˆ๋ฌด ํฌ๊ฒŒ ์„ค์ •ํ•˜๋ฉด OOM(Out of Memory) ๋ฐœ์ƒ ๊ฐ€๋Šฅ
    // โŒ ์ž˜๋ชป๋œ ์˜ˆ
    drivers.memory({ maxSize: "10gb" })  // ์„œ๋ฒ„ ๋ฉ”๋ชจ๋ฆฌ๋ณด๋‹ค ํผ
    
    // โœ… ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ
    drivers.memory({ maxSize: "500mb" })  // ์ ์ ˆํ•œ ํฌ๊ธฐ
    
  2. Redis ์—ฐ๊ฒฐ ๊ณต์œ : Redis ์ธ์Šคํ„ด์Šค๋Š” driver์™€ bus์—์„œ ๊ฐ™์€ ์ธ์Šคํ„ด์Šค ์‚ฌ์šฉ
    // โœ… ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ
    const redis = new Redis({...});
    store()
      .useL2Layer(drivers.redis({ connection: redis }))
      .useBus(drivers.redisBus({ connection: redis }))
    
  3. ์Šคํ† ์–ด ์ด๋ฆ„ ์ผ์น˜: ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ์˜ store ์˜ต์…˜์€ ์„ค์ •์— ์ •์˜๋œ ์ด๋ฆ„๊ณผ ์ผ์น˜ํ•ด์•ผ ํ•จ
    // sonamu.config.ts
    stores: {
      myStore: store()...
    }
    
    // โŒ ์ž˜๋ชป๋œ ์˜ˆ
    @cache({ store: "wrongName" })  // ์—๋Ÿฌ ๋ฐœ์ƒ
    
    // โœ… ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ
    @cache({ store: "myStore" })
    
  4. Bus ์—†์ด L2๋งŒ ์‚ฌ์šฉ: ์—ฌ๋Ÿฌ ์„œ๋ฒ„์—์„œ L2(Redis)๋งŒ ์‚ฌ์šฉํ•˜๋ฉด L1์ด ๋™๊ธฐํ™”๋˜์ง€ ์•Š์Œ
    // โŒ ์œ„ํ—˜: ์„œ๋ฒ„ ๊ฐ„ L1 ๋ถˆ์ผ์น˜ ๊ฐ€๋Šฅ
    store()
      .useL1Layer(drivers.memory({ maxSize: "100mb" }))
      .useL2Layer(drivers.redis({ connection: redis }))
    // Bus ์—†์Œ โ†’ ๋ฌดํšจํ™” ์‹œ ๋‹ค๋ฅธ ์„œ๋ฒ„์˜ L1์€ ๊ทธ๋Œ€๋กœ
    
    // โœ… ์•ˆ์ „: Bus๋กœ L1 ๋™๊ธฐํ™”
    store()
      .useL1Layer(drivers.memory({ maxSize: "100mb" }))
      .useL2Layer(drivers.redis({ connection: redis }))
      .useBus(drivers.redisBus({ connection: redis }))
    

๋‹ค์Œ ๋‹จ๊ณ„