Post

๐ŸŽฑ Redis๋ฅผ ๋„์ž…ํ•ด์„œ ์บ์‹ฑ ์ž‘์—…์„ ์ง„ํ–‰ํ•ด๋ณด์ž!!! [2ํƒ„]

๐ŸŽฑ Redis๋ฅผ ๋„์ž…ํ•ด์„œ ์บ์‹ฑ ์ž‘์—…์„ ์ง„ํ–‰ํ•ด๋ณด์ž!!! [2ํƒ„]

๐Ÿ–ค Intro

์˜ค๋Š˜ ๋ณผ ๋ถ€๋ถ„์€ REDIS ์บ์‹ฑ์„ ์„ค๊ณ„ํ•˜๋ฉด์„œ, ๊ณผ์—ฐ ์บ์‹ฑํ•ด์•ผ ํ•˜๋Š” ๊ฐ’๋“ค์„ โ€œ์–ด๋–คโ€ํ˜•ํƒœ๋กœ ์ €์žฅํ•˜๋Š”๊ฒŒ ์ข‹์„์ง€, ๊ทธ๋ฆฌ๊ณ  ๊ฐ๊ฐ์˜ ์ •๋ณด์— ๋Œ€ํ•œ TTL์„ ์–ด๋А์ •๋„๋กœ ์ง€์ •ํ•ด์•ผ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ํ™•๋Œ€์‹œํ‚ฌ ์ˆ˜ ์žˆ์„์ง€์— ๋Œ€ํ•œ ๊ณ ๋ฏผ์„ ๋‹ด๊ณ  ์žˆ๋‹ค!

๐Ÿฉถ Start

๐Ÿซจ ๊ณ ๋ฏผโ€ฆ REDIS์— ์–ด๋– ํ•œ ํ˜•ํƒœ๋กœ ๊ฐ’์„ ์ €์žฅํ•  ๊ฒƒ์ธ๊ฐ€?

์šฐ๋ฆฌ๋Š” ๊ฒฐ๊ตญ REDIS์— user์˜ ์ •๋ณด์— ํ•ด๋‹นํ•˜๋Š” ๊ฐ’๋“ค์„ ๋„ฃ์–ด๋‘๊ณ  ์ด๋ฅผ ์š”์ฒญ์— ๋„ฃ์–ด์„œ LLMํ•œํ…Œ ์ƒํ’ˆ ์ถ”์ฒœ์„ ๋ฐ›๋Š” ๊ฒƒ์ด ๋ชฉํ‘œ์ด๊ธฐ ๋•Œ๋ฌธ์—, ํ•ด๋‹น ๊ฐ’๋“ค์„ JSON์œผ๋กœ ๋งŒ๋“ค์–ด์„œ ์ €์žฅ ํ•ด๋‘๋Š” ๊ฒƒ์ด ์—ฌ๋Ÿฌ๋ชจ๋กœ ํŽธํ•˜๋‹ค.

์ด๋ ‡๊ฒŒ JSON ์ž์ฒด๋กœ ์ €์žฅํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” Config์— ์ปค์Šคํ…€์œผ๋กœ ์ง๋ ฌํ™” ์„ธํŒ…์„ ํ•ด๋‘๋Š” ๊ฒƒ์ด ์ข‹๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ JSON ์ง๋ ฌํ™”์˜ ๊ฒฝ์šฐ๋Š” ์น˜๋ช…์ ์ธ ๋‹จ์ ์ด ์žˆ๋‹ค. Spring Data Redis์—์„œ๋Š” ์ €์žฅ์˜ ๋‹จ์œ„๊ฐ€ JSON(๊ฐ์ฒด ๋‹จ์œ„)์ด๊ธฐ ๋•Œ๋ฌธ์— creditLeft ๊ฐ™์€ ํ•„๋“œ ํ•˜๋‚˜๋งŒ ๋ฐ”๊ฟ€ ๋•Œ๋„ ์ƒˆ๋กœ์šด DTO๋ฅผ ๋งŒ๋“ค์–ด์„œ set ํ•ด์•ผ ํ•œ๋‹ค.

์ฆ‰, ๋งŒ์•ฝ ํ•„๋“œ์—์„œ update๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ๊ฐ€ ์ƒ๊ธฐ๋ฉด ๊ฐ์ฒด๋ฅผ ๋‹ค์‹œ ๋งŒ๋“ค์–ด์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค. ๋‚ด๋ถ€์ ์œผ๋กœ๋Š” ๋ฌธ์ž์—ด ๋ฎ์–ด์“ฐ๊ธฐ ๋ฐ–์— ์—†๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค!

