λ³Έλ¬Έ λ°”λ‘œκ°€κΈ°
  • μž₯원읡 κΈ°μˆ λΈ”λ‘œκ·Έ
πŸ”¬web application/- System architecture

cache 101 - Spring Cache 에 λŒ€ν•˜μ—¬ (feat. μΊμ‹œλ‘œ todo list λ₯Ό λ§Œλ“€μ–΄λ³΄μž)

by Wonit 2024. 3. 13.

cache 101 μ‹œλ¦¬μ¦ˆλŠ” web application 을 κ°œλ°œν•˜λ©° λ§ˆμ£Όν•˜λŠ” cache 에 λŒ€ν•΄ ν•„μš”ν•œ 지식과 λ„κ΅¬λ“€μ˜ μ‚¬μš©λ²•μ„ ν•™μŠ΅ν•˜λŠ” μ‹œλ¦¬μ¦ˆμž…λ‹ˆλ‹€.

 

의 μˆœμ„œλŒ€λ‘œ 글을 μ½μœΌμ‹œλ©΄ ν•™μŠ΅μ— 더 λ§Žμ€ 도움이 λ©λ‹ˆλ‹€.

 


μ˜€λŠ˜μ€ Spring μ—μ„œ μ œκ³΅ν•˜λŠ” Cache 에 λŒ€ν•΄μ„œ 이야기 ν•΄λ³Ό 것이닀

 

Spring μ—μ„œλŠ” Cache 에 λŒ€ν•˜μ—¬ Spring Transaction κ³Ό λ§ˆμ°¬κ°€μ§€λ‘œ 높은 좔상화λ₯Ό μ œκ³΅ν•œλ‹€

 

@Transactional // spring transaction support
public Todo create() {}

@Cacheable // spring cache support
public Todo create() {}

 

Spring μ—μ„œλŠ” 이λ₯Ό Cache Abstraction 이라고 λΆ€λ₯΄λŠ”데, 이번 μ‹œκ°„μ—λŠ” κ·Έ κ°œλ…κ³Ό case-study λ₯Ό 톡해 μ‚¬μš© λ°©λ²•κΉŒμ§€ μ•Œμ•„λ³Ό μ˜ˆμ •μ΄λ‹€.

λͺ©μ°¨

  • Cache 에 λŒ€ν•œ κ°„λž΅ 정리
  • Spring Cache Abstraction μ΄λž€?
    • cache κ΄€λ ¨λœ 핡심 클래슀 μ„€λͺ…
      • cache manager
    • Spring Boot Starter 에 ν¬ν•¨λœ κΈ°λ³Έ CacheManager
  • Spring Cache 의 핡심, Cache μ–΄λ…Έν…Œμ΄μ…˜
    • Cacheable
    • CachePut
    • CacheEvict
    • Caching
  • Case Study. Todo λ₯Ό λ§Œλ“€μ–΄λ³΄λ©° λ°°μš°λŠ” Spring Cache
    • step 1. μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ„ΈνŒ… 및 sample code κ΅¬ν˜„ (cache configuration)
    • step 2. @Cacheable 을 ν†΅ν•œ cache 쑰회
    • step 3. @CacheEvict λ₯Ό μ΄μš©ν•œ cache μ΄ˆκΈ°ν™”
    • step 4. @CachePut 을 μ΄μš©ν•œ cache μ—…λ°μ΄νŠΈ
    • step 5. @Caching 을 μ΄μš©ν•œ 볡합 cache 관리
  • Cache 더 잘 μ“°κΈ°
    • CachePut κ³Ό Type
    • invalidation λˆ„λ½κ³Ό stale data
    • cache expiration

Cache 에 λŒ€ν•œ κ°„λž΅ μ„€λͺ…

 

μ§€λ‚œ Cache 에 λŒ€ν•œ 거의 λͺ¨λ“  것 μ—μ„œ μš°λ¦¬λŠ” μΊμ‹œμ— λŒ€ν•œ κ°œλ…μ , 이둠적 λ‚΄μš©λ“€μ„ μ΄ν•΄ν•˜μ˜€λ‹€.

 

 

μ§€λ‚œ μ‹œκ°„ μ΄μ•ΌκΈ°ν–ˆλ˜ μΊμ‹œμ— λŒ€ν•΄μ„œ μ•„μ£Ό κ°„λž΅ν•˜κ²Œ μš”μ•½ν•΄λ³΄μž

 

  • μΊμ‹œλŠ” web application 의 응닡/처리 μ„±λŠ₯ ν–₯상을 μœ„ν•΄ μ‚¬μš©λœλ‹€
    • μΊμ‹œλŠ” 이전에 μ—°μ‚°λœ 값을 μž¬μ—°μ‚° ν•˜μ§€ μ•Šλ„λ‘ 미리 μ €μž₯ν•œλ‹€
    • 과거에 μ‘°νšŒν•œ 데이터가 λ³€ν•˜μ§€ μ•Šμ•˜λ‹€λ©΄, 이λ₯Ό λΉ λ₯΄κ²Œ μ ‘κ·Όν•  수 μžˆλ„λ‘ μ €μž₯ν•œλ‹€
    • 데이터λ₯Ό νŠΉμ • 곡간에 μ €μž₯ν•˜λŠ” 것을 μΊμ‹±ν•œλ‹€! 라고 ν‘œν˜„ν•œλ‹€
  • μΊμ‹œ μ €μž₯μ†Œμ— 데이터λ₯Ό μ‘°νšŒν•˜μ˜€μ„ λ•ŒλŠ”
    • 값이 μ‘΄μž¬ν•˜λ©΄ cache hit
    • μ‘΄μž¬ν•˜μ§€ μ•ŠμœΌλ©΄ cache miss
  • μΊμ‹œκ°€ 꽉 μ°¨λ©΄ 방좜, eviction 을 μˆ˜ν–‰ν•΄μ€˜μ•Ό ν•œλ‹€
    • 만료 μ‹œκ°„μ— μ˜ν•œ expiration
    • μ €μž₯μ†Œμ˜ lack of memory 에 μ˜ν•œ replacement

 

