๐ ๊ฐ์
๋ฐ๋ธ์ฝ์ค 4๊ธฐ์์ ๋ด์ฌ ํ๋ซํผ์ธ Anifriends ์์ ๋ณดํธ ๋๋ฌผ ์กฐํ ์ฑ๋ฅ์ ๋์ด๊ธฐ ์ํด ์ฒซ ํ์ด์ง์ ์ด ๋ฐ์ดํฐ ๊ฐ์๋ฅผ ์บ์ํ์๋ค. ๊ฐ๋ฐํ ์ง 3๊ฐ์ ์ง๋ฌ์ง๋ง, Redis์ ๋์์ฑ์ ๋ํด ๊ณต๋ถ๋ฅผ ๋ ํ๊ณ ๋ค์ ์ฝ๋๋ฅผ ๋ณด๋ ๋์์ฑ ๋ฌธ์ ๊ฐ ์์ ๋งํ ์ฝ๋๊ฐ ๋ณด์ฌ์ ์์ ์ ํ๋ ค๊ณ ํ๋ค.
์บ์ ์๋ฃ ๊ตฌ์กฐ
- ๋ณดํธ ๋๋ฌผ ์ฒซ ํ์ด์ง: ๋ณดํธ ๋๋ฌผ ๋ชฉ๋ก์ ์์ฑ ์๊ฐ ๊ธฐ์ค ๋ด๋ฆผ ์ฐจ์์ด๊ณ , Redis ์ ZSet(Sorted Sets)์ ์ฌ์ฉํ์๋ค. ZSet ์ score ๊ธฐ์ค ์ค๋ฆ์ฐจ์์ผ๋ก ์ ๋ ฌํ๋ฏ๋ก, score ์ -created_at ๋ก ์ค์ ํ์ฌ ์์ฑ ์๊ฐ ๊ธฐ์ค์ผ๋ก ๋ด๋ฆผ ์ฐจ์์ผ๋ก ์ ๋ ฌ๋ ๋ฐ์ดํฐ๋ฅผ ์บ์ํ์๋ค.
- ๋ณดํธ ๋๋ฌผ ์ด ์: Redis ์ Strings๋ฅผ ์ฌ์ฉํ์๋ค.
์บ์ ์ ๋ต
์ฝ๊ธฐ ์ ๋ต: Look-Aside
- ์บ์์ ๋ฐ์ดํฐ๊ฐ ์กด์ฌํ๋ฉด ํด๋น ๋ฐ์ดํฐ๋ฅผ ์๋ต
- ์บ์์ ๋ฐ์ดํฐ๊ฐ ์กด์ฌํ์ง ์์ผ๋ฉด DB์์ ๋ฐ์ดํฐ๋ฅผ ์กฐํํ๊ณ ์บ์ฑ ํ ์๋ต
- Cache Warming ์ ํตํด ์ดํ๋ฆฌ์ผ์ด์ ์ด ๋์์ง ๋ ์ฒซ ํ์ด์ง ์บ์
์ฐ๊ธฐ ์ ๋ต: Write Through
- ์์ฑ: ์๋ก์ด ๋ฐ์ดํฐ ์บ์, ์ด ๋ฐ์ดํฐ ๊ฐ์ ์ฆ๊ฐ
- ์์ : ์บ์ ๋์์ผ ๊ฒฝ์ฐ ํด๋น ๋ฐ์ดํฐ ์ญ์ ํ ์ฌ์บ์
- ์ญ์ : ์บ์ ๋์์ผ ๊ฒฝ์ฐ ํด๋น ๋ฐ์ดํฐ ์ญ์ , ์ด ๋ฐ์ดํฐ ๊ฐ์ ๊ฐ์
๐จ ๋ฌธ์ 1. ์บ์ ํฌ๊ธฐ๋ฅผ ์ ์งํ๊ธฐ ์ํ ๋ก์ง์์ ๋์์ฑ ๋ฌธ์ ๋ฐ์
์๋ก์ด ๋ฐ์ดํฐ๊ฐ ์ถ๊ฐ๋ ๊ฒฝ์ฐ, ์บ์์ ํฌ๊ธฐ๋ฅผ ์กฐํํ์ฌ ์ต๋ ํฌ๊ธฐ๋ฅผ ์ด๊ณผํ๋ค๋ฉด ๋ค์๊ณผ ๊ฐ์ด ZSet ์ ZREMRANGEBYSCORE ์ปค๋งจ๋๋ฅผ ์ฌ์ฉํ์ฌ ๋ง์ง๋ง ๋ฐ์ดํฐ๋ฅผ ์ญ์ ํ๋ค.
private void trimCache() {
if (zSetOperations.size(ANIMAL_ZSET_KEY) > ANIMAL_CACHE_SIZE) {
zSetOperations.removeRange(ANIMAL_ZSET_KEY, LAST_INDEX, LAST_INDEX);
}
}
์บ์์ ์ต๋ ํฌ๊ธฐ๊ฐ 30์ด๊ณ ๋์์ 50๊ฐ์ ๋ณดํธ ๋๋ฌผ ์์ฑ ์์ฒญ์ด ๋ฐ์ํ๋ค๊ณ ๊ฐ์ ํ์๋ค. ์ ์์ ์ธ ๋ก์ง์์๋ 30๊ฐ์ ๋ฐ์ดํฐ๊ฐ ์บ์ ๋์ด์ผ ํ๋ค.
@Test
@DisplayName("์ฑ๊ณต: ๋์์ ๋ณดํธ ๋๋ฌผ ์ถ๊ฐ")
void saveAnimal() throws InterruptedException {
// given
int count = 50;
int cacheSize = 30;
Shelter shelter = shelter();
List<Animal> animals = animals(shelter, count);
long id = 1;
for (Animal animal : animals) {
ReflectionTestUtils.setField(animal, "animalId", id++);
ReflectionTestUtils.setField(animal, "createdAt", LocalDateTime.now());
}
// when
ExecutorService executorService = Executors.newFixedThreadPool(count);
CountDownLatch latch = new CountDownLatch(count);
// when
for (int i = 0; i < count; i++) {
int finalI = i;
executorService.submit(() -> {
try {
animalRedisRepository.saveAnimal(animals.get(finalI));
} finally {
latch.countDown();
}
});
}
latch.await();
// then
assertThat(redisTemplate.opsForZSet().range("animal:animals", 0, -1))
.hasSize(cacheSize);
}