์—ฌ๊ธฐ์„œ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธด๋‹คโ€ฆ JSON์œผ๋กœ ์ €์žฅํ•˜๋ฉด ์šฐ๋ฆฌ๊ฐ€ ์š”์ฒญ ๊ฐ’์„ ๋„ฃ์„๋•Œ ํŽธ๋ฆฌํ•œ ๊ฒƒ์€ ์•Œ๊ฒ ๋Š”๋ฐ, ์‚ฌ์‹ค ์šฐ๋ฆฌ์˜ ๊ฒฝ์šฐ, โ€œ๊ด€์‹ฌ์‚ฌโ€ ๊ฐ’์„ ๋งค๋ฒˆ ๋ฌผ์–ด๋ณด๊ณ , ๊ฐฑ์‹ ํ•˜๋Š” ๋ฐฉ์‹์ด๋ผ ์ €์žฅ๋œ ๊ฐ์ฒด ์•ˆ์—์„œ ๋ณ€๋™์ด ์ƒ๊ธฐ๊ฒŒ ๋œ๋‹ค.

๊ทธ๋ž˜์„œ ๊ณ ๋ฏผ์„ ํ–ˆ๋Š”๋ฐ, ๋ณ€๋™์ด ๋งŽ์€ ๊ฐ’์€ Hash์— ๋‘๊ณ  ๋ฉ์–ด๋ฆฌ ์‘๋‹ต(JSON ์Šค๋ƒ…์ƒท)์€ JSON์œผ๋กœ ์ €์žฅํ•˜๋Š” ์‹์˜ ํ˜ผํ•ฉ ์ „๋žต์„ ์‚ฌ์šฉํ•˜๊ธฐ๋„ ํ•œ๋‹ค!!!

๋˜ ๋‹ค๋ฅธ ๊ณ ๋ฏผ๊ฑฐ๋ฆฌ,,, ๊ณผ์—ฐ ์บ์‹ฑ์˜ TTL์€ ์–ผ๋งˆ๋กœ ์žก์•„์•ผ ํ•˜๋Š”๊ฐ€? ๊ทธ๋ฆฌ๊ณ  DB๋ฅผ ๋œ ์ฐŒ๋ฅด๋ฉด์„œ๋„ ์ž”์•ก์€ ์‹ ์„ ํ•˜๊ฒŒ ๊ฐ€์ ธ๊ฐ€๋ ค๋ฉด ์–ด๋–ป๊ฒŒ ํ•ด์•ผํ•˜๋Š”๊ฐ€?

์—ฌ๊ธฐ์„œ ๋˜ ๋‹ค๋ฅธ ์ข…๋ฅ˜์˜ ๊ณ ๋ฏผ์ด ์ƒ๊ฒผ๋‹ค..

์›๋ž˜ ์ƒ๊ฐํ–ˆ๋˜ ๋ฐฉํ–ฅ์€, ์ดˆ๋ฐ˜์— ์ฑ„ํŒ…์„ ์‹œ์ž‘ํ•  ๋•Œ, ์šฐ์„  REDIS์— ์บ์‹ฑํ•ด๋‘” ์œ ์ € ์ •๋ณด๊ฐ€ ์žˆ์œผ๋ฉด DB๋ฅผ ๊ฑฐ์น˜์ง€ ์•Š๊ณ  ๋ฐ”๋กœ REDIS์—์„œ ๊ฐ’์„ ๊ฐ€์ ธ์˜จ๋‹ค.

์ทจ๋ฏธ(๊ด€์‹ฌ์‚ฌ)์˜ ๊ฒฝ์šฐ๋Š” ๋ฐ”๋€Œ์—ˆ๋‹ค๋ฉด ๋‹ค์Œ ์‘๋‹ต์—์„œ ๋ฐ”๋€ ๊ฐ’์œผ๋กœ DB์™€ REDIS๋ฅผ ์ „๋ถ€ ๊ฐฑ์‹ ํ•ด์ค€๋‹ค.

์ด๋ ‡๊ฒŒ ํ•  ๊ฒฝ์šฐ, ๋ณ€๋™์ด ํ™•์ •์ธ ๊ด€์‹ฌ์‚ฌ๋งŒ Hash๋กœ ์ €์žฅํ•˜๋ฉด ๋œ๋‹ค

