๐Ÿ›ขRedis Cache

ozzingยท2022๋…„ 12์›” 8์ผ
0

์บก์Šคํ†ค๋””์ž์ธ ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋ฉฐ ์†๋„ ๊ฐœ์„ ์„ ์œ„ํ•ด In-Memory ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์ธ Redis๋ฅผ ํ™œ์šฉํ•˜์˜€๋‹ค. ํ•ด๋‹น ์„œ๋น„์Šค์˜ ๊ฒฝ์šฐ ์‚ฌ์šฉ์ž๋กœ๋ถ€ํ„ฐ ๋‹จ์–ด๊ฐ€ ์ž…๋ ฅ๋˜๋ฉด Word2Vec ๋ชจ๋ธ์„ ์‚ฌ์šฉํ•˜๋Š” AI ์„œ๋ฒ„์— ๊ทธ์— ๋Œ€ํ•œ ์œ ์˜์–ด๋ฅผ ์š”์ฒญํ•œ๋‹ค. ์ด ๋•Œ ๋งค๋ฒˆ ๋‹จ์–ด๋ฅผ ๋ถ„์„ํ•˜๋Š”๋ฐ ์†Œ์š”๋˜๋Š” ์‹œ๊ฐ„์„ ์ค„์ด๊ณ ์ž ํŠน์ • ๋‹จ์–ด์— ๋Œ€ํ•œ ๋ฆฌ์Šคํฐ์Šค๋ฅผ ์บ์‹ฑํ•˜์˜€๋‹ค. ๋˜, ๊ฐ€์žฅ ์ž์ฃผ ์กฐํšŒ๋˜๋Š” ์ •๋ณด์ธ ์œ ์ € ์ •๋ณด๋„ ์บ์‹ฑํ•˜์˜€๋‹ค.


Redis

1. ๊ฐœ์š”

  • Remote Dictionary Server
  • ๋ฉ”๋ชจ๋ฆฌ ๊ธฐ๋ฐ˜์˜ key-value ๊ตฌ์กฐ์˜ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ
  • ๋ชจ๋“  ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฉ”๋ชจ๋ฆฌ์— ์ €์žฅ๋˜์–ด ๋น ๋ฅธ Read, Write ์†๋„๋ฅผ ๋ณด์žฅํ•˜๋Š” NoSQL
  • ์ผ๋ฐ˜์ ์œผ๋กœ ์‹ฑ๊ธ€ ์Šค๋ ˆ๋“œ ์‚ฌ์šฉ
  • String, Set, Sorted Set, Hash, List ๋“ฑ ๋‹ค์–‘ํ•œ ๋ฐ์ดํ„ฐ ํƒ€์ž… ์ œ๊ณต

2. ๊ธฐ๋Šฅ

  • In-Memory ์บ์‹ฑ
    • API ํ˜ธ์ถœ ์‹œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ ๊ฐ’์„ ์•ž๋‹จ์— ์บ์‹ฑ
  • Pub/Sub ๋ฉ”์„ธ์ง€ ํ
    • ๋ฆฌ์ŠคํŠธ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๋ฅผ ์‚ฌ์šฉํ•œ ๋ฉ”์„ธ์ง€ ํ, ๋Œ€๊ธฐ์—ด
  • ์„ธ์…˜ ์Šคํ† ์–ด
    • ์‚ฌ์šฉ์ž ์ธ์ฆ ํ† ํฐ, ์„ธ์…˜ ์ƒํƒœ ๋“ฑ ์„ธ์…˜ ์ •๋ณด ๊ด€๋ฆฌ

3. ์‹ฑ๊ธ€ ์Šค๋ ˆ๋“œ ๋™์ž‘

์žฅ์ 

  • Context-Switch ๋น„์šฉ ์ ˆ๊ฐ
  • ์Šค๋ ˆ๋“œ ๊ฐ„ ์ž์› ๊ณต์œ ๋กœ ์ธํ•œ ๋ฌธ์ œ์—์„œ ์ž์œ ๋กœ์›€
    • shared data ๊ด€๋ฆฌ ๋ถˆํ•„์š”