์ญ์๋ ๋ฐ์ดํฐ๊ฐ ์ ๋๋ก ์บ์ ๋์ง ์์๋ค. ์ฌ์ง์ด ๋จ ํ ๊ฐ์ ๋ฐ์ดํฐ๋ ์บ์๋์ง ์์๋ค. 50๊ฐ์ ํธ๋์ญ์ ์์ ์บ์์ ์ต๋ ํฌ๊ธฐ๋ฅผ ์ด๊ณผํ๋ค๊ณ ํ๋จํ๋ฉฐ ๋ง์ง๋ง ๋ฐ์ดํฐ๋ฅผ ์ญ์ ํ๊ธฐ ๋๋ฌธ์ด๋ค.

โ ํด๊ฒฐ ๋ฐฉ๋ฒ
ZSet ์ ZREMRANGEBYSCORE ์ปค๋งจ๋๋ฅผ ์ฌ์ฉํ์ฌ ๋ง์ง๋ง ๋ฐ์ดํฐ๊ฐ ์๋ ์บ์์ ์ต๋ ๊ฐ์๋ฅผ ์ด๊ณผํ ๋ฐ์ดํฐ๋ฅผ ์ญ์ ํ๋๋ก ์์ ํ์๋ค.
private void trimCache() {
zSetOperations.removeRange(ANIMAL_ZSET_KEY, ANIMAL_CACHE_SIZE, LAST_INDEX);
}

๐จ ๋ฌธ์ 2. ๋ฐ์ดํฐ ๊ฐ์ ์ฆ๊ฐ ๋ก์ง์์ ๊ฐฑ์ ์์ค ๋ฐ์
๋ฐ์ดํฐ ์ถ๊ฐ๋๊ฑฐ๋ ์ญ์ ๋ ๋ ์บ์๋ ๊ฐ์๋ฅผ ์กฐํํ์ฌ (ํ์ฌ ๊ฐ์ + 1)์ ํ์ฌ ์ ์ฅํ๋ค.
@Override
public void increaseTotalNumberOfAnimals() {
Long cachedCount = getTotalNumberOfAnimals();
valueOperations.set(TOTAL_NUMBER_OF_ANIMALS_KEY, cachedCount + 1);
}
๋์์ 10๊ฐ์ ๋ณดํธ ๋๋ฌผ ์์ฑ ์์ฒญ์ด ๋ฐ์ํ๋ค๊ณ ๊ฐ์ ํ์๋ค. ์ ์์ ์ธ ๋ก์ง์์๋ 10๊ฐ๋ก ์บ์ ๋์ด์ผ ํ๋ค.
@Test
@DisplayName("์ฑ๊ณต: ๋์์ ์ด ๋๋ฌผ ์ ์ฆ๊ฐ")
void increaseTotalNumberOfAnimals() throws InterruptedException {
// given
int count = 10;
// when
ExecutorService executorService = Executors.newFixedThreadPool(count);
CountDownLatch latch = new CountDownLatch(count);
// when
for (int i = 0; i < count; i++) {
executorService.submit(() -> {
try {
animalRedisRepository.increaseTotalNumberOfAnimals();
} finally {
latch.countDown();
}
});
}
latch.await();
// then
assertThat(animalRedisRepository.getTotalNumberOfAnimals()).isEqualTo(count);
}

๊ฒฐ๊ณผ๋ 10๊ฐ๊ฐ ์๋ 1๊ฐ์๋ค. ์ฌ๋ฌ ํธ๋์ญ์ ์์ ๋์ผํ ๊ฐ์๋ฅผ ์กฐํํ๊ณ ๊ฐฑ์ ํ๊ธฐ ๋๋ฌธ์ด๋ค.

โ ํด๊ฒฐ ๋ฐฉ๋ฒ
ZSet ์ INCR ์ปค๋งจ๋๋ฅผ ์ฌ์ฉํ์ฌ ์์์ ์ผ๋ก ๊ฐ์๊ฐ ์ฆ๊ฐ๋๋๋ก ์์ ํ์๋ค.
public void increaseTotalNumberOfAnimals() {
valueOperations.increment(TOTAL_NUMBER_OF_ANIMALS_KEY);
}

๊ฐ์ํ ๋๋ ๋์ผํ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๊ณ , DECR ์ปค๋งจ๋๋ฅผ ์ฌ์ฉํ์ฌ ํด๊ฒฐํ์๋ค.