๊ทธ๋Ÿฌ๋‚˜, ์‚ฌ์‹ค ์šฐ๋ฆฌ ์ถ”์ฒœ ์„œ๋น„์Šค๋ฅผ ์œ„ํ•ด์„œ๋Š” ์ž”์•ก๊ณผ ์ตœ๊ทผ ๊ตฌ๋งคํ•œ ์ƒํ’ˆ์— ๋Œ€ํ•œ ์นดํ…Œ๊ณ ๋ฆฌ ์ •๋ณด๋„ REDIS์— ๊ฐ™์ด ์บ์‹ฑ๋˜๋Š”๋ฐ, ์ด ์ •๋ณด๋“ค ๋ณ€๋™์„ฑ์ด ๋„ˆ๋ฌด ํฌ๊ณ , ์ˆœ์‹๊ฐ„์— ๋ณ€ํ•œ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด์„œ, ์ดˆ๊ธฐ ์ •๋ณด๋ฅผ REDIS์— ์ €์žฅํ–ˆ์–ด๋„ ๊ทธ ์งง์€ 1~2๋ถ„์˜ ์ˆœ๊ฐ„ ์•ˆ์— ๋ฌผ๊ฑด์„ ๋Œ€๋Ÿ‰ ๊ตฌ๋งคํ•ด์„œ ์ž”์•ก์ด ํฌ๊ฒŒ ๋ณ€๋™๋  ์ˆ˜๋„ ์žˆ๋Š” ๊ฒƒ์ด๋‹ค!!!!

๊ทธ๋Ÿฌ๋‚˜ ์šฐ๋ฆฌ ์ถ”์ฒœ ์„œ๋น„์Šค์˜ ๊ฒฝ์šฐ, ์‚ฌ์šฉ์ž ์ž”์•ก์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์ƒํ’ˆ ์ถ”์ฒœ์ด ์ด๋ค„์ง€๊ณ , ์‚ฌ์‹ค์ƒ ์ž”์•ก๊ณผ ์ถ”์ฒœ๋œ ์ƒํ’ˆ์ด ๋งž์ง€ ์•Š์œผ๋ฉด ์ถ”์ฒœ ์„œ๋น„์Šค์˜ ๋งค๋ ฅ๋„๊ฐ€ ๋งค์šฐ ๋งŽ์ด ๊ฐ์†Œํ•˜๊ณ  ์‚ฌ์šฉ์ž ๊ฒฝํ—˜๋„๋„ ๋งค์šฐ ๋–จ์–ด์ง€๊ธฐ์—โ€ฆ..์ด ๋ถ€๋ถ„์— ๋Œ€ํ•œ ๊ณ ๋ฏผ์ด ํ•„์š”ํ–ˆ๋‹ค.

์ฆ‰, โ€œDB๋ฅผ ๋œ ์ฐŒ๋ฅด๋ฉด์„œ๋„ ์ž”์•ก์€ ์‹ ์„ ํ•˜๊ฒŒโ€๊ฐ€ ํฌ์ธํŠธ์ธ ๊ฒƒ์ด๋‹ค.

ํ•ด๊ฒฐ์ฑ… - ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ํ‚ค ์„ค๊ณ„

์ด ๋ถ€๋ถ„์€ ํ‚ค ์„ค๊ณ„ ๋ฐฉ์‹์„ ๋‘ ๊ฐœ๋กœ ๋‚˜๋ˆ„๊ณ , ๋ณ€๋™์ด ๋งŽ์€ ์š”์†Œ๋งŒ TTL์„ ์ž‘๊ฒŒ ์„ค์ •ํ•˜์—ฌ DB ์กฐํšŒ ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์„ค๊ณ„ํ•˜๋ฉด ๊ณ ๋ฏผ์ ์„ ์–ด๋А์ •๋„ ํ•ด์†Œํ•  ์ˆ˜ ์žˆ๋‹ค.