λ§Œμ•½ 기얡이 λ‚˜μ§€ μ•Šκ±°λ‚˜ 처음 λ³΄λŠ” μ‚¬λžŒλ“€μ΄λΌλ©΄ μ•žμ„  글을 λ‹€μ‹œ 보고 와도 μ’‹λ‹€

 

Spring 의 Cache Abstraction μ΄λž€?

 

2012 λ…„ Spring framework 3.1 버전이 릴리즈 되고 4.1 버전 μ¦ˆμŒμ— Spring Integration 의 ν•˜μœ„ λͺ¨λ“ˆλ‘œ Cache Abstraction μ΄λΌλŠ” λͺ¨λ“ˆμ΄ ν•¨κ»˜ ν¬ν•¨λ˜μ–΄ 릴리즈 λ˜μ—ˆλ‹€,

 

JPA 와 λ§ˆμ°¬κ°€μ§€λ‘œ 캐싱도 μ—­μ‹œ JCache (JSR-107) 이라고 λΆˆλ¦¬λŠ” 캐싱에 λŒ€ν•œ ν‘œμ€€μ΄ μ‘΄μž¬ν•˜μ˜€λŠ”λ°, 2012년도 μΆœμ‹œν•œ Spring 3.1 버전과 4.1 버전 이후뢀터 이λ₯Ό κ³΅μ‹μ μœΌλ‘œ 지원 및 κ°•λ ₯ν•œ ν™•μž₯ κΈ°λŠ₯을 μ œκ³΅ν•œλ‹€

 

Spring μ—μ„œλŠ” JCache μ—μ„œ μ‚¬μš©ν•  수 μ—†λŠ” κΈ°λŠ₯λ“€ 외에도 λ”μš± λ‹€μ–‘ν•œ κΈ°λŠ₯을 μ œκ³΅ν•œλ‹€.

 

λŒ€ν‘œμ μœΌλ‘œλŠ” 단일 μΊμ‹œλ§Œ μ§€μ›ν•˜λŠ” JCache 와 달리 CacheResolver λ₯Ό 톡해 cache 에 name 을 지정할 수 μžˆμ–΄ multi cache ꡬ성이 κ°€λŠ₯ν•˜λ‹€

 

이제 μ—­μ‚¬λŠ” μ—¬κΈ°κΉŒμ§€λ§Œ μ•Œμ•„λ³΄κ³ , μ‹€μ œλ‘œ Spring Cache Abstraction λ₯Ό μ§€νƒ±ν•˜λŠ” κ΅¬μ„±μš”μ†Œλ“€μ— λŒ€ν•΄μ„œ κ°„λž΅νžˆ μ•Œμ•„λ³΄μž

 

Spring Cache Abstraction 의 핡심 μš”μ†Œλ“€

 

Spring Cache Abstraction μ—μ„œλŠ” 크게 3가지 ν΄λž˜μŠ€κ°€ μ‘΄μž¬ν•œλ‹€

 

  1. CacheManager
  2. Cache
  3. CacheResolver

 

이 3개의 핡심 ν΄λž˜μŠ€λ“€κ³Ό μ—¬λŸ¬κ°€μ§€ CacheOperation 듀을 톡해 Spring 은 AOP 둜 캐싱을 μ§€μ›ν•œλ‹€

 

1. CacheManager

 

CacheManager λŠ” μ΄λ¦„μ—μ„œλΆ€ν„° μ•Œ 수 μžˆλ“― Spring cache abstraction 의 κ°€μž₯ 핡심적인 ν΄λž˜μŠ€μ΄λ‹€.

 

Redis, Caffeine ν˜Ήμ€ Ehcache 와 같은 μΊμ‹œ κ΅¬ν˜„μ²΄μ˜ μΈμŠ€ν„΄μŠ€λ₯Ό 생성/κ΄€λ¦¬ν•˜λŠ” 역할을 μˆ˜ν–‰ν•œλ‹€.

 

Spring Cache λ₯Ό μ‚¬μš©ν•˜λ €λ©΄ ν•„μˆ˜μ μœΌλ‘œ CacheManager μΈμŠ€ν„΄μŠ€λ₯Ό λ“±λ‘ν•΄μ€˜μ•Ό ν•œλ‹€.

 

이 CacheManager interface 의 DIP 덕뢄에 μš°λ¦¬κ°€ μ—¬λŸ¬κ°€μ§€ cache 의 κ΅¬ν˜„μ²΄λ“€μ„ μ›ν•˜λŠ” μ‹œμ μ— νŽΈλ¦¬ν•˜κ²Œ 변경이 κ°€λŠ₯ν•΄μ‘Œλ‹€

 

2. CacheResolver 와 Cache

CacheResolver λŠ” name 을 기반으둜 Cache λ₯Ό 찾을 수 있게 ν•΄μ€€λ‹€.

 

μ•žμ„œ λ§ν•œ JCache 와 λ‹€λ₯Έ 점인 name 을 톡해 multi cache κ°€ κ°€λŠ₯ν•˜λ„λ‘ ν•˜λŠ” 역할을 μˆ˜ν–‰ν•œλ‹€

 

CacheResolver λ₯Ό 톡해 Cache λΌλŠ” 객체λ₯Ό μ°Ύμ•„μ˜€λ©΄, μš°λ¦¬λŠ” ν•΄λ‹Ή κ°μ²΄μ—κ²Œ Put, Evict λ‚˜ 쑰회 λͺ…령을 μˆ˜ν–‰ν•œλ‹€.

 

Spring Boot 와 Cache Abstraction

 

기본적으둜 Spring Cache Abstraction 은 κ΅¬ν˜„μ΄ μ•„λ‹Œ μΆ”μƒμœΌλ‘œ 즉, Implementation 을 λ“±λ‘ν•΄μ€˜μ•Ό ν•œλ‹€

 

ν•˜μ§€λ§Œ spring-boot-starter-cache λ₯Ό gradle 의쑴으둜 μΆ”κ°€ν•˜λ©΄ autoconfiguration 에 μ˜ν•΄ κΈ°λ³Έ κ΅¬ν˜„μ²΄κ°€ λ“±λ‘λœλ‹€.

 

이 ν΄λž˜μŠ€λŠ” spring-boot-autoconfigure 에 μ‘΄μž¬ν•˜λŠ” SimpleCacheConfiguration ν΄λž˜μŠ€μ΄λ‹€

 