๋‹จ์ 

  • long-time ๋ช…๋ น์–ด ์ˆ˜ํ–‰ ์‹œ ๋‹ค๋ฅธ ๋ช…๋ น์–ด๋“ค์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์—†์Œ
    • ๋ฐ์ดํ„ฐ๊ฐ€ ๋งค์šฐ ๋งŽ์€ ์ƒํ™ฉ์—์„œ ์—ฌ๋Ÿฌ๊ฐœ์˜ ํ‚ค๋ฅผ ๋‹ค๋ฃจ๋Š” ๋ช…๋ น์–ด
  • ํ•œ๋ฒˆ์— ๋งŽ์€ ์–‘์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•  ๋•Œ ๋ช…๋ น์–ด ๋ธ”๋ฝ์„ ๋ง‰๊ธฐ ์œ„ํ•ด ๋ฉ€ํ‹ฐ ์Šค๋ ˆ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ๋„ ํ•จ

Redis Cache

1. Caching ์ „๋žต

  • Lazy-Loading
    • ๋ฐ์ดํ„ฐ ์š”์ฒญ์ด ๋“ค์–ด์˜ค๋ฉด Cache์— ๋กœ๋”ฉ
    • CacheEvict ์ดํ›„ GET ์š”์ฒญ์ด ๋“ค์–ด์˜ค๋ฉด ๋‹ค์‹œ Cacheable
    • ์บ์‹œ ๋ฏธ์Šค๊ฐ€ ๋ฐœ์ƒํ•ด๋„ ์–ด์ฐจํ”ผ ๋””๋น„์—์„œ ๊ฐ€์ ธ์™€ ํฐ ํƒ€๊ฒฉ ์—†์Œ
    • ์š”์ฒญ๋ฐ›์€ ๋ฐ์ดํ„ฐ๋งŒ ์ €์žฅํ•ด ํšจ์œจ์ 
    • Cache Miss๊ฐ€ ์ž์ฃผ ๋ฐœ์ƒํ•  ๊ฒฝ์šฐ ๋”œ๋ ˆ์ด๊ฐ€ ์‹ฌํ•จ
    • Cache Miss ๋ฐœ์ƒ์‹œ ์ƒˆ๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ๊ฐ€์ ธ์˜จ ๋ฐ์ดํ„ฐ๋ฅผ ์บ์‹œ์— ์ €์žฅ, ์ตœ์‹  ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•˜๊ธฐ ์–ด๋ ค์›Œ์ง
  • Write-Through
    • ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ ์ˆ˜์ •ํ•  ๋•Œ ๋ชจ๋‘ Cache์™€ DB๊ฐ€ ์—…๋ฐ์ดํŠธ ๋จ
    • Cacheable, Cacheput ๋“ฑ ์‚ฌ์šฉํ•ด ์ถ”๊ฐ€/์ˆ˜์ • ์‹œ ๊ณ„์†ํ•ด์„œ ์บ์‹œ ์—…๋ฐ์ดํŠธ
    • ์กฐํšŒ ์‹œ ์บ์‹œ๋ฅผ ์ฝ์–ด์˜ค๊ธฐ๋งŒ ํ•˜๋ฏ€๋กœ ์บ์‹œ ๋ฏธ์Šค ๋ฐœ์ƒ ์‹œ ์น˜๋ช…์ 
    • ํ•ญ์ƒ ์ตœ์‹ ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ง
    • ์ฝํžˆ์ง€ ์•Š๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ ์กด์žฌํ•ด ๋ฆฌ์†Œ์Šค ๋‚ญ๋น„ ๋ฐœ์ƒ
  • TTL
    • ์œ„ ๋‘ ๋ฐฉ๋ฒ•์— TTL์„ ๋”ํ•˜๋ฉด ๋ฌธ์ œ๊ฐ€ ์–ด๋Š์ •๋„ ํ•ด๊ฒฐ
    • Lazy-Loading โ†’ ์ผ์ • ์‹œ๊ฐ„์ด ์ง€๋‚œ ๋’ค ๊ฐ’์ด ์‚ญ์ œ๋˜์–ด ๋‹ค์Œ ์กฐํšŒ ์‹œ ์ตœ์‹  ์ƒํƒœ ์œ ์ง€
    • Write-Through โ†’ ์—…๋ฐ์ดํŠธ ๋˜์ง€ ์•Š๋Š” ๋ฐ์ดํ„ฐ๋Š” ์ž๋™ ์‚ญ์ œ