๋ณ€ํ™”๊ฐ€ ์ ์€ name, creditLimit์„ JSON์œผ๋กœ ๋ฌถ์–ด์„œ TTL์„ 1์‹œ๊ฐ„ ~ 1์ผ ์ •๋„๋กœ ๊ธธ๊ฒŒ ์„ค์ •ํ•˜๊ณ , ๋ณ€ํ™”๊ฐ€ ์ƒ๋Œ€์ ์œผ๋กœ ๋งŽ์€ creditLeft์™€ hobbies์˜ TTL์„ ์งง๊ฒŒ (5๋ถ„) ์„ค์ •ํ•˜๊ณ , Hash๋กœ ์ €์žฅ๋˜๋„๋ก ํ•˜์—ฌ ์ด๋ฒคํŠธ๋กœ ๊ฐฑ์‹ ๋˜๋„๋ก ํ•˜๋Š” ๊ฒƒ์ด๋‹ค ๋˜ํ•œ, ์ฐธ๊ณ ๋กœ ํ‚ค๋ฅผ ์ƒ์„ฑํ• ๋•Œ userId๋Š” ํ‚ค์— ๋„ฃ์„ ๊ฒƒ์ด๋ฏ€๋กœ userId๋Š” ๋”ฐ๋กœ value์— ์ €์žฅํ•  ํ•„์š”๊ฐ€ ์—†๋‹ค.

๊ทธ๋Ÿฌ๋‚˜, ์—ฌ๊ธฐ๊นŒ์ง€ ์ •ํ•ด๋’€๋Š”๋ฐ ๋‹ค์‹œ ๊ณ ๋ฏผ์ ์ด ์ƒ๊ฒผ๋‹คโ€ฆ

์•„๋‹ˆ ๊ทธ๋Ÿฌ๋ฉด, ์ฑ„ํŒ…๋ฐฉ์„ ์—ด์–ด๋‘” ์ฑ„๋กœ 10๋ถ„ ์ •๋„๊ฐ€ ์ง€๋‚ฌ๋Š”๋ฐ ๋‹ค์‹œ ์ถ”์ฒœํ•ด๋‹ฌ๋ผ๊ณ  ํ•œ๋‹ค๋˜๊ฐ€ ํ•˜๋ฉด ์–ด๋–ป๊ฒŒ ํ•ด?

๊ทธ๋Ÿฌ๋ฉด ๋˜ ์ด๋Ÿฐ ๋ฌธ์ œ๊ฐ€ ์กด์žฌํ•œ๋‹ค. TTL์„ 5๋ถ„ ์ •๋„๋กœ ์งง๊ฒŒ ํ•ด๋’€๋‹ค๊ณ  ํ•˜์ž. ๊ทธ๋ ‡๋‹ค๋ฉด ์‚ฌ์šฉ์ž๊ฐ€ ์ฑ„ํŒ…๋ฐฉ์„ ์˜ค๋ž˜ ์ผœ ๋‘” ์ƒํƒœ์—์„œ TTL ์‹œ๊ฐ„๋ณด๋‹ค ๋งŽ์€ ์‹œ๊ฐ„์„ ํ˜๋ ค๋ณด๋‚ธ ๋‹ค์Œ์— ๋‹ค์‹œ ์ถ”์ฒœํ•ด๋‹ฌ๋ผ๊ณ  ํ•˜๋ฉด ์–ด๋–ป๊ฒŒ ํ•ด์•ผํ• ๊นŒ?

๊ทธ๋ž˜์„œ ์ด๋Ÿด๋•Œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ฐฉ๋ฒ•์ด ๋ฐ”๋กœ ์บ์‹œ ๋ฏธ์Šค(Read-Through) ํŒจํ„ด์ด๋‹ค.

์บ์‹œ ๋ฏธ์Šค(Read-Through) ํŒจํ„ด?

์บ์‹œ ๋ฏธ์Šค๋ž€, Redis์—์„œ ๋จผ์ € ์กฐํšŒํ•˜๋˜, ๊ฐ’์ด ์—†์„ ๊ฒฝ์šฐ DB์—์„œ ๋‹ค์‹œ โ€œ์‹ ์„ ํ•œ ๊ฐ’โ€์„ ๊ฐ€์ ธ์™€์„œ ์„ธํŒ…ํ•ด์„œ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฐฉ์‹œ์„ ์˜๋ฏธํ•œ๋‹ค.

์ด๋ ‡๊ฒŒ ์ง„ํ–‰ํ•ด๋„, TTL์ด ์กด์žฌํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํ‰์†Œ์—๋„ DB ๋ถ€ํ•˜๋ฅผ ์–ด๋А์ •๋„ ๋ฐฉ์ง€ ํ•  ์ˆ˜ ์žˆ๋‹ค.

์ฆ‰, ๋‚ด๊ฐ€ ๋‚ด๋ฆฐ ์ตœ์ข… ์ „๋žต์„ ์ •๋ฆฌํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