ν•΄λ‹Ή 클래슀λ₯Ό 보면 CacheManager Bean 이 μ—†λ‹€λ©΄ 기본으둜 ConcurrentCacheManager λ₯Ό μ‚¬μš©ν•œλ‹€.

 

ConcurrentCacheManager λ‚΄λΆ€μ μœΌλ‘œλŠ” 일반적인 HashMap μ—μ„œ segmented locking 을 μΆ”κ°€ν•˜μ—¬ Thread-Safe λ₯Ό 보μž₯ν•˜λŠ” ConcurrentHashMap 을 μ΄μš©ν•œλ‹€.

 

CacheManager λ“±λ‘ν•˜κΈ°

 

CacheManager Bean 을 Redis λ‚˜ Caffeine κ³Ό 같은 λ‹€λ₯Έ κ΅¬ν˜„μ²΄λ₯Ό μ΄μš©ν•˜λŠ” 것 μ—­μ‹œ κ°€λŠ₯ν•˜λ‹€.

 

μš°λ¦¬κ°€ 직접 Bean 으둜 RedisCacheManager λ‚˜ CaffeineCacheManager λ₯Ό λ“±λ‘ν•΄μ£Όκ±°λ‚˜ Configuration property λ₯Ό μ΄μš©ν•˜λ©΄ λœλ‹€.

 

 

spring cache 의 ConfigurationProperties 클래슀λ₯Ό 보면 "spring.cache" λΌλŠ” μ„€μ • 값을 기반으둜 λ™μž‘ν•˜λŠ”λ°, λ‚΄λΆ€μ μœΌλ‘œλŠ” Redis property, Caffeine property 등을 μ§€μ›ν•˜λ―€λ‘œ μžμ„Έν•œ 속성과 configuration 은 Spring docs-Configuring the Cache Storage λ₯Ό ν™•μΈν•˜μž

 

Spring Cache 의 핡심, Cache μ–΄λ…Έν…Œμ΄μ…˜

 

Spring Cache 은 λ‹€λ₯Έ Spring family 와 λ™μΌν•˜κ²Œ μ–΄λ…Έν…Œμ΄μ…˜μ„ ν†΅ν•œ μ„ μ–Έμ μœΌλ‘œ μΊμ‹œλ₯Ό 관리할 수 있게 ν•΄μ€€λ‹€.

 

λ‹€μŒ ν–‰μœ„μ™€ μ–΄λ…Έν…Œμ΄μ…˜λ§Œ 이해해도 Spring Cache λ₯Ό κ°€λ³κ²Œ μ΄μš©ν•˜λŠ”λ° 무리가 없을 것이닀

 

Spring Cache Abstraction μ—μ„œλŠ” 5가지 μ–΄λ…Έν…Œμ΄μ…˜μ„ μ œκ³΅ν•œλ‹€.

 

  • @Cacheable -> Cache 에 값을 μ“°κΈ°/읽기
  • @CachePut -> Cache 에 값을 κ°±μ‹ 
  • @CacheEvict -> Cache λ₯Ό μ΄ˆκΈ°ν™”
  • @Caching -> cache 연산을 ν•˜λ‚˜λ‘œ λ§Œλ“€μ–΄μ£ΌκΈ°
  • @CacheConfig -> cache μ„€μ • κ³΅μœ ν•˜κΈ°

 

μ•„λž˜μ— λ‚˜μ˜¬ case-study μ—μ„œ μžμ„Ένžˆ μ•Œμ•„λ³΄λ„λ‘ ν•˜κ³  μ§€κΈˆμ€ κ°œλ…λ§Œ ν•˜λ‚˜μ”© ν•΅μ‹¬λ§Œ μ•Œμ•„λ³΄μž

 

@Cacheable

 

λ©”μ„œλ“œμ˜ 호좜 κ²°κ³Ό, 즉 return value λ₯Ό cache 에 μ €μž₯ν•˜κ³  이후 λ™μΌν•œ μš”μ²­μ΄λΌκ³  νŒλ‹¨λ  경우 μ‹€μ œ λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•˜μ§€ μ•Šκ³  cache 에 μ‘΄μž¬ν•˜λŠ” 값을 λ°˜ν™˜ν•œλ‹€

 

μ–΄λ–€ 데이터λ₯Ό μΊμ‹±ν•œλ‹€ 라고 ν–ˆμ„ λ•Œ @Cacheable 만 λͺ…μ‹œν•˜λ©΄ λœλ‹€.

 

@CachePut

 

λ©”μ„œλ“œμ˜ 호좜 κ²°κ³Ό, 즉 return value λ₯Ό κ°•μ œλ‘œ μΊμ‹œμ— μ €μž₯/κ°±μ‹  ν•œλ‹€

 

보톡 caching 된 데이터λ₯Ό update ν•  λ•Œ μ‚¬μš©ν•œλ‹€

 

@CacheEvict

 

cache 에 μ‘΄μž¬ν•˜λŠ” 데이터가 stale λ˜μ–΄ invalidate μ‹œν‚¬ λ•Œ, 즉 μΊμ‹œλ₯Ό μ΄ˆκΈ°ν™”ν•  λ•Œ μ‚¬μš©ν•œλ‹€

 

cache λŠ” 정합성이 μ€‘μš”ν•˜λ‹€. μ–΄λ–€ 데이터λ₯Ό @Cacheable 둜 μΊμ‹œμ— μ μž¬ν–ˆλŠ”λ°, κ·Έ 데이터가 μ—…λ°μ΄νŠΈ λ˜μ—ˆλ‹€.

 

Put 을 톡해 ν•˜λ‚˜ν•˜λ‚˜ update ν•˜κΈ°λ„ νž˜λ“€κ³  ν•  μˆ˜λ„ 없을 λ•ŒλŠ” Evict λ₯Ό ν†΅ν•΄μ„œ μΊμ‹±λœ 데이터λ₯Ό λ‚ λ €λ²„λ¦¬λŠ” 것도 쒋은 방법이닀.

 

@Caching

 

μ—¬λŸ¬ μΊμ‹œ 연산듀을 ν•˜λ‚˜λ‘œ 묢을 λ•Œ μ‚¬μš©ν•œλ‹€

 