2. ์บ์‹œ ์ €์žฅ์†Œ ์œ„์น˜

  • Local Cache
    • ๋ฐ์ดํ„ฐ ์กฐํšŒ ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ์—†๋‹ค.
    • ์„œ๋ฒ„ ๊ฐ„ ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ์ด ๊นจ์งˆ ์ˆ˜ ์žˆ๋‹ค.
    • ์„œ๋ฒ„ ๊ฐ„ ๋™๊ธฐํ™”๊ฐ€ ์–ด๋ ต๊ณ  ๋™๊ธฐํ™” ๋น„์šฉ์ด ๋ฐœ์ƒํ•œ๋‹ค.
  • Global Cache
    • ์™ธ๋ถ€ ์บ์‹œ ์ €์žฅ์†Œ์— ์ ‘๊ทผํ•˜๋Š” ๊ณผ์ •์—์„œ ๋„คํŠธ์›Œํฌ I/O ๋น„์šฉ ๋ฐœ์ƒ
    • ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ ์œ ์ง€ ๊ฐ€๋Šฅ

ํ”„๋กœ์ ํŠธ ์ ์šฉ

1. ์บ์‹œ ์ข…๋ฅ˜

Annotation๊ธฐ๋Šฅ
@Cacheable์บ์‹œ ์กฐํšŒ ํ›„ ๋ฐ˜ํ™˜, ์—†์„ ๊ฒฝ์šฐ ์ƒ์„ฑ
@CachePut๋ฌด์กฐ๊ฑด ์บ์‹œ ์ €์žฅ
@CacheEvict์บ์‹œ ์‚ญ์ œ

2. ์บ์‹œ TTL Prefix ๋ณ„ ์ปค์Šคํ…€

private final Long DEFAULT_HOURS = 1L;

// ๋™์˜์–ด ์กฐํšŒ
public final static String SYNONYM_KEY = "Synonym";
private final Long SYNONYM_HOURS = 24L;

// ์œ ์ € ์ •๋ณด ์กฐํšŒ
public final static String USER_KEY = "User";
private final Long USER_HOURS = 6L;

@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
		// default configuration
    RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig()
            .disableCachingNullValues()
            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(
                    new GenericJackson2JsonRedisSerializer()))
            .entryTtl(Duration.ofHours(DEFAULT_HOURS));

    Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
		
		// ๋™์˜์–ด configuration
    cacheConfigurations.put(SYNONYM_KEY,
            RedisCacheConfiguration.defaultCacheConfig()
                    .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(
                            new GenericJackson2JsonRedisSerializer())).entryTtl(Duration.ofHours(SYNONYM_HOURS)));
    // ์œ ์ € ์ •๋ณด configuration
		cacheConfigurations.put(USER_KEY,
            RedisCacheConfiguration.defaultCacheConfig()
                    .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(
                            new GenericJackson2JsonRedisSerializer())).entryTtl(Duration.ofHours(USER_HOURS)));

		// default + custom configuration์œผ๋กœ RedisCacheManager ์ƒ์„ฑ ๋ฐ ๋ฐ˜ํ™˜
    return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(connectionFactory)
            .cacheDefaults(configuration)
            .withInitialCacheConfigurations(cacheConfigurations).build();
}

3. ์œ ์ € ์ •๋ณด ์ˆ˜์ • Request, Command, Info, Response ๋“ฑ ํ•„์š” DTO ์ž‘์„ฑ

@Getter
@Builder
@ToString
public static class EditAccountRequest {

    private final String name;
    private final String plan;

}

4. ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์ž‘์„ฑ

  • Domain Layer์˜ Entity Class์—์„œ ๋‚ด์šฉ ๋ณ€๊ฒฝ
public void editAccount(UserCommand.EditAccountRequest request) {
    updateName(request.getName());
    updatePlan(request.getPlan());
}