์ข…๋ฅ˜key namevaluesTTL๊ธฐํƒ€
snapshotv1:snap:user:{userId}{"name":"ํ”ผ์šฉํฌ","creditLimit":30000}12hRead-Through
balancev1:bal:user:{userId}balance(int)3mRead-Through
hobbyv1:hb:user:{userId}hobby(String)5mRead-Through

์Šค๋ƒ…์ƒท JSON ์ง๋ ฌํ™”๋ฅผ ์œ„ํ•œ Config ์„ค์ •

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory cf) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(cf);

        // Key๋Š” String, Value๋Š” JSON ์ง๋ ฌํ™”
        var stringSer = new StringRedisSerializer();
        var jsonSer   = new GenericJackson2JsonRedisSerializer();

        template.setKeySerializer(stringSer);
        template.setValueSerializer(jsonSer);
        template.setHashKeySerializer(stringSer);
        template.setHashValueSerializer(jsonSer);

        template.afterPropertiesSet();
        return template;
    }
}

์ด๋ ‡๊ฒŒ Config์— ์ง๋ ฌํ™”๋ฅผ ์ •์˜ํ•ด์•ผ ํ•˜๋Š” ์ด์œ ๋Š”, Spring Boot ๊ธฐ๋ณธ RedisTemplate<String, Object> ๋Š” JdkSerializationRedisSerializer๋ฅผ ์“ฐ๋Š”๋ฐ, ์ด ๋•Œ๋ฌธ์— Redis์— ๋ฐ”์ดํŠธ ๋ฐฐ์—ด(์ด์ƒํ•œ ๋ฐ”์ด๋„ˆ๋ฆฌ)๋กœ ์ €์žฅ์ด ๋˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

๊ทธ๋ž˜์„œ GenericJackson2JsonRedisSerializer๋ฅผ ์ด์šฉํ•ด์„œ JdkSerializationRedisSerializer๊ฐ€ ์•„๋‹Œ, ์šฐ๋ฆฌ๊ฐ€ ์›ํ•˜๋Š” Json ๋ฐฉ์‹์œผ๋กœ ๋ฌธ์ œ ์—†์ด ์ €์žฅ๋˜๋„๋ก ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.

๋ณธ๊ฒฉ์ ์œผ๋กœ ์บ์‹ฑ์„ ์‹œ์ž‘ํ•ด๋ณด์ž!

์—ฐ๋™๊นŒ์ง€ ์™„๋ฃŒ ๋˜์—ˆ์œผ๋‹ˆ, ์ด์ œ ์„œ๋ฒ„์—์„œ > ๋ ˆ๋””์Šค๋กœ ์บ์‹ฑํ•˜๋Š” ๋‹จ๊ณ„๋ฅผ ์ง„ํ–‰ํ•ด์•ผ ํ•œ๋‹ค.

๋‚˜์˜ ๊ฒฝ์šฐ, ์š”์†Œ๋งˆ๋‹ค TTL์„ ๋‹ค๋ฅด๊ฒŒ ์„ค์ •ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ์„ธ๋ถ€์ ์ธ ์„ค์ •์ด ํ•„์š”ํ•˜๋‹ค. ์ฆ‰, JSON ์ง๋ ฌํ™” + ํ•ญ๋ชฉ ์ข…๋ฅ˜๋ณ„ ์„œ๋กœ ๋‹ค๋ฅธ TTL + ์ปค๋ฐ‹ ์ดํ›„ ์บ์‹ฑ ์ผ๊ด€์„ฑ์„ ์ง€์ผœ์•ผ ํ•˜๋Š” ๊ฒƒ์ธ๋ฐ, ๊ทธ๋Ÿฌ๋ฏ€๋กœ RedisTemplate + ์ „์šฉ ์บ์‹œ ์ปดํฌ๋„ŒํŠธ ๋ถ„๋ฆฌ๋กœ ๋งŒ๋“œ๋Š” ๊ฒƒ์ด ์ข‹๋‹ค.

๋‚˜์˜ ๊ฒฝ์šฐ๋Š” ์ด๋ฏธ RedisTemplate๋ฅผ ํ†ตํ•ด JSON ์ง๋ ฌํ™”๋ฅผ ์„ค์ •ํ•ด ๋‘” ์ƒํƒœ์ด๋ฏ€๋กœ, ์บ์‹œ ์ŠคํŽ™๋งŒ ์งœ๋‘๋ฉด ๋œ๋‹ค.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
REDIS ์บ์‹ฑ์„ ์œ„ํ•œ TTL ์„ค์ • ๊ฐ’