μ–΄λ–€ λ©”μ„œλ“œ μœ„μ—μ„œλŠ” cache 된 데이터가 update 되며 λ™μ‹œμ— λ‹€λ₯Έ cache μ—λŠ” evict κ°€ λ˜μ–΄μ•Ό ν•  λ•Œ @Caching 을 μ‚¬μš©ν•˜λ©΄ μ’‹λ‹€

 

Case Study. Todo λ₯Ό λ§Œλ“€μ–΄λ³΄λ©° λ°°μš°λŠ” Spring Cache

 

이제 μ‹€μ œλ‘œ κ°„λ‹¨ν•œ Todo λ₯Ό λ“±λ‘ν•˜κ³  쑰회, μˆ˜μ •ν•  수 μžˆλŠ” μ„œλΉ„μŠ€μ— Spring Cache λ₯Ό μΆ”κ°€ν•΄λ³΄μž

 

λ‚΄λΆ€μ μœΌλ‘œλŠ” λ³΅μž‘ν•  수 μžˆμ§€λ§Œ Spring Cache κ°€ 정말 좔상화λ₯Ό 잘 ν•΄λ†“μ•˜κΈ° λ•Œλ¬Έμ—, μ‰½κ²Œ μ‚¬μš©ν•  수 μžˆλ‹€

 

λ‹€μŒ 4가지 step 을 ν†΅ν•΄μ„œ cache λ₯Ό κ²½ν—˜ν•΄λ³΄κ³  μΆ”κ°€μ μœΌλ‘œ μΊμ‹œλ₯Ό μ‚¬μš©ν•  λ•Œ λ§ˆμ£Όν•  수 μžˆλŠ” λ¬Έμ œλ„ ν•¨κ»˜ μ•Œμ•„λ³Ό 것이닀

 

  • step 1. μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ„ΈνŒ… 및 sample code κ΅¬ν˜„
  • step 2. @Cacheable 을 ν†΅ν•œ cache 쑰회
  • step 3. @CacheEvict λ₯Ό μ΄μš©ν•œ cache μ΄ˆκΈ°ν™”
  • step 4. @CachePut 을 μ΄μš©ν•œ cache μ—…λ°μ΄νŠΈ
  • step 5. @Caching 을 μ΄μš©ν•œ 볡합 μΊμ‹œ 관리

 

μœ„μ˜ μ½”λ“œμ™€ ν…ŒμŠ€νŠΈ ν™˜κ²½μ„ μœ„ν•œ μžμ„Έν•œ μ„ΈνŒ… 및 μ„ΈλΆ€ κ΅¬ν˜„ λ‘œμ§λ“€μ€ https://github.com/my-research/spring-cache μ—μ„œ 확인할 수 μžˆλ‹€.

 

GitHub - my-research/spring-cache: spring cache abstraction

spring cache abstraction. Contribute to my-research/spring-cache development by creating an account on GitHub.

github.com

 

step 1. Todo μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ„ΈνŒ… 및 μ½”λ“œ κ΅¬ν˜„

 

μš°λ¦¬κ°€ λ§Œλ“€μ–΄λ³Ό TODO application 은 λ‹€μŒ 4κ°€μ§€μ˜ API 듀을 μ œκ³΅ν•˜κ³  μžˆλ‹€.

 

 

API 듀은 성격에 λ”°λΌμ„œ command 와 query 둜 λΆ„λ₯˜ν•  수 μžˆλ‹€.

 

  1. TODO λ₯Ό μƒμ„±ν•œλ‹€ (command)
  2. TODO 의 μƒνƒœλ₯Ό λ³€κ²½ν•œλ‹€ (command)
  3. TODO 상세λ₯Ό μ‘°νšŒν•œλ‹€ (query)
  4. user κ°€ μ†Œμœ ν•œ λͺ¨λ“  TODO λ₯Ό μ‘°νšŒν•œλ‹€ (query)

 

λΉ λ₯΄κ²Œ μ € 4개의 API λ₯Ό κ΅¬ν˜„ν•  것인데 사싀 핡심은 todo λ₯Ό κ΅¬ν˜„ν•˜λŠ” 것이 μ•„λ‹ˆλ―€λ‘œ 핡심 둜직만 보여쀄 것이닀.

 

μžμ„Έν•œ μ½”λ“œλ“€μ€ μ•žμ„œ 이야기 ν–ˆλ˜ git repository μ—μ„œ 확인할 수 μžˆλ‹€.

 

λ¨Όμ € todo 의 μƒνƒœλ₯Ό λ³€κ²½ν•˜λŠ” command service λ₯Ό κ΅¬ν˜„ν•΄λ³΄μž

// TODO λ₯Ό μƒμ„±ν•œλ‹€
fun create(userId: Long, name: String): Todo {
  val todo = Todo(
    userId = userId,
    name = name,
  )
  return repository.save(todo)
}

// TODO 의 μƒνƒœλ₯Ό λ³€κ²½ν•œλ‹€
fun transit(todoId: Long, status: String): Todo {

  val todo = repository.findById(todoId).orElseThrow()

  todo.transitTo(TodoStatus.valueOf(status))

  return repository.save(todo)
}

그리고 todo 의 μƒνƒœλ₯Ό μ‘°νšŒν•˜λŠ” query service λ₯Ό κ΅¬ν˜„ν•΄λ³΄μž.

 

μ—¬κΈ°μ„œ λˆˆμ—¬κ²¨λ΄μ•Ό ν•  점은 repository 에 μ‘°νšŒν•˜λŠ” λ‘œμ§μ— μΊμ‹œλ₯Ό μ μš©ν•œ ν›„ 극적인 μ„±λŠ₯ ν–₯상을 μ²΄κ°ν•˜κΈ° μœ„ν•΄ μ˜λ„μ μœΌλ‘œ Thread sleep 을 μ€¬λ‹€λŠ” 점이닀

 

// userId 에 ν•΄λ‹Ήν•˜λŠ” λͺ¨λ“  TODO λ₯Ό μ‘°νšŒν•œλ‹€
fun findAllBy(userId: Long): List<Todo> {
  SleepUtils.sleep()
  return repository.findAllByUserId(userId).toList()
}