public void updateName(String name) {
    if (name != null) {
        this.name = name;
    }
}

public void updatePlan(String plan) {
    if (plan != null) {
        this.plan = PlanStatus.valueOf(plan);
    }
}

5. ์œ ์ € ์กฐํšŒ API

  • Cacheable ์‚ฌ์šฉํ•ด์„œ ์œ ์ € ์กฐํšŒ ์‹œ ์บ์‹œ๊ฐ€ ์žˆ์œผ๋ฉด ๋ฐ˜ํ™˜, ์—†์œผ๋ฉด ์ƒ์„ฑ
@Operation(summary = "์œ ์ € ์ •๋ณด")
@Cacheable(value = USER_KEY, key = "#user.getId()", cacheManager = "cacheManager")
@GetMapping(value = "", produces = "application/json; charset=utf-8")
@ResponseBody
public CommonResponse getUserAccount(@AuthenticationPrincipal User user) {
		...
}

6. ์œ ์ € ์ˆ˜์ • API

  • CachePut ์‚ฌ์šฉํ•ด์„œ ์œ ์ € ์ •๋ณด ์ˆ˜์ • ์‹œ ๋ฌด์กฐ๊ฑด ์บ์‹œ ์ €์žฅ
@Operation(summary = "์œ ์ € ์ •๋ณด ์ˆ˜์ •")
@CachePut(value = USER_KEY, key = "#user.getId()", cacheManager = "cacheManager")
@PatchMapping(value = "", produces = "application/json; charset=utf-8")
@ResponseBody
public CommonResponse editUserAccount(@AuthenticationPrincipal User user,
        @Valid @RequestBody UserRequest.EditAccountRequest request) {
		...
}

7. ๋™์˜์–ด ์ถ”์ฒœ (๋‹จ์–ด ๋ถ„์„) API

  • Cacheable ์‚ฌ์šฉํ•ด์„œ ๋™์˜์–ด ์กฐํšŒ ์‹œ ์บ์‹œ๊ฐ€ ์žˆ์œผ๋ฉด ๋ฐ˜ํ™˜, ์—†์œผ๋ฉด ์ƒ์„ฑ
@Operation(summary = "๋‹จ์–ด ๋ถ„์„")
@Cacheable(value = SYNONYM_KEY, key = "#word", cacheManager = "cacheManager", unless = "#word==''")
@GetMapping(value = "/question", produces = "application/json; charset=utf-8")
@ResponseBody
public CommonResponse recommendSynonym(@AuthenticationPrincipal User user, @RequestParam String word) {

    var formResult = formRecommendFacade.recommendSynonym(word);
    var response = formResponseMapper.of(formResult);

    return CommonResponse.onSuccess(response);
}

  • ์ถ”๊ฐ€๋กœ ๋งŒ์•ฝ ์œ ์ € ์ •๋ณด๋ฅผ ๋ณธ์ธ๋งŒ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋Š” ์„œ๋น„์Šค๋ผ๋ฉด ๋กœ๊ทธ์•„์›ƒ ์‹œ์ ์— @CacheEvict๋ฅผ ์‚ฌ์šฉํ•ด ์บ์‹œ๋ฅผ ์ง€์›Œ ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ๋ฅผ ํ•ด์ค„ ์ˆ˜ ์žˆ์„ ๊ฒƒ ๊ฐ™๋‹ค.
  • ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์„ ๋ ˆ๋””์Šค ์„œ๋ฒ„์— ์ €์žฅํ•˜๊ณ  TTL์„ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ๋งŒ๋ฃŒ์‹œ๊ฐ„๊ณผ ์ผ์น˜์‹œ์ผœ ํ•ด๋‹น ํ† ํฐ์ด ๋ ˆ๋””์Šค์— ์กด์žฌํ•˜๋ฉด ๋‹ค์Œ ๋กœ์ง์„ ์‹คํ–‰ํ•˜๊ณ , ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ ๋ฌด์กฐ๊ฑด ๋กœ๊ทธ์•„์›ƒ ์‹œํ‚ฌ ์ˆ˜ ์žˆ๋‹ค.

0๊ฐœ์˜ ๋Œ“๊ธ€