ํ‚ค ์˜ˆ์‹œ : v1:snap:user:123, v1:bal:user:123, v1:hb:user:123
 */
@Getter
@RequiredArgsConstructor
public enum CacheSpec {
    // 1) ์‚ฌ์šฉ์ž ์Šค๋ƒ…์ƒท(JSON)
    SNAPSHOT("v1:snap", Duration.ofHours(12)),
    // 2) ์ž”์•ก(String) - 3๋ถ„
    BALANCE("v1:bal", Duration.ofMinutes(3)),
    // 3) ์ทจ๋ฏธ(List<String> or Set<String>) - 5๋ถ„
    HOBBIES("v1:hb", Duration.ofMinutes(5));

    private final String prefix; //ํ‚ค ๋„ค์ž„ ์ŠคํŽ˜์ด์Šค
    private final Duration ttl;
}

๋˜ํ•œ, ์„œ๋น„์Šค ๋‹จ์—์„œ ์ตœ๋Œ€ํ•œ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ์บ์‹ฑ์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ ๋ ˆ๋””์Šค๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ ์œ ํ‹ธ ํด๋ž˜์Šค๊ฐ€ ํ•„์š”ํ•˜๋‹ค.

์ด ์œ ํ‹ธ ํด๋ž˜์Šค๋Š”, ๋ ˆ๋””์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์„œ๋น„์Šค ๋‹จ์—์„œ ์ผ๊ด€์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๋„๋ก ํ•˜๊ธฐ ์œ„ํ•ด ์ •์˜ํ•ด๋‘”๋‹ค.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@Component
@RequiredArgsConstructor
public class CacheStore {
    // ์ •์˜ํ•ด๋‘” REDIS TEMPLATE๋ฅผ ๊ฐ€์ ธ์˜ด
    private final RedisTemplate<String, Object> redis;

    // ํ‚ค๋ฅผ ํ•œ ๊ณณ์—์„œ ๋งŒ๋“ ๋‹ค.
    private String key(CacheSpec spec, Object userId){
        return spec.getPrefix() + ":user:" + userId;
    }

    // put ์ฒซ๋ฒˆ์งธ๋Š” ๊ทธ ์ŠคํŽ™(CacheSpec)์— ์ •ํ•ด๋‘” ๊ธฐ๋ณธ TTL
    // put ๋‘๋ฒˆ์งธ๋Š” ์ •์˜ํ•œ ttl์„ ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ์„๋•Œ ์‚ฌ์šฉํ•œ๋‹ค.
    public void put(CacheSpec spec, Object userId, Object value) {
        put(spec, userId, value, spec.getTtl());
    }

    public void put(CacheSpec spec, Object userId, Object value, Duration ttl) {
        redis.opsForValue().set(key(spec, userId), value, ttl);
    }

    // redis๋Š” Object๋งŒ ๋ฐ˜ํ™˜ํ•˜๊ธฐ ๋•Œ๋ฌธ์— DTO๋ฅผ ๊บผ๋‚ด ์“ฐ๋ ค๋ฉด ์บ์ŠคํŒ…์ด ํ•„์ˆ˜์ด๋‹ค.
    // ๊ทธ๋ž˜์„œ redis.opsForValue().get(...)์˜ ๊ฐ’์€ ํ•ญ์ƒ Object์ด๊ธฐ ๋•Œ๋ฌธ์— ์ด๊ฑธ ์ •์˜ํ•ด์„œ ์„œ๋น„์Šค ๋‹จ์—์„œ ์บ์ŠคํŒ…์„ ํ•˜์ง€ ์•Š๋„๋ก ํ•ด์•ผ ํ•œ๋‹ค.
    @SuppressWarnings("unchecked")
    public <T> T get(CacheSpec spec, Object userId, Class<T> type) {
        Object raw = redis.opsForValue().get(key(spec, userId));
        return type.cast(raw); //typecast๋ฅผ ํ•œ ๋ฒˆ ํ•ด์ฃผ๊ธฐ ๋•Œ๋ฌธ์—, ์ž˜๋ชป๋œ ํƒ€์ž…์ผ๊ฒฝ์šฐ ClassCastException๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.
    }

    // DB์—์„œ ํ•ด๋‹น ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฐ”๋€Œ์—ˆ๊ฑฐ๋‚˜ ์—†์–ด์กŒ์„ ๋•Œ, ์บ์‹œ๋„ ์ง€์›Œ์„œ ์ผ๊ด€์ ์œผ๋กœ ๋งŒ๋“ค๊ธฐ ์œ„ํ•จ์ด๋‹ค.
    // โ€œTTL ๋งŒ๋ฃŒโ€๋ฅผ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š๊ณ  ์ฆ‰์‹œ ๋ฌดํšจํ™”ํ•  ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค.
    public void evict(CacheSpec spec, Object userId) {
        redis.delete(key(spec, userId));
    }