// TODO id λ₯Ό 톡해 상세λ₯Ό μ‘°νšŒν•œλ‹€
fun findBy(id: Long): Todo {
  SleepUtils.sleep()
  return repository.findById(id).orElseThrow()
}

 

이제 μ•žμ„  λ‘œμ§λ“€μ„ http λ₯Ό ν†΅ν•˜μ—¬ ν˜ΈμΆœν•  수 μžˆλ„λ‘ κ°„λ‹¨ν•œ Controller 만 κ΅¬ν˜„ν•΄μ£Όλ©΄ μ‹€μŠ΅ μ€€λΉ„κ°€ λλ‚œλ‹€

 

step 2. @Cacheable 을 μ΄μš©ν•œ cache 쑰회

μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ΄ μ€€λΉ„λ˜μ—ˆμœΌλ‹ˆ todo λ₯Ό ν•˜λ‚˜ μƒμ„±ν•˜κ³  userId 둜 μ‘°νšŒν•΄λ³΄μž!

 

μš°λ¦¬λŠ” μ•žμ„œμ„œ thread sleep 을 톡해 latency λ₯Ό μ˜λ„μ μœΌλ‘œ λ°œμƒμ‹œμΌ°λ‹€. (μ•½ 3초)

 

ν•΄λ‹Ή λ©”μ„œλ“œμ— μΊμ‹œλ₯Ό μ μš©ν•˜μ—¬ μ„±λŠ₯ ν–₯상을 μ‹œμΌœλ³΄μž. μš°λ¦¬κ°€ λ°°μ› λ˜ @Cacheable 을 λͺ…μ‹œν•΄μ£Όλ©΄ λœλ‹€

 

@Cacheable(cacheNames = ["todosByUserId"])
fun findAllBy(userId: Long): List<Todo> {
  // 둜직 μƒλž΅
}


@Cacheable λ₯Ό μ‚¬μš©ν•  λ•ŒλŠ” cacheName 을 μ§€μ •ν•΄μ€˜μ•Ό ν•˜λŠ”λ°, 이 cache name 은 μ€‘μš”ν•œ 역할을 μˆ˜ν–‰ν•œλ‹€.

 

그럼 μœ„μ˜ κ·Έλ¦Όκ³Ό 같이 spring cache κ°€ λ‚΄λΆ€μ μœΌλ‘œ μΊμ‹œλ₯Ό cache name (globally unique) -> cache key (locally unique) μˆœμ„œλ‘œ κ΅¬λΆ„ν•˜μ—¬ μ €μž₯ν•˜κ³  μ‘°νšŒν•œλ‹€.

 

κ²°κ΅­ μœ„μ˜ λ©”μ„œλ“œκ°€ 호좜되면 todosByUserId λΌλŠ” cache 에 userId(예λ₯Ό λ“€μ–΄ 1004) 의 key λ₯Ό 가진 Todo(id: 1, name:"λ°₯λ¨ΉκΈ°") λΌλŠ” value κ°€ μ €μž₯λœλ‹€

 

κ²°κ³Ό

 

cache λ₯Ό μ μš©ν•œ ν›„ API call 의 μ„±λŠ₯이 ν–₯μƒν•˜λŠ” 것은 λ„ˆλ¬΄λ‚˜λ„ λ‹Ήμ—°ν•˜λ‹€

 

  • 첫번쨰 μš”μ²­ & 응닡
    • cache miss λ°œμƒ 3s μ†Œμš”
    • μ—°μ‚°μ˜ κ²°κ³Όλ₯Ό cache 에 μ €μž₯
  • λ‘λ²ˆμ§Έ μš”μ²­ & 응닡
    • cache hit λ°œμƒ (n)ms μ†Œμš”


step 3. @CacheEvict λ₯Ό μ΄μš©ν•œ cache μ΄ˆκΈ°ν™”

μ—¬κΈ°μ„œ λ§Œμ•½ todo λ₯Ό 또 μΆ”κ°€ν•˜κ³  userId 둜 λͺ¨λ“  todo λ₯Ό μ‘°νšŒν•˜λ©΄ μ–΄λ–»κ²Œ 될까?

 

μš°λ¦¬κ°€ μ•žμ„  step μ—μ„œ cache miss 에 μ˜ν•΄ todo 정보듀을 μΊμ‹œμ— μ μž¬ν•˜μ˜€μœΌλ‹ˆ 이후 λͺ¨λ“  API call 은 cache hit κ°€ λ°œμƒν•˜μ—¬ backing store 인 database 에 접근을 ν•˜μ§€ μ•Šμ„ 것이닀.

 

κ²°κ΅­ μ‹€μ œλ‘œλŠ” todo κ°€ μΆ”κ°€λ˜μ—ˆμ§€λ§Œ cache 에 μ˜ν•΄ μ΅œμ‹ μ˜ 데이터λ₯Ό μ œκ³΅λ°›μ§€ λͺ»ν•˜κ²Œ λ˜λŠ” 상황이 λ°œμƒν•œλ‹€.

 

이떄, cache 에 μ‘΄μž¬ν•˜λŠ” 과거의 데이터λ₯Ό stale data 라고 ν•˜κ³  cache λ₯Ό μ΅œμ‹ μœΌλ‘œ κ°±μ‹ ν•˜κΈ° μœ„ν•΄ cache invalidate κ°€ λ°œμƒν•΄μ•Ό ν•œλ‹€.

 

cache invalidate λŠ” μ—¬λŸ¬κ°€μ§€ 방법이 μ‘΄μž¬ν•˜λŠ”λ°, κ°€μž₯ μ‰¬μš΄ λ°©λ²•μœΌλ‘œλŠ” cache 에 값을 μ§€μ›Œλ²„λ € cache miss λ₯Ό μœ λ„ν•˜κ³  db 에 λ‹€μ‹œ μ‘°νšŒν•˜λ„λ‘ ν•˜λŠ” 방법이닀.

 

이 방법을 μ‚¬μš©ν•˜λ €λ©΄ @CacheEvict λ₯Ό μ‚¬μš©ν•˜λ©΄ λœλ‹€

 