    // Read-Through: ์บ์‹œ ๋ฏธ์Šค ์‹œ ๋กœ๋” ์‹คํ–‰ โ†’ ์บ์‹œ์— ์ €์žฅ ํ›„ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ์„ค์ •ํ•œ๋‹ค.
    public <T> T getOrLoad(CacheSpec spec, Object userId, Class<T> type, Supplier<T> loader) {
        // 1) ์บ์‹œ ๋จผ์ € ์กฐํšŒ
        T cached = this.get(spec, userId, type);
        if (cached != null) {
            return cached;
        }

        // 2) ๋ฏธ์Šค๋ฉด "๋กœ๋”" ์‹คํ–‰ (์—ฌ๊ธฐ์„œ DB ์ ‘๊ทผ์ด ์ˆ˜ํ–‰๋จ)
        // ์—ฌ๊ธฐ์„œ Supplier<T>๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์กฐํšŒ ๋กœ์ง์„ ์ˆจ๊ธธ ์ˆ˜ ์žˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ์กฐํšŒ ๋กœ์ง์ด ์„œ๋น„์Šค ๋‹จ์œผ๋กœ ์ƒˆ์–ด๋‚˜๊ฐ€์ง€ ์•Š๋„๋ก ํ•œ๋‹ค.
        T loaded = loader.get();

        // 3) ๊ฐ’์ด ์žˆ์œผ๋ฉด ์บ์‹œ์— ๋„ฃ๊ณ  ๋ฐ˜ํ™˜ (ํ‚ค/TTL์€ spec์—์„œ ๊ฐ€์ ธ์˜ด)
        if (loaded != null) {
            this.put(spec, userId, loaded, spec.getTtl());
        }
        return loaded;
    }
}

redis.opsForValue()๋ฅผ ํ†ตํ•ด์„œ String ๊ธฐ๋ฐ˜ ์—ฐ์‚ฐ์ž๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด์„œ redis์˜ ๊ธฐ๋ณธ ์—ฐ์‚ฐ์ž๋“ค์„ ์ด์šฉํ•  ์ค€๋น„๋ฅผ ํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

set(key(spec, userId), value, ttl)์„ ํ†ตํ•ด์„œ ๋‚ด๊ฐ€ ์›ํ•˜๋Š” key๋กœ ์กฐํ•ฉ์„ ํ•ด์„œ, ํ•ด๋‹นํ•˜๋Š” value๋ฅผ ์ €์žฅํ•˜๋ฉด redis์— ์ €์žฅ์ด ๊ฐ€๋Šฅํ•œ ๊ฒƒ์ด๋‹ค. ์ด๋ฅผ put์œผ๋กœ ์ •์˜ํ•ด์„œ ๋ชจ๋“  ๊ณณ์—์„œ ์ผ๊ด€์ ์œผ๋กœ ๊ฐ’์„ ์ €์žฅํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌ์„ฑํ–ˆ๋‹ค.

getOrLoad์€ redis์— ๊ฐ’์ด ์—†์œผ๋ฉด Supplier๋กœ ๊ฐ์‹ผ db ์กฐํšŒ ๋กœ์ง์„ ์‹คํ–‰ํ•˜๊ณ , ์žˆ์œผ๋ฉด ๋ฐ”๋กœ cache์— ์žˆ๋Š” ๊ฐ’์„ ๊บผ๋‚ผ ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•œ ๋กœ์ง์ด๋‹ค. ์ด ๋กœ์ง์ด ๋ฐ”๋กœ read-through๋ฅผ ์œ„ํ•œ ๋ถ€๋ถ„์ด๋‹ค.

Supplier๋ฅผ ์‚ฌ์šฉํ•œ ์ด์œ ๋Š” ๋ฌด์—‡์ธ๊ฐ€?

์ด๋Ÿฐ ์˜๋ฌธ์„ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋ ‡๋‹ค๋ฉด ์™œ ๊ตณ์ด db๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๋กœ์ง์„ ๋ฐ”๋กœ ๋„ฃ์ง€ ์•Š๊ณ , ์ด๋ ‡๊ฒŒ Supplier๋กœ ํ•œ ๋ฒˆ ๊ฐ์‹ธ๋Š” ๋ฐฉ์‹์œผ๋กœ ์ง„ํ–‰ํ•˜๋Š” ๊ฒƒ์ผ๊นŒ?