@CacheEvict(cacheNames = ["todosByUserId"], key = "#userId")
fun create(userId: Long, name: String): Todo {
  val todo = Todo(userId, name)
  return repository.save(todo)
}


@CacheEvict
μ–΄λ…Έν…Œμ΄μ…˜μ΄ λΆ™μ–΄μžˆλŠ” λ©”μ„œλ“œκ°€ 호좜되면 cacheName κ³Ό key 에 mapping 된 value 듀을 λͺ¨λ‘ λͺ¨λ‘ evict(방좜, 제거)ν•˜λŠ” λͺ…령을 μˆ˜ν–‰ν•œλ‹€.

 

그럼 μ•žμ„œ μ΄μ•ΌκΈ°ν–ˆλ˜κ²ƒ 처럼 todo κ°€ μƒˆλ‘­κ²Œ μΆ”κ°€λ˜λ©΄ userId 에 ν•΄λ‹Ήλ˜λŠ” λͺ¨λ“  todo cache λ₯Ό μ§€μ›Œλ²„λ¦¬κΈ° λ•Œλ¬Έμ— μΆ”ν›„ 쑰회λ₯Ό μˆ˜ν–‰ν•˜λŠ” client κ°€ stale data λ₯Ό 받지 μ•Šκ²Œ λœλ‹€

 

step 4. @CachePut 을 μ΄μš©ν•œ cache update

 

userId 둜 μ‘°νšŒν•  λ•Œ cache λ₯Ό μ μš©ν–ˆλŠ”λ°, μ΄λ²ˆμ—λŠ” todo detail 을 μ‘°νšŒν•˜λŠ” api 에도 cache λ₯Ό μ μš©ν•΄λ³΄μž

 

μ—­μ‹œ λ§ˆμ°¬κ°€μ§€λ‘œ @Cacheable 을 μ΄μš©ν•˜λ©΄ λœλ‹€. μ΄λ²ˆμ—λŠ” todoById λΌλŠ” cache name 을 μ§€μ •ν•΄λ³΄μž.

 

@Cacheable("todoById")
fun findBy(id: Long): Todo {
  SleepUtils.sleep()
  return repository.findById(id).orElseThrow()
}

이런 μƒν™©μ—μ„œ λ§Œμ•½ todo λ₯Ό update ν•˜λ©΄ cache λŠ” μ–΄λ–»κ²Œ 될까?

 

그럼 μ•žμ„œ μ΄μ•ΌκΈ°ν–ˆλ˜ λ°©μ‹μœΌλ‘œ @CacheEvict(cacheNames = ["todoById"], key = "#userId") λ₯Ό 톡해 update 된 todo κ°€ μ—…λ°μ΄νŠΈλ  λ•Œλ§ˆλ‹€ Eviction 을 μˆ˜ν–‰ν•˜μ—¬ cache λ₯Ό μ΅œμ‹ ν™”μ‹œν‚¬ 수 μžˆμ„ 것이닀.

 

ν•˜μ§€λ§Œ cache evict λŠ” 말 κ·ΈλŒ€λ‘œ μΊμ‹œμ—μ„œ λ°©μΆœμ‹œμΌœλ²„λ¦¬λŠ” 연산이기 λ•Œλ¬Έμ— 항상 cache miss κ°€ λ°œμƒν•˜κ²Œ λœλ‹€.

 

κ·Έλž˜μ„œ 자주 update λœλ‹€λ©΄ cache evict & cache miss κ°€ μž¦μ•„μ§€λ―€λ‘œ μΊμ‹œλ₯Ό μ΄μš©ν•œ μ„±λŠ₯ ν–₯상을 κΈ°λŒ€ν•˜κΈ°κ°€ μ–΄λ ΅λ‹€.

 

이런 μƒν™©μ—μ„œλŠ” @CachePut 을 톡해 cache 에 μ €μž₯된 값을 update μ‹œμΌœλ²„λ¦¬λŠ” 방법을 μ‚¬μš©ν•˜λ©΄ 쒋은 해결책이 될 수 μžˆλ‹€.

 

@CachePut(cacheNames = ["todoById"], key = "#todoId")
fun transit(todoId: Long, status: String): Todo {

  val todo = repository.findById(todoId).orElseThrow()
  todo.transitTo(TodoStatus.valueOf(status))

  return repository.save(todo)
}

 

ν•΄λ‹Ή μ΄λ ‡κ²Œ @CachePut 을 μ‚¬μš©ν•˜κ²Œ λœλ‹€λ©΄, ν•΄λ‹Ή μ–΄λ…Έν…Œμ΄μ…˜μ΄ 뢙은 λ©”μ„œλ“œμ˜ return 값을 cache 에 직접 update ν•˜κ²Œ λ˜λ―€λ‘œ cache miss κ°€ λ°œμƒν•  일이 μ—†λ‹€.

 

step 5. @Caching 을 μ΄μš©ν•œ 볡합 μΊμ‹œ 관리

 

μ§€κΈˆ μš°λ¦¬λŠ” cache λ₯Ό μ΄μš©ν•΄μ„œ λ‹€μŒκ³Ό 같은 것을을 ν–ˆλ‹€

 

  • userId 에 ν•΄λ‹Ήν•˜λŠ” λͺ¨λ“  todo λ₯Ό μ‘°νšŒν•  수 μžˆλ‹€
  • todo λ₯Ό 생성/μΆ”κ°€ν•  수 μžˆλ‹€.
    • todo κ°€ μΆ”κ°€λ˜λ©΄ userId 에 μ—°κ²°λœ μΊμ‹œλ₯Ό evict ν•˜μ—¬ stale 을 λ°©μ§€ν•˜μ˜€λ‹€.
  • todo 의 μƒνƒœλ₯Ό λ³€κ²½ν•  수 μžˆλ‹€
    • todoId 에 μ—°κ²°λœ μΊμ‹œλ₯Ό update ν•˜μ—¬ stale 을 λ°©μ§€ν•˜μ˜€λ‹€

 

ν•˜μ§€λ§Œ ν•œ 가지 λ¬Έμ œκ°€ μžˆλ‹€. λ‹€μŒ ν”Œλ‘œμš°λ₯Ό 봐보자

 

  1. todo 생성
  2. userId 둜 todo 전체 쑰회 -> cache 적재
  3. νŠΉμ • todo μ—…λ°μ΄νŠΈ -> userId cache μ—λŠ” 반영 x
  4. userId 둜 todo 전체 쑰회 -> stale 데이터 λ°˜ν™˜

 

이 상황을 보면 todo update μ—°μ‚°μ—μ„œ 2개의 cache λ₯Ό invalidation ν•΄μ£Όμ–΄μ•Ό ν•œλ‹€λŠ” 것을 μ˜λ―Έν•œλ‹€.

 

  1. todoById μΊμ‹œλ₯Ό update
  2. todosByUserId μΊμ‹œλ₯Ό evict

 

2번이 update κ°€ μ•„λ‹Œ evict 인 μ΄μœ λŠ” λ°˜ν™˜κ°’ λ•Œλ¬Έμ΄λ‹€.

 

@CachePut 은 λ©”μ„œλ“œ 호좜 λ°˜ν™˜κ°’μ„ cache 에 update ν•œλ‹€κ³  ν–ˆλŠ”λ°, transit λ©”μ„œλ“œμ˜ λ°˜ν™˜κ°’μ€ Todo 클래슀인 반면 todosByUserId cache 에 μ €μž₯된 value λŠ” List<Todo> μ΄λ―€λ‘œ λ°˜ν™˜ νƒ€μž…μ΄ λ‹€λ₯΄λ―€λ‘œ μ‚¬μš©ν•  수 μ—†λ‹€.

 

λ˜ν•œ param 으둜 userId λ₯Ό 받을 수 μ—†μœΌλ―€λ‘œ νŠΉμ • key 에 ν•΄λ‹Ήν•˜λŠ” cache λ₯Ό μ§€μšΈ 수 μ—†λ‹€.

 

@CacheEvict 의 속성 쀑에 allEntries λΌλŠ” 속성이 μžˆλŠ”λ°, ν•΄λ‹Ή 속성은 cache name 에 ν•΄λ‹Ήν•˜λŠ” λͺ¨λ“  μΊμ‹œ μ—”νŠΈλ¦¬λ₯Ό μ§€μ›Œλ²„λ¦΄ 수 μžˆλ‹€.

 

μ΄λ ‡κ²Œ 2개의 cache μ–΄λ…Έν…Œμ΄μ…˜μ„ μ‚¬μš©ν•  λ•Œμ—λŠ” @Caching μ΄λΌλŠ” μ–΄λ…Έν…Œμ΄μ…˜μ„ 톡해 μ—¬λŸ¬κ°œμ˜ μΊμ‹œ 연산을 ν•˜λ‚˜λ‘œ 묢을 수 μžˆλ‹€.

 

@Caching(
  put = [CachePut(cacheNames = ["todoById"], key = "#todoId")],
  evict = [CacheEvict(cacheNames = ["todosByUserId"], allEntries = true)]
)
fun transit(todoId: Long, status: String): Todo {

  val todo = repository.findById(todoId).orElseThrow()
  todo.transitTo(TodoStatus.valueOf(status))

  return repository.save(todo)
}

Spring Cache 더 잘 μ“°κΈ°

1. CachePut 을 μ‚¬μš©ν•˜λ©° μ£Όμ˜ν•  점

 

@CachePut 연산은 method 의 λ°˜ν™˜ 값이 cache 에 update λœλ‹€.

 

이 νŠΉμ„± λ•Œλ¬Έμ— λ°œμƒν•  수 μžˆλŠ” λ¬Έμ œκ°€ ν•˜λ‚˜ μžˆλ‹€.

 

cache update κ°€ runtime 에 λ°œμƒν•˜κΈ° λ•Œλ¬Έμ— λ©”μ„œλ“œμ˜ λ°˜ν™˜ 값이 기쑴에 cache 에 μ €μž₯된 객체의 type κ³Ό λ§žμ§€ μ•ŠλŠ”λ‹€λ©΄ @Cacheable κ°€ λͺ…μ‹œλœ λ‘œμ§μ—μ„œ ClassCastException 이 λ°œμƒν•  수 μžˆλ‹€.

 

λ°”λ‘œ μ•žμ„  step 5 μ—μ„œ λ°œμƒν•  수 μžˆλŠ” λ¬Έμ œμ™€ λ™μΌν•˜λ‹€

 

compiler type check λ₯Ό μ΄μš©ν•  수 μ—†κΈ° λ•Œλ¬Έμ— 항상 νƒ€μž…μ— λŒ€ν•΄μ„œλŠ” 주의λ₯Ό κΈ°μšΈμ—¬μ•Ό ν•œλ‹€

 

2. 볡합 μΊμ‹œ 연산을 μ‚¬μš©ν•˜λ©° μ£Όμ˜ν•  점

 

μ•žμ„  case-study 의 step5 λ₯Ό 보면 μ„œλ‘œ λ‹€λ₯Έ μΊμ‹œ 연산을 ν•˜λ‚˜λ‘œ λ¬Άμ—ˆλŠ”λ°, @CacheEvict λŠ” μœ„μ™€ 같은 μƒν™©μ—μ„œλŠ” μ΅œλŒ€ν•œ μ‚¬μš©ν•˜μ§€ μ•ŠλŠ” 것이 λ°”λžŒμ§ν•˜λ‹€.

 

@CacheEvict λ₯Ό μ‚¬μš©ν•˜κ²Œ λœλ‹€λ©΄ μžμ—°μŠ€λŸ½κ²Œ cache key 에 ν•΄λ‹Ήν•˜λŠ” 데이터λ₯Ό μ—†μ• λŠ” 것이기 λ•Œλ¬Έμ— μžμ—°μŠ€λŸ½κ²Œ cache miss ν™•λ₯ μ΄ μ˜¬λΌκ°„λ‹€.

 

κ²°κ΅­ μΊμ‹œλ‘œ μΈν•œ μ„±λŠ₯ ν–₯상을 κΈ°λŒ€ν•˜κΈ° μ–΄λ €μ›Œμ§„λ‹€.

 