๋Œ€ํ‘œ์ ์ธ ์ด์œ ๋Š” โ€œ์ค‘๋ณต ๋กœ์งโ€์„ ์ค„์ด๊ธฐ ์œ„ํ•จ์ด๋‹ค.

Supplier๋ฅผ ์ด์šฉํ•ด์„œ DB ์กฐํšŒ ๋กœ์ง์„ CacheStore์— ๊ณ ์ •ํ•˜์ง€ ์•Š๊ณ , ์„œ๋น„์Šค ๋‹จ์—์„œ ํ•„์š”ํ•œ ์ฟผ๋ฆฌ๋ฅผ ์ž์œ ๋กญ๊ฒŒ ์ฃผ์ž…ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๋ฉด์„œ, CacheStore ๋‚ด๋ถ€ ๋กœ์ง์€ ์žฌ์‚ฌ์šฉ ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•  ์ˆ˜ ์žˆ๊ธฐ ์œ„ํ•จ์ธ ๊ฒƒ์ด๋‹ค.

์‹ค์ œ Supplier๋ฅผ ์ด์šฉํ•ด์„œ method๋ฅผ ์ •์˜ํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

1
2
3
4
5
6
7
8
public MemberCachingDto getSnapshot(Long memberId) {
        return cacheStore.getOrLoad(
                CacheSpec.SNAPSHOT,
                memberId,
                MemberCachingDto.class,
                () -> memberRepository.findDtoByMemberId(memberId)
        );
    }

์ด๋ฅผ ์ด์šฉํ•ด์„œ ๋ ˆ๋””์Šค ์บ์‹ฑ์„ ์œ„ํ•œ Service class๋ฅผ ๊ตฌ์„ฑํ•ด๋ณด์ž!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true) // ๊ธฐ๋ณธ์€ ์กฐํšŒ ํŠธ๋žœ์žญ์…˜
public class MemberService {
    private final MemberRepository memberRepository;
    private final CacheStore cacheStore;

    public int getBalance(Long memberId) {
        return cacheStore.getOrLoad(
                CacheSpec.BALANCE,
                memberId,
                Integer.class,
                // ์บ์‹œ ๋ฏธ์Šค์ผ ๋•Œ๋งŒ ์‹คํ–‰๋˜๋Š” DB ๋กœ๋”ฉ ํ•จ์ˆ˜์ด๋‹ค. ์ด๋ฅผ Supplier๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํ•„์š”ํ•œ ํ•จ์ˆ˜๋ฅผ ์ •์˜ํ•˜๋ฉด ๋œ๋‹ค.
                () -> memberRepository.findBalanceByMemberId(memberId)
        );
    }

    public String getHobby(Long memberId) {
        return cacheStore.getOrLoad(
                CacheSpec.HOBBIES,
                memberId,
                String.class,
                () -> memberRepository.findHobbyByMemberId(memberId)
        );
    }

    public MemberCachingDto getSnapshot(Long memberId) {
        return cacheStore.getOrLoad(
                CacheSpec.SNAPSHOT,
                memberId,
                MemberCachingDto.class,
                () -> memberRepository.findDtoByMemberId(memberId)
        );
    }
}

์ด๋ ‡๊ฒŒ ์ •์˜ํ•˜๋ฉด, ๊ฐ๊ฐ ์ •์˜๋œ ํ‚ค - ๋ฐธ๋ฅ˜ ์…‹ ๋งˆ๋‹ค ๊ทธ ํ˜•ํƒœ์— ๋งž๋Š” ๊ฐ’์„ ์บ์‹œ์—์„œ ๋ถˆ๋Ÿฌ์˜ค๊ฑฐ๋‚˜, ์ €์žฅํ•  ์ˆ˜ ์žˆ๋‹ค.

์ด๋ฅผ ์‹ค์ œ๋กœ ์–ด๋–ป๊ฒŒ ํ™œ์šฉํ•˜๋Š”์ง€๋Š”, ๋‹ค์Œ ํฌ์ŠคํŒ…์—์„œ ์ง์ ‘ Test Code๋ฅผ ์‚ดํŽด๋ณด๋ฉฐ ์•Œ์•„๋ณด๋„๋ก ํ•˜์ž.

This post is licensed under CC BY 4.0 by the author.