κ·ΈλŸΌμ—λ„ λΆˆκ΅¬ν•˜κ³  μš°λ¦¬λŠ” ν•΄λ‹Ή λ©”μ„œλ“œμ—μ„œ cache key(userId) λ₯Ό 받을 수 μ—†μœΌλ‹ˆ CacheEvict λ₯Ό μ‚¬μš©ν•  수 밖에 μ—†λ‹€.

 

μ΄λ•Œ CacheManager λ₯Ό 직접 μ‚¬μš©ν•œλ‹€λ©΄ evict λŒ€μ‹  put 을 μˆ˜ν–‰ν•  수 μžˆλ‹€.

 

사싀상 μš°λ¦¬κ°€ μ‚¬μš©ν•˜λŠ” @Cacheable μ΄λ‚˜ @CachePut κ³Ό 같은 μ–΄λ…Έν…Œμ΄μ…˜μ€ Spring AOP λ₯Ό μœ„ν•œ 것이고 κ²°κ΅­ λ‚΄λΆ€μ μœΌλ‘œλŠ” CacheManager μ—κ²Œ 연산을 μš”μ²­ν•˜λŠ” 것이닀.

 

λ‹€μŒκ³Ό 같이 CacheManager μ—κ²Œ 직접 연산을 ν•˜λŠ” 방법도 μ‘΄μž¬ν•œλ‹€.

 

@Caching(
  put = [CachePut(cacheNames = ["todoById"], key = "#todoId")],
)
fun transit(todoId: Long, status: String): Todo {

  val todo = repository.findById(todoId).orElseThrow()
  todo.transitTo(TodoStatus.valueOf(status))

  cacheManager.getCache("todosByUserId")
    ?.evict(todo.userId)

  return repository.save(todo)
}

 

3. μΊμ‹œ 만료 μ •μ±…

 

μΊμ‹œμ—λŠ” expiration μ΄λΌλŠ” κ°œλ…μ΄ μžˆλ‹€.

 

νŠΉμ • μ‹œκ°„μ΄ μ§€λ‚˜λ„ μΊμ‹œκ°€ μ—…λ°μ΄νŠΈλ˜μ§€ μ•ŠλŠ”λ‹€λ©΄ μ•„μ˜ˆ ν•΄λ‹Ή μΊμ‹œλ₯Ό λ°©μΆœμ‹œμΌœλ²„λ¦¬λŠ” 것이닀.

 

기본적으둜 ConcurrentCacheManager μ—λŠ” cache expiration 이 μ—†μ§€λ§Œ caffeine κ΅¬ν˜„μ²΄μ—λŠ” expiration 이 μ‘΄μž¬ν•œλ‹€.

 

μ•„λž˜λŠ” Caffeine Cache λ₯Ό μ‚¬μš©ν•  λ•Œ μΆ”κ°€ν•΄μ£ΌλŠ” bean config λ₯Ό 가져와봀닀

 

@Configuration
class CacheConfig {
  @Bean
  fun caffeineCacheManager(caffeine: Caffeine<Any, Any>): CacheManager {
    val manager = CaffeineCacheManager()
    manager.setCaffeine(caffeine)
    return manager
  }

  @Bean
  fun caffeine(): Caffeine<Any, Any>? {
    return Caffeine.newBuilder()
      .initialCapacity(10)
      .expireAfterWrite(Duration.of(100, ChronoUnit.SECONDS))
      .recordStats()
  }
}

 

μœ„μ˜ μ½”λ“œλ₯Ό 보면 expireAfterWrite μ˜΅μ…˜μ„ 톡해 write 이후 μ–Έμ œ ν•΄λ‹Ή cache λ₯Ό expire μ‹œν‚¬μ§€ λͺ…μ‹œν•  수 μžˆλ‹€.

 

마치며

 

μ΄λ ‡κ²Œ μ˜€λŠ˜μ€ Spring Cache Abstraction 을 ν†΅ν•΄μ„œ cache 에 λŒ€ν•œ κ°œλ…κ³Ό μ‹€μ œ κ·Έ κ΅¬ν˜„μ„ μ•Œμ•„λ³΄μ•˜λ‹€.

 

λ‹¨μˆœν•΄λ³΄μ΄λŠ” todo application 을 λ§Œλ“€λ”λΌλ„ μ—¬λŸ¬κ°€μ§€ μΊμ‹œ 정합성에 λŒ€ν•œ κ³ λ―Ό ν¬μΈνŠΈκ°€ μ‘΄μž¬ν•œλ‹€λŠ” 것을 μ—¬λŸ¬λΆ„λ„ ν™•μΈν–ˆμ„ 것이닀.

 

이처럼 cache λŠ” μ†Œν”„νŠΈμ›¨μ–΄λ₯Ό λΉ λ₯΄μ§€λ§Œ λ³΅μž‘ν•˜κ²Œ λ§Œλ“œλŠ” μš”μ†Œμ€‘ ν•˜λ‚˜λ‹€.

 

μ•žμ„  예제처럼 자주 λ³€κ²½λ˜λŠ” λ°μ΄ν„°μ—λŠ” μΊμ‹œκ°€ 맀우 λΉ„νš¨μœ¨μ μ΄λ‹€. 였히렀 관리 μš”μ†Œλ§Œ λŠ˜λ¦¬λŠ” 것이닀.

 

이처럼 λ§žμ§€ μ•ŠλŠ” 상황에 μΊμ‹œλ₯Ό λ„μž…ν•˜κ²Œ λœλ‹€λ©΄ μ‹œμŠ€ν…œμ€ λ³΅μž‘μ„±μ˜ λŠͺ에 λΉ μ Έλ²„λ¦¬κ²Œ λ˜λ‹ˆ μ£Όμ˜ν•˜μž.

 

μ•„λ§ˆλ„ 이 μ‹œλ¦¬μ¦ˆλ₯Ό μ „λΆ€ μ™„μ£Όν•œλ‹€λ©΄ 어디에 μΊμ‹œλ₯Ό λ°°μΉ˜ν•˜κ³  μ–΄λ–€μ‹μœΌλ‘œ μΊμ‹œ 정합성을 λ§žμΆ°μ€˜μ•Ό 할지 insight λ₯Ό μ–»μ–΄κ°ˆ 수 있으리라 λ―ΏλŠ”λ‹€.

 

λŒ“κΈ€