Cache#1 ์คํ๋ง์ ์บ์ ์ถ์ํ(@Cacheable)
by JiwonDev
์คํ๋ง ํ๋ ์์ํฌ๋ ๋ค๋ฅธ ์๋น์ค์์ ํตํฉ & ๊ธฐ๋ฅ ์ถ์ํ๋ฅผ ์ ๊ณตํด์ค๋ค. (RedisTemplate, RestTemplate, MailSender....)
2012๋ ์คํ๋ง 3.1๋ฒ์ ๋ถํฐ ์บ์ ์ถ์ํ(Cache Abstraction)์ ์ ๊ณตํด์ฃผ๋ฉฐ, ์ด๋ 2014๋ ์คํ๋ง 4.1์์ ์ฌ๋ฌ๊ฐ์ง๊ฐ ๊ฐ์ ๋์๋ค.
์คํ๋ง ํ๋ ์์ํฌ์ ๋ค๋ฅธ ์๋น์ค์์ ํตํฉ(Integration) ๊ด๋ จ ๋ฌธ์
๐ ์บ์์ ๋ํ ์ดํด
2022.08.28 - [๐ฑ Spring Framework] - Cache#2 ์บ์์ ๋ํ ์ดํด
๐ ์คํ๋ง์ ์บ์ ์ถ์ํ (Spring Cache Abstraction)
์ด๋ ธํ ์ด์ ๊ธฐ๋ฐ์ผ๋ก ์บ์๋ฅผ ์ถ์ํํ์ฌ ์ฌ์ฉํ ์ ์๋ค. ์ด๋ Spring Data ์ @Transcational ๊ณผ ๊ต์ฅํ ์ ์ฌํ๋ค.
AOP ๊ธฐ๋ฐ์ผ๋ก ๋ฉ์๋์ ๊ฒฐ๊ณผ๋ฅผ ์บ์ฑํ์ฌ ์ฌ์ฉํ ์ ์๋ค. (* ์บ์ฑ๋ ๊ฒฝ์ฐ, ๋ฉ์๋๋ฅผ ์คํํ์ง ์๊ณ ๋ฐ๋ก ๊ฒฐ๊ณผ๋ฅผ ๋ฐํํ๋ค.)
@Cacheable("bestSeller") // cacheName: bestSeller, cachekey : BookNo๋ก, ๋ฉ์๋ ๊ฒฐ๊ณผ๋ฅผ ์บ์ฑํ๋ค.
public Book getBestSeller(String bookNo) {
return result
}
์คํ๋ง์ ์บ์ ์ถ์ํ์ ์ฌ์ฉ๋๋ ๊ฐ์ฒด๋ค์ org.springframework.cache ํจํค์ง์์ ์ฐพ์๋ณผ ์ ์๋ค.
org.springframework.cache.Cache
org.springframework.cache.CacheManager
๐ ์คํ๋ง ํ๋ ์์ํฌ์ ์บ์ ๊ตฌํ์ฒด(์ ์ฅ์)๋ ์๋ค.
Cache, CacheManager๋ ์บ์ ์ฌ์ฉ์ ์ถ์ํํ๋๊ฑฐ์ง, ์ค์ ์บ์ ์ ์ฅ์๋ ํฌํจํ์ง ์๋๋ค.
์บ์์ ๋ํ ์ธ๋ถ์ค์ (TTL, ์ ์ฑ )์ด๋ ๋ฉํฐ ์ค๋ ๋ ๋๊ธฐํ๋ฑ์ ์บ์ ์ ์ฅ์ ๊ตฌํ์ฒด๊ฐ ์ฒ๋ฆฌํด์ผํ๋ค. ์ด๋ ๊ณต์๋ฌธ์์๋ ๊ฐ์กฐ๋ ๋ด์ฉ์ด๋ค.
@Cacheable ๊ฐ์ ์ด๋
ธํ
์ด์
๋ค์ ์คํ๋ง ๋น์ผ๋ก ๋ฑ๋ก๋์ด์๋ CacheManager ์ธํฐํ์ด์ค๋ฅผ ์ด์ฉํ์ฌ ์บ์๋ฅผ ์ ์ฉ์ํจ๋ค.
์ด๋ฅผ ์ฝ๋๋ก ์ง์ ๊ตฌํํด๋ ๋์ง๋ง, ์คํ๋ง๋ถํธ์์ ์๋์ ๊ฐ์ ์บ์ ๊ตฌํ์ฒด๋ค์ CacheManager๋ก ์ฝ๊ฒ ๋ง๋ค ์ ์๊ฒ ์ ๊ณตํด์ค๋ค.
// ์คํ๋ง๋ถํธ๋ฅผ ์ด์ฉํ์ฌ ํ์
์์ ์์ฃผ ์ฌ์ฉํ๋ ์บ์ ๊ตฌํ์ฒด๋ค์ ๋๋ถ๋ถ ์ฝ๊ฒ ์ฌ์ฉํ ์ ์๋ค.
implementation 'org.springframework.boot:spring-boot-starter-cache'// ์คํ๋ง application ๋ด๋ถ ์บ์
implementation 'org.springframework.boot:spring-boot-starter-data-redis'// redis
implementation 'com.github.ben-manes.caffeine:caffeine'// caffenie
ํด๋น ์บ์ ๊ตฌํ์ฒด๋ค์ ๋ฐ๋ก ์ค์ ํด์ฃผ์ง ์์๋, ์ค์ ํ์ผ(yml) ๊ธฐ๋ฐ์ผ๋ก ์บ์ ์ค์ ์ด ๊ฐ๋ฅํ๋ค.
๐ ์คํ๋ง์ ์บ์ ์ถ์ํ ์ ์ฉ๋ฐฉ๋ฒ (@EnableCaching)
๊ฐ๋จํ๋ค. @Configuration ํด๋์ค์ @EnableCaching ๋ฅผ ์ถ๊ฐํ๊ณ , CacheManager ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ๋ ๋น์ ๋ฑ๋กํ๋ฉด ๋๋ค.
ํ๋ฒ ๋ ์ธ๊ธํ๋๋ฐ, ์คํ๋ง์ ์บ์ ์ถ์ํ ์ด๋
ธํ
์ด์
๋ค์ CacheManager ๋ฅผ ํตํด ์บ์๋ฅผ ์ฌ์ฉํ๋ค.
CacheManager ์ ์ค์ ๋ฐฉ๋ฒ์ ์บ์ ๊ตฌํ์ฒด๋ง๋ค ๋ค๋ฅผ ์ ์๋ค. ์ด๋ ๊ธ ํ๋จ์ ๋ค์ ์ค๋ช
ํ๋, ์ผ๋จ์ ๊ฐ๋ณ๊ฒ ๋ณด๊ณ ๋์ด๊ฐ์
@Configuration
@EnableCaching
class RedisCacheConfig {
// ์คํ๋ง์์ ์บ์ ์ถ์ํ๋ CacheManager ๊ฐ์ฒด๋ฅผ ํตํด ์๋ํ๋ค. ํด๋น ๋น์ ๋ฑ๋กํด์ฃผ์
@Bean
fun userCacheManager(objectMapper: ObjectMapper, redisProperties: RedisProperties): CacheManager {
// RedisClient ๊ตฌํ์ฒด๋ฅผ Lettuce ๋ก ๋ณ๊ฒฝ (์คํ๋ง-๋ถํธ-๋ ๋์ค์ ๊ธฐ๋ณธ ๊ตฌํ์ฒด : Jedis )
val redisConnectionFactory = LettuceConnectionFactory(redisProperties.host, redisProperties.port)
// Redis ์บ์์ค์
val redisConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer ()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer ()))
.disableCachingNullValues()
.entryTtl(Duration.ofHours(5L));
// ์คํ๋ง์์ ์ฌ์ฉํ CacheManager ๋น ์์ฑ (spring-boot-starter-data-redis)
return RedisCacheManager.RedisCacheManagerBuilder
.fromConnectionFactory(redisConnectionFactory)
.cacheDefaults(redisConfiguration)
.build()
}
}
๋ฌผ๋ก CacheManager ์ธํฐํ์ด์ค๋ฅผ ์ง์ ๊ตฌํํด๋ ๋์ง๋ง, ์คํ๋ง์์๋ ์ด๋ฏธ ์๋์ ๊ฐ์ด ๋ค์ํ ๊ตฌํ์ฒด๋ฅผ ์ ๊ณตํ๊ณ ์๋ค.
- ConcurrentMapCacheManager : ๊ฐ๋จํ๊ฒ ConcurrentHashMap์ ์ฌ์ฉํ๋ CocurrentMapCache ๋ฅผ ์ฌ์ฉํ๋ค.
- SimpleCacheManager : ํ๋กํผํฐ๋ฅผ ์ด์ฉํด์ ์ฌ์ฉํ ์บ์๋ฅผ ์ง์ ๋ฑ๋กํด์ฃผ๋ ๋ฐฉ๋ฒ(๊ธฐ๋ณธ์ ์ผ๋ก ์ ๊ณตํ๋ ์บ์๊ฐ ์๋ค)
์ด๋ ์คํ๋ง Cache ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํด์ ์บ์ ํด๋์ค๋ฅผ ์ง์ ๋ง๋๋ ๊ฒฝ์ฐ ํ ์คํธ์์ ์ฌ์ฉํ๊ธฐ์ ์ ๋นํ๋ค. - CompositeCacheManager : ํ๋ ์ด์์ ์บ์ ๋งค๋์ ๋ฅผ ์ฌ์ฉํ๋๋ก ์ง์ํด์ฃผ๋ ํผํฉ ์บ์ ๋งค๋์ ๋ค.
์ฌ๋ฌ ์บ์ ์ ์ฅ์๋ฅผ ๋์์ ์ฌ์ฉํด์ผ ํ ๋ ์ด๋ค. cacheManagers ํ๋กํผํฐ์ ์ ์ฉํ ์บ์ ๋งค๋์ ๋น์ ๋ชจ๋ ๋ฑ๋กํด์ฃผ๋ฉด ๋๋ค. - NoOpCacheManager : ์บ์๋ฅผ ์ฌ์ฉํ์ง ์๋๋ค. ์บ์๊ฐ ์ง์๋์ง ์๋ ํ๊ฒฝ์์ ๋์ํ ๋ ์บ์ ๊ด๋ จ ์ค์ ์ ์ ๊ฑฐํ์ง ์์๋ ์๋ฌ๊ฐ ๋์ง ์๊ฒ ํด์ค๋ค.
๊ทธ ์ธ ๋ง์ด ์ฌ์ฉํ๋ ์บ์์ ์ฅ์์ ๊ตฌํ์ฒด๊ฐ ์ด๋ฏธ ์กด์ฌํ๋ค.
- JCacheCacheManager: ์๋ฐ ํ์ค JSR107 ๊ธฐ๋ฐ์ผ๋ก ๋์ํ๋ ๊ตฌํ์ฒด๋ฅผ ์ํ ์บ์๋งค๋์
- RedisCacheManager : Redis ์ง์ํ๋ ์บ์๋งค๋์
- EhCacheCacheManager : EhCache ์ง์ํ๋ ์บ์๋งค๋์
- CaffeineCacheManager: Caffeine ์ง์ํ๋ ์บ์๋งค๋์
- ... ์๋ต
๐ ์บ์ ์ฌ์ฉ๋ฐฉ๋ฒ
์คํ๋ง ์บ์๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๋ฉ์๋ ๋จ์์ AOP๋ก ๊ตฌํ๋์ด ์๋ค.
๋ฉ์๋์ ์๋ ์ด๋ ธํ ์ด์ ๋ค์ ์ฌ์ฉํ์ฌ ๋ฉ์๋ ๋ฐํ๊ฐ์ ์บ์ฑํ ์ ์๋ค. ์บ์ฑ๋ ๊ฒฝ์ฐ ๋ฉ์๋๋ ์์ ์คํ๋์ง ์๋๋ค๋ ์ ์ ์ ์ํ์.
๐งจ @Transcational๊ณผ ๋์ผํ๊ฒ, @Cacheable์ด ๊ฑธ๋ ค์๋ ํด๋์ค ๋ด๋ถ ๋ฉ์๋๋ฅผ ํธ์ถ(Self-invocation)ํ๋ฉด AOP๊ฐ ๋์ํ์ง ์๋๋ค.
SPEL ๋ฌธ๋ฒ ( ๊ณต์๋ฌธ์ )
์กฐ๊ฑด์ ๋ฐ๋ผ ์บ์๋ฅผ ๋ค๋ฅด๊ฒ ๋์ํ๊ฒ ํ๊ฑฐ๋, ์บ์ key๋ฅผ ์ง์ ํ๊ณ ์ถ๋ค๋ฉด SPEL ๋ฌธ๋ฒ์ ์ฌ์ฉํ์ฌ ์ค์ ํ ์ ์๋ค.
์คํ๋ง ์บ์๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๋ฉ์๋์ ํ๋ผ๋ฉํ๋ฅผ ์บ์ key ๋ก ์ฌ์ฉํ๋ค. ( #๊ฐ, #๊ฐ์ฒด๋ช
.๋ณ์๋ช
, #๊ฐ์ฒด๋ช
.๋ฉ์๋()) ๋ก ์ฌ์ฉํ๋ฉด ๋๋ค.
// ํน์ ํ๋ผ๋ฏธํฐ๋ฅผ ํค๋ก ์ฌ์ฉ
@Cacheable(value="product", key="#productNo")
Product bestProduct(String productNo, User user, Date dateTime) {
}
// ํ๋ผ๋ฏธํฐ์ ํน์ ํ๋กํผํฐ ๊ฐ์ ํค๋ก ์ฌ์ฉ
@Cacheable(value="product", key="#condition.productNo")
Product bestProduct(SearchCondition condition) {
}
// ์ฌ์ฉ์์ ํ์
์ด ๊ด๋ฆฌ์์ธ ๊ฒฝ์ฐ์๋ง ์บ์๋ฅผ ์ ์ฉ (user.type ํ๋กํผํฐ๊ฐ "ADMIN"์ธ ๊ฒฝ์ฐ์๋ง ์บ์ ์ ์ฉ)
@Cacheable(value="user", condition="#user.type == 'ADMIN'")
public User findUser(User user) {
...
}
๋ง์ฝ ๋ฐํ๊ฐ์ด๋ ๋ฉ์๋์ ์ด๋ฆ๋ฑ ๋ค๋ฅธ ๊ฐ์ ์ฌ์ฉํ๊ณ ์ถ๋ค๋ฉด Spring Cache์ ์ฉ SPEL ๋ฌธ๋ฒ์ ์ฌ์ฉํ๋ฉด ๋๋ค.
@Cacheable
(์กฐ๊ฑด์ ๋ฐ๋ผ) ๋ฉ์๋ ๊ฒฐ๊ณผ๊ฐ์ ์บ์๋ก ์ ์ฅํ๋ค.
์ฌ๋ฌ ๊ฐ์ ์บ์ ์ด๋ฆ์ ๋ถ์ผ ์ ์์ผ๋ฉฐ, ์ต์ ํ๋ ์ด์์ cache hit์ด ์กด์ฌํ๋ค๋ฉด, ๋ฉ์๋๋ฅผ ์คํํ์ง ์๊ณ ํด๋น ์บ์ ๊ฐ์ ๋ฐ๋ก ๋ฐํํ๋ค.
@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
@Cacheable(cacheNames="books", key="#isbn.rawNumber")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
// unless = "..SPEL.." ์ ๊ฒฐ๊ณผ๊ฐ true์ด๋ฉด ์บ์ฑํ์ง ์๋๋ค.
@Cacheable(value="spittleCache" unless="#result.message.contains('NoCache')")
Spittle findOne(long id);
// condition = ".." ๊ฒฐ๊ณผ๊ฐ true ์ด๋ฉด ์บ์ฑํ๋ค.
// ๋ฌผ๋ก unless ์ condition์ ๋ ๋ค ์ ์ด๋ ๋๋ค.
@Cacheable(value="spittleCache" unless="#result.message.contains('NoCache')" condition="#id >= 10")
Spittle findOne(long id);
// ์ฐธ๊ณ ๋ก ์ฝํ๋ฆฐ์ฒ๋ผ ?. null-safety ๋ฌธ๋ฒ๋ ์ ๊ณตํด์ค๋ค ใ
@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result?.hardback")
public Optional<Book> findBook(String name)
@Cacheable(value="..") ๋ @Cacheable(cacheName="..") ๊ณผ ๊ฐ์ ๊ฐ์ด๋ค.
์บ์๋ฅผ ํ ์กฐ๊ฑด์ condition, ์บ์๋ฅผ ํ์ง ์์ ์กฐ๊ฑด์ unless ์ ์ ์ผ๋ฉด ๋๋ค. ํ์ํ๋ค๋ฉด ๋ ๋ค ์ ์ด๋ ๋๋ค.
๐งจ ์ฃผ์ - condition์ ๋ฉ์๋๋ฅผ ํธ์ถํ๊ธฐ ์ ์, unless๋ ๋ฉ์๋๋ฅผ ํธ์ถํ ๋ค์ ํ๊ฐํ๋ค. (๊ณต์๋ฌธ์)
@Cacheable(condition="..")์ ๋ฉ์๋๋ฅผ ํธ์ถํ๊ธฐ ์ ์ SPEL์ ์คํํด์ true์ธ์ง ํ์ธํ๋ค.
์ฆ ๋ฉ์๋์ ํ๋ผ๋ฉํ๊ฐ ์๋, ๋ฉ์๋์ ๋ฐํ๊ฐ (#result) ๋ฑ์ ์กฐ๊ฑด์ ์ฌ์ฉํ ์ ์๋ค. ์บ์ ์กฐ๊ฑด์ด ์ ์์ ์ผ๋ก ๋์ํ์ง ์๋๋ค.
๋ฐํ๊ฐ์ ์บ์ ํค๋ก ์ฌ์ฉํ๊ณ ์ถ์ ๊ฒฝ์ฐ, unless ์กฐ๊ฑด๋ง์ ์ฌ์ฉํด์ผํ๋ค.
๊ฟํ์ ํ๋ ์ฃผ์๋ฉด CacheableResult ๊ฐ์ ์ ์ฉ ๊ฐ์ฒด๋ฅผ ํ๋ ๋ง๋ค์ด์ ์ฌ์ฉํ๋ฉด cache hit ์ฌ๋ถ๋ ์ ์ ์๊ณ , SPEL ์กฐ๊ฑด๋ ๊น๋ํด์ง๋ค.
// ๋ด๊ฐ ๋ง๋ Cache์ฉ ๊ฐ์ฒด
class CacheableResult<T>(
val data: T,
val isFailed: Boolean = false // ๊ธฐ๋ณธ ๊ฐ์ isFailed=false ๋ก ์ค์
)
/* ๋ฉ์๋ ์คํ ํ๋ฅผ ํ๊ฐํ๊ณ ์ถ๋ค๋ฉด unless ๋ฅผ ์ฌ์ฉํด์ผํ๋ค. ์ด ๋ ์ด๋ฐ์์ผ๋ก ๊ตฌ์ฑํ๋ฉด ์ฝ๋ ๊ฐ๋
์ฑ์ด ์ข์์ง๋ค. */
@Cacheable(value = ["myCacheName"], unless = "#result.isFailed()")
fun getData(categoryId: String): CacheableResult<List<CategoryRes>> {
return CacheableResult(
data = data,
isFailed = true // ์ด๋ ๊ฒ ์บ์ ์ฌ๋ถ๋ฅผ ๋ช
์์ ์ผ๋ก ์ง์ ํ ์ ์๋ค.
)
}
@CacheEvict, @CachePut
- @CacheEvict : (์กฐ๊ฑด์ ๋ฐ๋ผ) ์บ์๋ฅผ ์ญ์ ํ๋ค. ์ฐธ๊ณ ๋ก Evict์ ์ซ์๋ด๋ค, ํด๊ฑฐํ๋ค๋ ์๋ฏธ์ ๋จ์ด์ด๋ค.
// bestProduct ์บ์์ ๋ด์ฉ์ ์ ๊ฑฐํ๋ค
@CacheEvict(value="bestProduct")
public void refreshBestProducts() {
// ...
}
// productNo์ ๊ฐ์ ํค๊ฐ์ ๊ฐ์ง ์บ์๋ฅผ ์ ๊ฑฐํ๋ค.
@CacheEvict(value="product", key="#product.productNo")
public void updateProduct(Product product) {
// ...
}
// ๊ทธ๋ฅ ํต์งธ๋ก ์ญ์ ํ๋ค.
@CacheEvict(value = "bestSeller", allEntires = true)
public void clearBestSeller() {
}
- @CachePut : ๋ฉ์๋ ์คํ์ ๋ฐฉํดํ์ง ์๊ณ , ์บ์๋ฅผ ์ ๋ฐ์ดํธํ๋ค. (๋ฉ์๋๋ ์ธ์ ๋ ์คํ๋๋ค.)
// ํน์ ์บ์๋ฅผ ์ญ์ ํ๊ณ ์ถ๋ค๋ฉด @CacheEvict ์ ์ฌ์ฉํ์.
@CacheEvict(value = ["product"], key = "#id")
fun updateProduct(long id, dto: UpdateProductDto) {
val product: Product = repository.findById(id)
product.update(
name = dto.name,
price = dto.price,
owner = dto.owner
)
}
// ๋ฉ์๋ ๊ฒฐ๊ณผ๊ฐ ์บ์๊ฐ ๋์์์๋ ๋ฉ์๋๋ฅผ ํญ์ ์คํ์ํค๊ณ ์ถ๋ค๋ฉด, @CachePut์ ์ฌ์ฉํ์
@CachePut(cacheNames = ["exampleStore"], key = "#cacheData.value", condition = "#cacheData.value.length() > 5")
fun updateCacheData(cacheData:CacheData){
return cacheData
}
@CacheConfig, @Caching
- @CacheConfig : ํด๋์ค ๋จ์๋ก ์บ์์ค์ ์ ํ ๋ ์ฌ์ฉํ๋ค. (ํด๋์ค ๋ด์ ๋ชจ๋ ๋ฉ์๋์ ์ค์ ์ด ์ ์ฉ๋๋ค.)
- @Caching : ์ฌ๋ฌ ๊ฐ์ ์บ์ฑ ๋์(@Cacheable, @CacheEvict, @CachePut)์ ํ๋๋ก ๋ฌถ์ ๋ ์ฌ์ฉํ๋ค.
@CacheConfig(cacheNames= "books", cacheManager="redisCacheManager")
public class CustomerDataService {
// ๋ฉ์๋์ ๋ฐ๋ก ์ ์ง์์๋, @CacheConfig์ ์ ์ ๋ด์ฉ์ด ํฌํจ๋๋ค.
@Cacheable // == @Cacheable(cacheName="books", cacheManager="redisCacheManager")
public String getAddress(Customer customer) {...}
}
@Caching(evict = {
@CacheEvict("addresses"),
@CacheEvict(value="directory", key="#customer.name") })
public String getAddress(Customer customer) { /* ... */ }
๐โ๏ธ @Cacheable(key="..")๋ ์๋ตํด๋ ์ฌ์ฉ์ด ๊ฐ๋ฅํ๊ธด ํฉ๋๋ค.
์ถ์ฒํ๋ ๋ฐฉ๋ฒ์ ์๋์ง๋ง, ํน์ ํ๋๋ฅผ key๋ก ์ฌ์ฉํ์ง ์๊ณ , ๊ทธ๋ฅ ์๋ตํด์ ์ฌ์ฉํ ์ ์๋ค.
// ๊น-๋
@Cacheable("books")
public Book findBook(ISBN isbn) { ... }
์ด ๊ฒฝ์ฐ ์คํ๋ง์ SimpleKeyGenerator์ ์ํด ์๋์ ๊ฐ์ด Cache Key๋ฅผ ์์ฑํ๋ค.
SimpleKeyGenreator, SimpleKey ์์ค์ฝ๋
์๋นํ ์ฌํํ๋ค. ์ด๊ฒ ์ ๋ถ์ด๋ค
SimpleKey๋ ์๋์ ๊ฐ์ด ๊ตฌํ๋์ด์๋ค.
- ๋ฉ์๋ ํ๋ผ๋ฉํ๊ฐ ์๋ ๊ฒฝ์ฐ : SimpleKey.empty()
- ๋ฉ์๋ ํ๋ผ๋ฉํ๊ฐ 1๊ฐ์ธ ๊ฒฝ์ฐ : ํด๋น ํ๋ผ๋ฉํ ๊ฐ(๊ทธ๋๋ก ๋ฐ์, SimpleKey๋ก ๊ฐ์ธ์ง ์๋๋ค)
- ๋ฉ์๋ ํ๋ผ๋ฉํ๊ฐ ์ฌ๋ฌ๊ฐ์ธ ๊ฒฝ์ฐ : SimpleKey(..ํ๋ผ๋ฉํ๋ค..) (* ๋ชจ๋ ํ๋ผ๋ฉํ์ hashcode ๊ฐ์ ์ด์ฉํด ํ๋์ ๋ณตํฉํค๋ก ๋ง๋ฆ)
์ถ์ฒํ๋ ๋ฐฉ๋ฒ์ ์๋์ง๋ง ์ด๋ KeyGenerator ์ธํฐํ์ด์ค๋ฅผ ์ง์ ๊ตฌํํ๊ณ ๋น์ผ๋ก ๋ฑ๋กํด์ ์ด๋ฅผ ๋ณ๊ฒฝํ ์ ์๋ค.
KeyGenerator ๊ตฌํ ์ฝ๋
@Bean
public KeyGenerator keyGenerator() {
return new CustomKeyGenerator();
}
public class CustomKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder keyBuilder = new StringBuilder();
keyBuilder.append(method.getName());
keyBuilder.append(SimpleKeyGenerator.generateKey(params));
return keyBuilder.toString();
}
}
* ์คํ๋ง์์ ์บ์ ํค๋ฅผ ๋น๊ตํ ๋, hashcode()๋ง ์ฌ์ฉํ๊ณ equals()๋ฅผ ์ฌ์ฉํ์ง ์์ต๋๋ค.
์๋ฐ์ hashcode๋ 32๋นํธ ๋ฉ๋ชจ๋ฆฌ ์ฃผ์๊ธฐ๋ฐ์ผ๋ก ์์ฑ๋ฉ๋๋ค. ์ฆ 64๋นํธ ์ด์์ฒด์ ์ฌ์ฉ ์ ์์ฃผ ๋ฎ์ ํ๋ฅ ๋ก ํด์์ถฉ๋์ด ๋ฐ์๊ฐ๋ฅํฉ๋๋ค. ํด๋น ์ด์(SPR-10237) ๋๋ฌธ์ ์คํ๋ง 4.0์์๋ DefaultKeyGenerator ๋ฅผ ์ ๊ฑฐํ๊ณ SimpleKeyGenerator ๋ก ๋ณ๊ฒฝํ์์ต๋๋ค.
๐โ๏ธ ์ด๋ ธํ ์ด์ ์ ์ปค์คํ ํด์ ์ฌ์ฉํ ์๋ ์์ต๋๋ค.
์ด๋ ์๋ ์คํ๋ง์์ ์ ๊ณตํ๋ ๊ธฐ๋ฅ์ด๊ธด ํฉ๋๋ค.
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Cacheable(cacheNames="books", key="#isbn")
public @interface SlowService {
}
@SlowService // == @Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
๐ CacheManager ๊ฐ ์ฌ๋ฌ๊ฐ ์ผ ๋ (์บ์ ์ ์ฅ์๋ฅผ ์ฌ๋ฌ ๊ฐ๋ฅผ ์ธ ๋)
@EnableCaching ์ผ๋ก ์คํ๋ง ์บ์๋ฅผ ํ์ฑํ ์ํค๋ฉด, CacheManager ํ์
์ ๋น์ ์กฐํํ์ฌ ๊ธฐ๋ณธ ๋งค๋์ ๋ก ๋ฑ๋กํฉ๋๋ค.
์ด ๋ ๋ฑ๋ก๋ CacheManager ๋น ํ์
์ด ์ฌ๋ฌ ๊ฐ๋ผ๋ฉด ์บ์ ํ์ฑํ ๋์ค ์๋ฌ๊ฐ ๋ฐ์ํฉ๋๋ค. ( NoUniqueBeanDefinitionException )
@Configuration
@EnableCaching
public class CacheConfig extends CachingConfigurerSupport {
@Override
@Bean // ex) EHCacheManager
public CacheManager cacheManager() { ... CacheManager A }
@Bean // ex) RedisCacheManager
public CacheManager bCacheManager() { ... CacheManager B }
}
ํด๊ฒฐ์ฑ 1. @Primary
์ถ์ฒํ๋ ๋ฐฉ๋ฒ์ ์๋์ง๋ง, ๋ญ ์คํ๋ง ์ปจํ
์ด๋์ @Primary๋ฅผ ์ฌ์ฉํ๋ฉด ํด๊ฒฐ๋๊ธด ํฉ๋๋ค.
@Configuration
@EnableCaching
public class MultipleCacheManagerConfig {
@Bean
@Primary
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager("customers", "orders");
cacheManager.setCaffeine(Caffeine.newBuilder()
.initialCapacity(200)
.maximumSize(500)
.weakKeys()
.recordStats());
return cacheManager;
}
@Bean
public CacheManager alternateCacheManager() {
return new ConcurrentMapCacheManager("customerOrders", "orderprice");
}
}
์ด์ ๊ธฐ๋ณธ ์บ์๋งค๋์ ๋ @Primary ๊ฐ ์ฌ์ฉ๋๊ณ , ํ์ํ ๋ ์๋์ ๊ฐ์ด ์บ์๋งค๋์ ๋น ์ด๋ฆ์ ๋ช ์ํด์ฃผ๋ฉด ๋ฉ๋๋ค.
@Cacheable(cacheNames = "customers") // @Primary CacheManager ์ฌ์ฉ๋จ
public Customer getCustomerDetail(Integer customerId) {
return customerDetailRepository.getCustomerDetail(customerId);
}
// ๋ค๋ฅธ ์บ์๋งค๋์ ์ฌ์ฉ๋จ
@Cacheable(cacheNames = "customerOrders", cacheManager = "alternateCacheManager")
public List<Order> getCustomerOrders(Integer customerId) {
return customerDetailRepository.getCustomerOrders(customerId);
}
ํด๊ฒฐ์ฑ 2(์ถ์ฒ). CachingConfigurerSupport
์คํ๋ง @Configuration์ ํด๋น Configurer ๊ฐ์ฒด๋ฅผ ์์๋ฐ์์ ๊ตฌํํ๋ฉด ๋ฉ๋๋ค. ์ด๋ ํด๊ฒฐ์ฑ
1๊ณผ ๋์ผํ๊ฒ ์ฌ์ฉ ๊ฐ๋ฅํฉ๋๋ค.
@Primary ๊ฐ ์๋๋ฐ๋ CacheManager ์ฌ๋ฌ ๊ฐ ๋ฑ๋ก์ ์๋ฌ๊ฐ ๋จ์ง ์๋ ์ด์ -> CacheResolver๊ฐ ์์ฑ๋์๊ธฐ ๋๋ฌธ
์ด๋ @EnableCaching์ ์ฌ์ฉํ์ ๋, ์คํ๋ง ์ปจํ
์ด๋๋ฅผ ๋๋ฒ๊น
ํด๋ณด๋ฉด ์ฝ๊ฒ ์ ์ ์์ต๋๋ค.
์คํ๋ง CacheManager๋ฅผ ๋ฑ๋กํ ๋, ๋ง์ฝ CacheResolver๊ฐ ์กด์ฌํ๋ค๋ฉด ๊ธฐ๋ณธ CacheManager๋ฅผ ์กฐํ-๋ฑ๋กํ์ง ์์ต๋๋ค.
CacheResolver ๋ ์กฐ๊ฑด์ ๋ฐ๋ผ ๋ค๋ฅธ ์บ์๋งค๋์ ๋ฅผ ๋ฑ๋กํ๊ณ ์ถ์ ๋, ์ฌ์ฉ์๊ฐ ํด๋น ์ธํฐํ์ด์ค๋ฅผ ์ปค์คํ
ํด์ ๋ง๋ค ์ ์์ต๋๋ค.
CachingConfigurerSupport ๋ฅผ ์์๋ฐ์ผ๋ฉด, @Configuration ๊ฐ์ฒด๊ฐ ์คํ๋ง ๋น์ผ๋ก ๋ฑ๋ก๋ ๋ Configurer๊ฐ ๋์ํ๋ฉด์ CacheResolver๋ฅผ ์ถ๊ฐํฉ๋๋ค.
์กฐ๊ธ ๋ ์ ํํ๋ ์๋์ ๊ฐ์ ๊ณผ์ ์ ๊ฑฐ์ณ์ ์คํ๋ง ์ธํฐ์
ํฐ๋ก CacheResolver๊ฐ ์์ฑ๋๋๋ฐ, ๊ทธ๋ฅ ๋์ด๊ฐ์๋ค ใ
ใ
- ProxyCachingConfiguration ์ค์ ํด๋์ค๊ฐ ์คํ๋๋ ๊ณผ์ ์ค์ ์์ ํด๋์ค์ธ AbstractCachingConfiguration ์ ์๋ setConfigurers ๋ฉ์๋๊ฐ ์คํ์ด ๋๋ค.
- setConfigurers ๋ฉ์๋์์ useCachingConfigurer ๋ฉ์๋๋ฅผ ํธ์ถํ๋๋ฐ, ์ด ๋ ํ๋ผ๋ฏธํฐ๋ก ๋ฐ์ CachingConfigurer ๊ฐ์ฒด ์์ cacheManager ๋ฅผ ๊ฐ์ ธ์์ this.cacheManager ์ ์ฃผ์ ํ๋ค.
- ProxyCachingConfiguration ์ค์ ํด๋์ค์ ์๋ BeanFactoryCacheOperationSourceAdvisor Bean์ด ๋ฑ๋ก๋๋ ๊ณผ์ ์์ CacheInterceptor Bean์ด ์ถ๊ฐ๋ก ๋ฑ๋ก๋๋ค.
- CacheInterceptor Bean์ด ๋ฑ๋ก๋๋ ๊ณผ์ ์์ cacheResolver๊ฐ ๋ฑ๋ก์ด ๋๋ค.
- cacheResolver๊ฐ ๋ฑ๋ก์ด ๋์๊ธฐ ๋๋ฌธ์ getCacheResolver ๋ฉ์๋๊ฐ null์ด ์๋๋ฏ๋ก Bean์ ์ฐพ์ง ์๊ฒ ๋๊ณ ์๋ฌ๊ฐ ๋์ง ์๊ฒ ๋๋ ๊ฒ์ด๋ค.
@Configuration
@EnableCaching
public class MultipleCacheManagerConfig extends CachingConfigurerSupport {
// CachingConfigurerSupport.cacheManager()๋ฅผ ์์ํ์ ใ
@Primary ์๋ต๊ฐ๋ฅ
@Override
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager("customers", "orders");
cacheManager.setCaffeine(Caffeine.newBuilder()
.initialCapacity(200)
.maximumSize(500)
.weakKeys()
.recordStats());
return cacheManager;
}
@Bean
public CacheManager alternateCacheManager() {
return new ConcurrentMapCacheManager("customerOrders", "orderprice");
}
}
ํด๊ฒฐ์ฑ 3. CompositeCacheManager ์ฌ์ฉํ๊ธฐ
์คํ๋ง CacheManager ๊ตฌํ์ฒด์ค์๋, ์์ ์บ์๋งค๋์ ๋ฅผ ์ฌ๋ฌ ๊ฐ ๋ฑ๋กํ ์ ์๋ ๊ตฌํ์ฒด๋ ์์ต๋๋ค.
@Bean
public CacheManager cacheManager(net.sf.ehcache.CacheManager cm, javax.cache.CacheManager jcm) {
CompositeCacheManager cacheManager = new CompositeCacheManager();
List<CacheManager> managers = new ArrayList<CacheManager>();
managers.add(new JCacheCacheManager(jcm));
managers.add(new EhCacheCacheManager(cm))
managers.add(new RedisCacheManager(redisTemplate()));
cacheManager.setCacheManagers(managers); // ๊ฐ๋ณ ์บ์ ๋งค๋์ ์ถ๊ฐ
return cacheManager;
}
์ด๋ ๊ถ์ฅํ๋ ๋ฐฉ๋ฒ์ ์๋๋๋ค๋ง, ์์๋๋ฉด ํตํฉํ
์คํธ ํ ๋ ์ ์ฉํฉ๋๋ค.
์๋ฅผ ๋ค์ด ๋ฉ์๋์์ ์์ฒญํ ์บ์๋งค๋์ ๊ฐ ๋ฑ๋ก๋์ง ์์๋ค๋ฉด NoOpCacheManager๊ฐ ์คํ๋๋๋ก ๋ง๋ค ์ ์์ต๋๋ค.
@TestConfiguration
public class TestConfig {
@Autowired
private CacheManager jdkCache; // ์์
@Autowired
private CacheManager guavaCache; // ์์
@Bean
@Primary
public CacheManager cacheManager() {
CompositeCacheManager manager = new CompositeCacheManager(jdkCache, guavaCache);
// CompositeCacheManager์ ํฌํจ๋ CacheManager ์ธ
// ๋ชจ๋ ์บ์ ์์ฒญ์ NoOpCache๋ก ๋ณด๋ (์ฌ๊ธฐ์์ jdkCache, guavaCache ์ธ์ ๋ชจ๋ ์บ์)
// NoOpCache๋ ์บ์๊ฐ ์๋ ๊ฒ๊ณผ ๋์ผํ๊ฒ ๋์
manager.setFallbackToNoOpCache(true);
return manager;
}
}
ํด๊ฒฐ์ฑ 4(์ถ์ฒ). CacheResolver ์ฌ์ฉํ๊ธฐ
์คํ๋ง 4.1 ๋ถํฐ๋ @Cacheable์ ์๋ฌด๋ฐ ์ค์ ๊ฐ์ ์ ์ง ์๋๋ผ๋ ์บ์๋งค๋์ ๋ฅผ ์์์ ์ฐพ์์ค๋๋ค.
์ด๋ cacheManager=".." ๊ฐ์ด ์๋ค๋ฉด ๊ธฐ๋ณธ์ผ๋ก ๋ฑ๋ก๋ SimpleCacheResolver๊ฐ ๋์ํ์ฌ CacheManager๋ฅผ ์ฐพ์์ฃผ๊ธฐ ๋๋ฌธ์ธ๋ฐ, ์ฐ๋ฆฌ๋ ์ด๋ฅผ ์ปค์คํ
ํด์ ์ฌ์ฉํ ์ ์์ต๋๋ค.
์๋ฅผ ๋ค์ด ๋ฉ์๋ ์ด๋ฆ์ ๋ฐ๋ผ ๋ค๋ฅธ ์บ์๊ฐ ๋์ํ๋๋ก ํ๊ณ ์ถ๋ค๋ฉด, ์๋์ ๊ฐ์ด ๊ตฌํํ ์ ์์ต๋๋ค.
public class MultipleCacheResolver implements CacheResolver {
private final CacheManager simpleCacheManager;
private final CacheManager caffeineCacheManager;
private static final String ORDER_CACHE = "orders";
private static final String ORDER_PRICE_CACHE = "orderprice";
public MultipleCacheResolver(CacheManager simpleCacheManager,CacheManager caffeineCacheManager) {
this.simpleCacheManager = simpleCacheManager;
this.caffeineCacheManager=caffeineCacheManager;
}
// resolveCaches ๋ฉ์๋๋ฅผ ์ฌ์ ์ํ๋ฉด ๋๋ค.
@Override
public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) {
Collection<Cache> caches = new ArrayList<Cache>();
// ํด๋น ์ฝ๋๋ context ํ๋ผ๋ฉํ๋ฅผ ์ด์ฉํด ํด๋น ๋ฉ์๋ ์ด๋ฆ์ ๋ฐ์์์ ๋น๊ตํ๋ค.
if ("getOrderDetail".equals(context.getMethod().getName())) {
caches.add(caffeineCacheManager.getCache(ORDER_CACHE));
} else {
caches.add(simpleCacheManager.getCache(ORDER_PRICE_CACHE));
}
return caches;
}
}
๊ทธ๋ฆฌ๊ณ ์ด๋ฅผ ํด๊ฒฐ์ฑ
3๊ณผ ๋์ผํ๊ฒ ๊ธฐ๋ณธ CacheResolver๋ก ๋ฑ๋กํด์ฃผ๋ฉด ๋ฉ๋๋ค.
CacheResolver๊ฐ ๋ฑ๋ก ๋ ๊ฒฝ์ฐ, @Cacheable์ ์ค์ ๊ฐ์ด ์์ ๋ CacheResolver๋ฅผ ๊ฑฐ์ณ์ ์บ์ ๋งค๋์ ๋ฅผ ์ฐพ๊ฒ๋ฉ๋๋ค.
@Configuration
@EnableCaching
public class MultipleCacheManagerConfig extends CachingConfigurerSupport {
@Override
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager("customers", "orders");
cacheManager.setCaffeine(Caffeine.newBuilder()
.initialCapacity(200)
.maximumSize(500)
.weakKeys()
.recordStats());
return cacheManager;
}
@Bean
public CacheManager alternateCacheManager() {
return new ConcurrentMapCacheManager("customerOrders", "orderprice");
}
// ์ด์ CacheManager๋ฅผ ๋ช
์ํ์ง ์์ผ๋ฉด ํด๋น CacheResolver๊ฐ ๋์ํ์ฌ ์ฐพ์์ค๋ค.
@Override
@Bean
public CacheResolver cacheResolver() {
return new MultipleCacheResolver(alternateCacheManager(), cacheManager());
}
}
@Cacheable( .. ์ค์ ..) ์์
1. ์ค์ ์ ๋ค ์๋ตํ๋๋ฐ ๋ฑ๋ก๋ CacheResovler๊ฐ ์์ผ๋ฉด ๊ธฐ๋ณธ CacheManager๋ฅผ ์ฌ์ฉํฉ๋๋ค.
2. ์ค์ ์ ๋ค ์๋ตํ๋๋ฐ ๋ฑ๋ก๋ CacheResolver๊ฐ ์๋ค๋ฉด, CacheResolver๊ฐ ๋์ํฉ๋๋ค.
3. ํน์ CacheManager๋ฅผ ์ฌ์ฉํ๊ณ ์ถ๋ค๋ฉด, @Cacheable(cacheManager="...")๋ก ๋ช ์ํด์ฃผ๋ฉด ๋ฉ๋๋ค.
4. ํน์ CacheResolver๋ฅผ ์ฌ์ฉํ๊ณ ์ถ๋ค๋ฉด, @Cacheable(cacheResolver="...")๋ก ๋ช ์ํด์ฃผ๋ฉด ๋ฉ๋๋ค.
@Cacheable(cacheNames = "orders", cacheResolver = "cacheResolver")
public Order getOrderDetail(Integer orderId) {
return orderDetailRepository.getOrderDetail(orderId);
}
@Cacheable(cacheNames = "orderprice", cacheResolver = "cacheResolver")
public double getOrderPrice(Integer orderId) {
return orderDetailRepository.getOrderPrice(orderId);
}
์ฌ๊ธฐ์ ๋ฌธ์ ๋ @Cacheable( cacheManager="..", cacheResovler="..") ์ด๋ ๊ฒ ๋ ๋ค ์ ์์ ๋ ๋ฌด์์ด ๋์ํ ์ง ๋ชจ๋ฅธ๋ค๋๊ฑด๋ฐ,
์ด ๊ฒฝ์ฐ์๋ ์บ์ ์ด๊ธฐํ(@EnableCaching) ์์ ์ ์คํ๋ง ์ปจํ ์ด๋๊ฐ ์์ธ๋ฅผ ๋์์ค๋๋ค. ๋ ์ค ํ๋๋ง ์ ์ด์ผํฉ๋๋ค.
์น์ ํ๊ฒ ํด๋น ์ด๋ ธํ ์ด์ ์ด ์ฌ์ฉ๋ ์์น์, ์ด๋ ๊ฒ ์ค์ ์ ๋ ๋ค ์ ์ผ๋ฉด ์๋๋ค๊ณ ์์ธ ๋ฉ์์ง๋ก ์๋ ค์ค๋๋ค.
๐ Redis
๋ ๋์ค๋ Spring-Data-Redis ํ๋ก์ ํธ๊ฐ ๋ณ๋๋ก ์กด์ฌํด์, ์คํ๋ง์์ ์ค์ ์ ์ฝ๊ฒ ํ ์ ์๋ค.
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
// ์คํ๋ง5 webflux ๊ธฐ๋ฐ์์ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ ์ถ๊ฐ (์์ง spring-data-redis์ ํตํฉ๋์ง ์์)
implementation 'org.springframework.boot:spring-boot-starter-data-redis-reactive'
Spring-Data-Redis์์๋ Redis Client๋ก Lettuce์ Jedis๋ฅผ ์ ๊ณตํด์ค๋ค.
2022๋
๊ธฐ์ค ์ค์ ์ ํธ์์ฑ์ด๋, ์ฑ๋ฅ, ์
๋ฐ์ดํธ ๋ชจ๋ ๊ฒ์ด Lettuce๊ฐ ๋ ์ข๊ธฐ ๋๋ฌธ์, ์ด๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ ๊ถ์ฅํ๋ค (๊ด๋ จ ๊ธ)
RedisTemplate
๋ ๋์ค๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด์ RedisTemplate ๊ฐ์ฒด๋ฅผ ๋น์ผ๋ก ๋ฑ๋กํด์ฃผ์ด์ผํ๋ค.
๊ฐ์ข
์ค์ ๊ฐ๋ค์ RedisTemplate์ ์ง์ ์ฝ๋๋ก ๋ฃ์ด๋ ๋์ง๋ง, ์๋์ ๊ฐ์ ์ค์ ๊ฐ์ผ๋ก ๋ฐ๋ก ๊ด๋ฆฌํ๋๊ฒ ์ข๋ค.
Spring Boot Data ๊ด๋ จ ํ๋กํผํฐ ์ ๋ฆฌ๊ธ
// application.yml
spring:
redis:
host: localhost
port: 6379
spring.redis.database | 0 | ์ปค๋ฅ์ ํฉํ ๋ฆฌ์ ์ฌ์ฉ๋๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ธ๋ฑ์ค |
spring.redis.host | localhost | ๋ ๋์ค ์๋ฒ ํธ์คํธ |
spring.redis.password | ๋ ๋์ค ์๋ฒ ๋ก๊ทธ์ธ ํจ์ค์๋ | |
spring.redis.pool.max-active | 8 | pool์ ํ ๋น๋ ์ ์๋ ์ปค๋ฅ์ ์ต๋์ (์์๋ก ํ๋ฉด ๋ฌด์ ํ) |
spring.redis.pool.max-idle | 8 | pool์ "idle" ์ปค๋ฅ์ ์ต๋์ (์์๋ก ํ๋ฉด ๋ฌด์ ํ) |
spring.redis.pool.max-wait | -1 | pool์ด ๋ฐ๋ฅ๋ฌ์ ๋ ์์ธ๋ฐ์ ์ ์ ์ปค๋ฅ์ ํ ๋น ์ฐจ๋จ์ ์ต๋ ์๊ฐ (๋จ์: ๋ฐ๋ฆฌ์ธ์ปจ๋, ์์๋ ๋ฌด์ ํ ์ฐจ๋จ) |
spring.redis.pool.min-idle | 0 | ํ์์ ๊ด๋ฆฌํ๋ idle ์ปค๋ฅ์ ์ ์ต์ ์ ๋์ (์์์ผ ๋๋ง ์ ํจ) |
spring.redis.port | 6379 | ๋ ๋์ค ์๋ฒ ํฌํธ |
spring.redis.sentinel.master | ๋ ๋์ค ์๋ฒ ์ด๋ฆ | |
spring.redis.sentinel.nodes | ํธ์คํธ:ํฌํธ ์ ๋ชฉ๋ก (์ฝค๋ง๋ก ๊ตฌ๋ถ) | |
spring.redis.timeout | 0 | ์ปค๋ฅ์ ํ์์์ (๋จ์: ๋ฐ๋ฆฌ์ธ์ปจ๋) |
RedisProperties ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ๋๋ฐ, ์ด๋ ๋ณ๊ฑด ์๋๊ณ application.yml ์ค์ ๊ฐ์ ๋ถ๋ฌ์ค๋ ํธ์ ๊ฐ์ฒด์ด๋ค.
// org.springframework.boot.autoconfigure.data.redis.RedisProperties
@ConfigurationProperties(prefix="spring.redis")
public class RedisProperties {...}
// ๋ฌผ๋ก RedisProperties ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ์ง ์์๋ ์ค์ ํ์ผ์ ์ฝ์ ์ ์๋ค.
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
์ด ์ค์ ๊ฐ์ ์ด์ฉํ์ฌ RedisTemplate ๋น์ ๊ตฌ์ฑํด์ฃผ๋ฉด ๋๋ค.
@Configuration
public class RedisConfig {
private final RedisProperties redisProperties;
@Bean // Redis์ Connection์ ์์ฑํ๋ ๊ฐ์ฒด๋ฅผ ์ ์ํ๋ค. ์ฌ๊ธฐ์์๋ Lettuce Redis Client ์ฌ์ฉ
public RedisConnectionFactory redisConnectionFactory(){
return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort());
}
@Bean // ์คํ๋ง์์ ์ ๊ณตํ๋ RedisTemplate์ ์ ์ํ๋ค. ์์์ ์ ์ํ ConnectionFactory๋ฅผ ์ฌ์ฉํ๋ค
public RedisTemplate<String, Object> redisTemplate(){
RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
//redisTemplate.setKeySerializer(new StringRedisSerializer()); //key serializer
//redisTemplate.setValueSerializer(new StringRedisSerializer()); //value serializer
//redisTemplate.setHashKeySerializer(new StringRedisSerializer());
//redisTemplate.setHashValueSerializer(new StringRedisSerializer());
// ์์ ์ค์ ๋ค์ ํ๋ฒ์ ์ค์ ํ๊ณ ์ถ๋ค๋ฉด DefaultSerializer ๋ฅผ ๋ฑ๋กํด์ฃผ๋ฉด ๋๋ค
//redisTemplate.setDefaultSerializer(new StringRedisSerializer());
return redisTemplate;
}
}
Redis Serializer ์ปค์คํ
์ฐธ๊ณ ๋ก Redis๋ Serializer๋ฅผ ๋ณ๋๋ก ๋ฑ๋กํ์ง ์๋ ๊ฒฝ์ฐ, key-value๋ฅผ ๋ ๋ค Byte[] ์ผ๋ก ์ ์ฅํด๋ฒ๋ฆฐ๋ค.
RedisTemplate์ ๊ธฐ๋ณธ๊ฐ์ JdkSerializationRedisSerializer ์ด๋ค.
- ์ด๋ ์๋ฐ์ Serializable ๊ฐ์ฒด๋ฅผ ๊ตฌํํด์ผ๋ง ์ง๋ ฌํ๊ฐ ๊ฐ๋ฅํ๋ค.
- ๊ธฐ๋ณธ๊ฐ์ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ ์๋ฐ๊ฐ ์๋ ๋ค๋ฅธ ์ธ์ด์์๋ Redis ์ ์ฅ์์ ๊ฐ์ ์ฝ์ ์ ์๋ค
- ์ธ๋ชจ ์๋ ์๋ฐ์ ๊ธฐ๋ณธ Object class์ ์ ๋ณด๊น์ง Redis์ ์ ์ฅํด์ ๋นํจ์จ์ ์ด๋ค.
๊ทธ๋์ ๋ณดํต ์๋ 3๊ฐ์ง Serializer๋ฅผ ๋ง์ด ์ฌ์ฉํ๋ค.
1. GenericJackson2JsonRedisSerializer
๋ณ๋์ ํด๋์ค ํ์
์ ์ง์ ํด์ฃผ์ง ์์๋, ๊ฐ์ฒด๋ฅผ Json ์ผ๋ก ์ง๋ ฌํํด์ค๋ค.
๋ค๋ง @class ์ Class ์ ์ ๊ฒฝ๋ก๋ฅผ ๋ฐ์๋ฒ๋ ค์, ๋ฐ์ดํฐ๋ฅผ ๊บผ๋ด์ฌ ๋ ๊ฐ์ ๋๋ ํ ๋ฆฌ์ ํด๋น DTO๊ฐ ๋ฐ๋์ ์กด์ฌํด์ผํ๋ ๋จ์ ์ด ์๋ค.
2. Jackson2JsonRedisSerializer
ํน์ ํด๋์ค ํ์ ์ ๋ช ์ํด์ค์ผํ๋ค. ๊ทธ ๋์ @class ์ ๋ณด๋ฅผ ๋ฐ์ดํฐ์ ํฌํจํ์ง ์๋๋ค.
๋ค๋ง ํด๋์ค ํ์ ๋ณ๋ก Serializer๋ฅผ ๋ณ๋๋ก ์์ฑํด์ค์ผํ๋๋ฐ, cacheDto ๊ฐ์ง๋ API๋ค์ด ๊ฐ์ ๊ณ ์ ํ RedisTemplate์ ๋ฐ๋ก ๋ค๊ณ ์์ด์ผํ๋ค.. ใ
redisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer(value.getClass()));
3. StringRedisSerializer + ObjectMapper
SpringBoot-Redis์ ํฌํจ๋ StringRedisSerializer๋ ๊ทธ๋ฅ ๋ชจ๋ key-value๋ฅผ ๋ฌธ์์ด๋ก ์ ์ฅํ๋ค.
๊ทธ๋ฆฌ๊ณ ๊ฐ(value)์ค์ ๋ฌธ์์ด๋ก ๋ณํ์ด ๋ถ๊ฐ๋ฅํ ๊ฐ์ฒด๋ฅผ ์ ์ฅํ ๋, ์๋์ ๊ฐ์ด ObjectMapper๋ฅผ ์ปค์คํ ํ์ฌ ์ฌ์ฉํ๋ค.
@Configuration
class RedisConfig {
@Bean
fun redisConnectionFactory(redisProperties: RedisProperties): RedisConnectionFactory {
return LettuceConnectionFactory(redisProperties.host, redisProperties.port)
}
@Bean
fun redisObjectMapper(): ObjectMapper = ObjectMapper()
.registerModule(kotlinModule())
.registerModule(JavaTimeModule())
.activateDefaultTyping(
BasicPolymorphicTypeValidator.builder().allowIfBaseType(Any::class.java).build(),
ObjectMapper.DefaultTyping.EVERYTHING
)
@Bean
fun redisTemplate(redisConnectionFactory: RedisConnectionFactory): RedisTemplate<String, Any?> {
val redisTemplate = RedisTemplate<String, Any?>()
redisTemplate.setConnectionFactory(redisConnectionFactory)
redisTemplate.keySerializer = StringRedisSerializer()
// GenericJackson2JsonRedisSerializer์ ObjectMapper๋ฅผ ์ปค์คํ
redisTemplate.valueSerializer = GenericJackson2JsonRedisSerializer(redisObjectMapper())
return redisTemplate
}
}
Redis ์ฌ์ฉํ๊ธฐ 1 - RedisTemplate ์ง์ ์ฌ์ฉ
์ฐธ๊ณ ๋ก ๋ ๋์ค๋ ๋จ์ ์คํธ๋ง ๋ฟ ์๋๋ผ, ์ฌ๋ฌ๊ฐ์ง ์๋ฃ๊ตฌ์กฐ๋ฅผ ์ ๊ณตํด์ฃผ๋๋ฐ ์ด๋ redisTemplate์ ์๋ ๋ฉ์๋๋ก ์ฌ์ฉํ ์ ์๋ค.
// ๋์ถฉ ์ด๋ ๊ฒ ์ฌ์ฉํ๋ฉด ๋๋ค.
object CacheUtil {
fun setValues(
redisTemplate: RedisTemplate<String, Any?>,
prefix: String,
key: String,
data: String,
timeout: Long,
timeUnit: TimeUnit
) {
redisTemplate.opsForValue().set("$prefix::$key", data, timeout, timeUnit)
}
fun getValuesOrNull(redisTemplate: RedisTemplate<String, Any?>, prefix: String, key: String): Any? {
return redisTemplate.opsForValue()["$prefix::$key"]
}
fun deleteValues(redisTemplate: RedisTemplate<String, Any?>, prefix: String, key: String) {
redisTemplate.delete("$prefix::$key")
}
fun getRedisTtl(redisTemplate: RedisTemplate<String, Any?>, prefix: String, key: String): Long? {
return redisTemplate.getExpire("$prefix::$key")
}
fun setRedisTtl(redisTemplate: RedisTemplate<String, Any?>, prefix: String, key: String, timeout: Long) {
redisTemplate.expire("$prefix::$key", Duration.ofSeconds(timeout))
}
}
Redis ์ฌ์ฉํ๊ธฐ2 - RedisRepositories (๋น์ถ์ฒ)
๊ถ์ฅํ๋ ๋ฐฉ๋ฒ์ ์๋์ง๋ง, Redis๋ฅผ ๋ง์น JPA์ฒ๋ผ ์ฌ์ฉํ ์ ์๋ค. ์ฐธ๊ณ ๋ก ๋ด๋ถ์ ์ผ๋ก RedisTemplate์ ์ฌ์ฉํ๋๊ฑด ๋์ผํ๋ค.
@Configuration
@EnableRedisRepositories // ์ถ๊ฐ
public class RedisConfig {
/*.. ์ดํ ๋์ผ .. */
}
@RedisHash(value = "refreshToken")
public class RefreshToken {
@Id // org.springframework.data.annotation.Id
private String userId;
private String refreshToken;
@TimeToLive
private long expiredTime;
}
// Spring.Data์ CrudRepository๋ฅผ ์์๋ฐ์ Repository ์ธํฐํ์ด์ค๋ฅผ ์ ์ํ๋ค.
public interface RefreshTokenRedisRepository extends CrudRepository<RefreshToken, String> {
}
// ๋ง์น JPA ์ฒ๋ผ ์ฌ์ฉํ ์ ์๋ค.
@Service
public class AdminUserRedisService {
private final RefreshTokenRedisRepository refreshTokenRedisRepository;
@Override
public RefreshToken save(RefreshToken adminUserToken) {
return refreshTokenRedisRepository.save(adminUserToken);
}
@Override
public RefreshToken findById(String userId) {
return refreshTokenRedisRepository.findById(userId).get();
}
}
@SpringBootTest
class AdminUserRedisServiceTest {
@Autowired
private AdminUserRedisService adminUserRedisService;
@DisplayName("token redis ์ ์ฅ success")
@Test
void tokenSave(){
//given
RefreshToken token = RefreshToken.builder()
.userId("test")
.refreshToken("test_token")
.expiredTime(60)
.build();
//when
RefreshToken refreshToken = adminUserRedisService.save(token);
//then
RefreshToken findToken = adminUserRedisService.findById(token.getUserId());
assertEquals(refreshToken.getRefreshToken(), findToken.getRefreshToken());
}
}
Redis ์ฌ์ฉํ๊ธฐ3 - ์คํ๋ง ์บ์ ์ถ์ํ CacheManager
RedisTemplate์ ์์ฑํ๋ ๊ณผ์ ์ ์์ ๋์ผํ๋ค. ๋ณดํต ๊ด๋ฆฌํ๊ธฐ ํธํ๊ฒ RedisConfig๋ฅผ ๋ณ๋๋ก ๋ถ๋ฆฌํด์ ์ง์ ํ๊ณค ํ๋ค.
ํ์ ํ๋ ์ฃผ์๋ฉด, @ConditionalOnProperty ์ด๋
ธํ
์ด์
์ ์ด์ฉํด yml ์ค์ ์ redis ์ ๋ณด๊ฐ ์์๋๋ง ๋ฑ๋กํ๊ฒ ํ ์ ์๋ค.
์คํ๋ง๋ถํธ์ ์กฐ๊ฑด๋ถ Configuration
@Configuration // spring.cache.type: redis ์ผ ๋ ํด๋น ์ค์ ์ ๋ฑ๋กํ๋ค, ๋ง์ฝ ์ค์ ๊ฐ์ด ์์ ์๋ ๊ฒฝ์ฐ ์์ฑํ์ง ์๋๋ค.
@ConditionalOnProperty(name = ["spring.cache.type"], havingValue = "redis", matchIfMissing = false)
class RedisConfig {
@Bean
fun redisConnectionFactory(redisProperties: RedisProperties): RedisConnectionFactory {
return LettuceConnectionFactory(redisProperties.host, redisProperties.port)
}
@Bean // ์ฐธ๊ณ ๋ก ๊ทธ๋ฅ String์ผ๋ก ์ ์ฅํ ๊ฑฐ๋ฉด, objectMapper๋ฅผ ์ปค์คํ
ํ ํ์๋ ์๋ค ใ
fun redisObjectMapper(): ObjectMapper = ObjectMapper()
.registerModule(kotlinModule())
.registerModule(JavaTimeModule())
.activateDefaultTyping(
BasicPolymorphicTypeValidator.builder().allowIfBaseType(Any::class.java).build(),
ObjectMapper.DefaultTyping.EVERYTHING
)
@Bean
fun redisTemplate(redisConnectionFactory: RedisConnectionFactory): RedisTemplate<String, Any?> {
val redisTemplate = RedisTemplate<String, Any?>()
redisTemplate.setConnectionFactory(redisConnectionFactory)
redisTemplate.keySerializer = StringRedisSerializer()
// GenericJackson2JsonRedisSerializer์ ObjectMapper๋ฅผ ์ปค์คํ
redisTemplate.valueSerializer = GenericJackson2JsonRedisSerializer(redisObjectMapper())
return redisTemplate
}
}
์ด๋ ๊ฒ ์์ฑํ Redis ๊ฐ์ฒด๋ค์ ์ด์ฉํ์ฌ CacheManager๋ฅผ ๋ฑ๋กํด์ฃผ๋ฉด ๋๋ค.
@Bean // RedisCacheManager ๋น
public CacheManager cacheManager(RedisTemplate redisTemplate) {
return new RedisCacheManager(redisTemplate); // Redisํ
ํ๋ฆฟ์ ์ธ์คํด์ค๋ฅผ ์์ฑ์์ ์ ๋ฌํ์ฌ, ์์ฑ
}
์ฐธ๊ณ ๋ก CacheManager๋ง ์ฌ์ฉํ ๊ฒฝ์ฐ, RedisTemplate๋ฅผ ๊ผญ ๋ง๋ค ํ์๋ ์๋ค. ์๋์ ๊ฐ์ด ๋น๋๋ก RedisTemplate ์ ์ค์ ๊ณผ ๋น์ทํ๊ฒ ๋ง๋ค ์ ์๋ค. (RedisCacheConfiguration)
@Configuration
@EnableCaching
class CacheConfig {
// spring.cache.type: redis ๊ฐ ์๋๋ผ๋ฉด, ์์ฑํ์ง ์๋๋ค. (์์ ๊ฐ์ด ์์ด๋ ์์ฑํ์ง ์๋๋ค)
@ConditionalOnProperty(name = ["spring.cache.type"], havingValue = "redis", matchIfMissing = false)
@Bean
fun cacheManager(
redisConnectionFactory: RedisConnectionFactory,
objectMapper:ObjectMapper
): CacheManager {
val redisConfiguration:RedisCacheConfiguration = RedisCacheConfiguration
// ํด๋น ์บ์๋งค๋์ ๊ฐ ์ฌ์ฉํ ๊ธฐ๋ณธ ์ค์
.defaultCacheConfig()
// 1. ์บ์ ์ ํจ๊ธฐ๊ฐ(Time to live) ์ค์
.entryTtl(Duration.ofSeconds(3600))
// 2. ์บ์ ์ด๋ฆ์ prefix:: ๋ฅผ ์๋์ผ๋ก ๋ถ์ฌ์ค.
.computePrefixWith(CacheKeyPrefix.simple())
// 3. key ์ง๋ ฌํ์ StringRedisSerializer ์ฌ์ฉ
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer()))
// 4. value ์ง๋ ฌํ์ GenericJackson2JsonRedisSerializer(์ปค์คํ
ํ ObjectMapper) ์ฌ์ฉ
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
GenericJackson2JsonRedisSerializer(objectMapper)
)
)
// ์์์ ์์ฑํ redisConnectionFactory ์ ์ฌ๊ธฐ์ ๋ง๋ redisConfiguration์ ๋ฑ๋กํจ
return RedisCacheManager.RedisCacheManagerBuilder
.fromConnectionFactory(redisConnectionFactory)
.cacheDefaults(redisConfiguration)
.build()
}
}
๐ Caffeine
Caffeine์ ์ฌ์ฉ๋ฒ์ด ๋จ์ํ ๋ก์ปฌ ์บ์(๋ฉ๋ชจ๋ฆฌ ์บ์)์ ํ๋๋ก, ์๋ฐ์ ConcurrentMap ๋ณด๋ค ์๋๊ฐ ๋น ๋ฅธ๊ฑธ๋ก ์ ๋ช ํ๋ค.
๋ ๋์ค ์ค์ ์ ๋ดค์ผ๋ฉด ๋๊ผ๊ฒ ์ง๋ง ์บ์ ๊ตฌํ์ฒด ์ค์ ์ด ๊ท์ฐฎ์๊ฑฐ์ง CacheManager ์ค์ ์ ๋ณ๊ฑฐ์๋ค ใ
// ์คํ๋ง ๋ก์ปฌ cache๋ฅผ ์ํ ์ค์
implementation 'org.springframework.boot:spring-boot-starter-cache'
// caffeine
implementation 'com.github.ben-manes.caffeine:caffeine'
@EnableCaching
@Configuration
public class CacheConfig {
@Bean // ์นดํ์ธ ๊ฐ์ฒด๋ฅผ ์์ฑํ๋ค. ๋ฌผ๋ก ๊ผญ ๋น์ผ๋ก ๋ฑ๋กํ ํ์๋ ์๋ค.
public Caffeine caffeineConfig() {
return Caffeine
.newBuilder()
.recordStats() // ์บ์ ํต๊ณ ๊ธฐ๋ก
.expireAfterWrite(60, TimeUnit.MINUTES) // .expireAfterWrite(java.time.Duration.ofMinutes(60L))
.maximumSize(5)
}
@Bean
public CacheManager cacheManager(Caffeine caffeine) {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(caffeine);
return cacheManager;
}
}
โ initialCapacity: ๋ด๋ถ ํด์ ํ ์ด๋ธ์ ์ต์ํ์ ํฌ๊ธฐ๋ฅผ ์ค์ ํฉ๋๋ค.
โ maximumSize: ์บ์์ ํฌํจํ ์ ์๋ ์ต๋ ์ํธ๋ฆฌ ์๋ฅผ ์ง์ ํฉ๋๋ค.
โ maximumWeight: ์บ์์ ํฌํจํ ์ ์๋ ์ํธ๋ฆฌ์ ์ต๋ ํฌ๊ธฐ๋ฅผ ์ง์ ํฉ๋๋ค. (* maximumSize์ ํจ๊ป ์ง์ ํ ์ ์๋ค, ๋ ์ค ํ๋๋ง)
โ expireAfterAccess: (์บ์๊ฐ ์์ฑ๋ ํ or ๋์ฒด๋๊ฑฐ๋ ๋ง์ง๋ง์ผ๋ก ์ฝ์ ํ) ํน์ ๊ธฐ๊ฐ์ด ๊ฒฝ๊ณผํ๋ฉด ์บ์์์ ์๋์ผ๋ก ์ ๊ฑฐ
โ expireAfterWrite: (ํญ๋ชฉ์ด ์์ฑ๋ ํ or ๋ฐ๋ ํ) ํน์ ๊ธฐ๊ฐ์ด ์ง๋๋ฉด ๊ฐ ํญ๋ชฉ์ด ์บ์์์ ์๋์ผ๋ก ์ ๊ฑฐ
โ refreshAfterWrite: ์บ์๊ฐ ์์ฑ๋๊ฑฐ๋ ๋ง์ง๋ง์ผ๋ก ์ ๋ฐ์ดํธ๋ ํ ์ง์ ๋ ์๊ฐ ๊ฐ๊ฒฉ์ผ๋ก ์บ์๋ฅผ ์๋ก๊ณ ์นจ(๊ฐฑ์ ) ํฉ๋๋ค.
โ weakKeys: ํค๋ฅผ weak reference๋ก ์ง์ ํฉ๋๋ค. (GC์์ ํ์๋จ)
โ weakValues: Value๋ฅผ weak reference๋ก ์ง์ ํฉ๋๋ค. (GC์์ ํ์๋จ)
โ softValues: Value๋ฅผ soft reference๋ก ์ง์ ํฉ๋๋ค. (๋ฉ๋ชจ๋ฆฌ๊ฐ ๊ฐ๋ ์ฐผ์ ๋ GC์์ ํ์๋จ)
โ recordStats: ์บ์์ ๋ํ Statics๋ฅผ ์ ์ฉํฉ๋๋ค.
์ฐธ๊ณ ๋ก recordStats()๋ ์๋์ ๊ฐ์ ์บ์ ํต๊ณ ๊ฐ์ฒด๋ฅผ ์์ฑํ๋ค. ์ด๋ Cache.stats() ๋ฉ์๋๋ก ํ์ธํ ์ ์๋ค.
// com.github.benmanes.caffeine.cache.Cache ์ธํฐํ์ด์ค
Cache.stats() // CacheStats ๋ฅผ ๋ฐํํ๋ค.
public final class CacheStats {
// ...
private final long hitCount;
private final long missCount;
private final long loadSuccessCount;
private final long loadFailureCount;
private final long totalLoadTime;
private final long evictionCount;
private final long evictionWeight;
// ...
public double hitRate() {/* ... */}
public long evictionCount() {/* ... */}
public double averageLoadPenalty() {/* ... */}
// ...
}
์บ์ ์ค์ ๊ฟํ1 - ENUM ์ด์ฉํ๊ธฐ
๋งค๋ฒ ์ค์ ๊ฐ๋ค์ Configuration์์ ์ซ์๋ก ๋ฐ๋ ๊ฑด, ์ ์ง๋ณด์๋ ๊ด๋ฆฌ ์ธก๋ฉด์์ ์ข์ง ๋ชปํ๋ค.
์๋์ ๊ฐ์ด ์ค์ ๊ฐ๋ค์ ๊ฐ์ง๊ณ ์๋ CacheType ๊ฐ์ฒด๋ฅผ ๋ง๋ค๊ณ , ์ด๋ฅผ Enum์ผ๋ก ๋ค๊ณ ์์.
@Getter
public enum CacheType {
ARTISTS("artists", 5 * 60, 10000), // cachename: artists
ARTIST_INFO("artistInfo", 24 * 60 * 60, 10000); // cachename: artistInfo
CacheType(String cacheName, int expiredAfterWrite, int maximumSize) {
this.cacheName = cacheName;
this.expiredAfterWrite = expiredAfterWrite;
this.maximumSize = maximumSize;
}
private String cacheName;
private int expiredAfterWrite;
private int maximumSize;
}
์บ์ ์ค์ ๊ฟํ2- ์ค์ (yml) ์ฝ์ด์ ์ด์ฉํ๊ธฐ
ํ๊ฑธ์ ๋ ๋์๊ฐ์, ์ค์ ๊ฐ๋ค์ ์๋ฐ ๊ฐ์ฒด๊ฐ ์๋ ์คํ๋ง ํ๋กํผํฐ๋ก ์ผ๊ด์ ์ผ๋ก ๊ด๋ฆฌํ์ฌ ์ฝ์ด์ค๋ฉด ๋ ๊น๋ํ๋ค.
my:
project:
cache-manager:
type: REDIS
caches:
- cacheName: accessToken
expireAfterWrite: 1800
- cacheName: loginOtp
expireAfterWrite: 300
type: CAFFEINE
caches:
- cacheName: test
expireAfterWrite: 180
maximumSize: 5
- cacheName: channels
expireAfterWrite: 180
maximumSize: 1
- cacheName: categoryContents
expireAfterWrite: 1800
maximumSize: 30
- cacheName: newContentIds
expireAfterWrite: 1800
maximumSize: 1
์ดํ ์ด ์ค์ ์ ์ฝ๊ฒ ์ฝ์ ์ ์๊ฒ ์คํ๋ง ํ๋กํผํฐ ๊ฐ์ฒด๋ฅผ ๋ฐ๋ก ๋ง๋ ๋ค.
// yml ํ๋กํผํฐ๋ฅผ ๊ฐ์ฒด๋ก ์ฝ์ด์ค๋ ์ด๋
ธํ์ด์
@ConstructorBinding
@ConfigurationProperties(prefix = "my.project.cache-manager") // ํ๋กํผํฐ ๊ฐ ๋ถ๋ฌ์ค๊ธฐ
data class MyCacheProperties(
val type: String,
val caches: List<ArBookCache>? = null
) {
data class MyCache(
val cacheName: String,
val expireAfterWrite: Long,
val maximumSize: Long? = null
)
}
์๋์ ๊ฐ์ด ํ๋กํผํฐ๋ฅผ ์ด์ฉํ์ฌ ์ค์ ์ค์ ๊ฐ์ ์ฝ๋๊ฐ ๋ชฐ๋ผ๋, ๋ฑ๋กํ ์ ์๊ฒ ์ถ์ํํ๋ค.
@Configuration
@EnableCaching // ์ฃผ์ - ConfigurationProperties๋ฅผ ์ฌ์ฉํ ๋, Enable ์ด๋
ธํ
์ด์
์ ์ถ๊ฐํด์ผ ์์ฑ๋๋ค.
@EnableConfigurationProperties(MyCacheProperties::class)
class CachingConfig() {
@Bean
fun cacheManager(myCacheProperties: MyCacheProperties): CacheManager{
val cacheProperties:List<MyCacheProperties.MyCache> = myCacheProperties.caches
?: return NoOpCacheManager() // yml ์ค์ ๊ฐ์ด ์๋ ๊ฒฝ์ฐ, ์บ์๋ฅผ ์ ์ฉํ์ง ์๋๋ก ๋ง๋ฆ
if(cacheProperties.type == "CAFFEINE"){
val caches:List<CaffeineCache> = cacheProperties.map{
CaffeineCache( it.cacheName, // it == MyCacheProperties.MyCache
Caffeine.newBuilder()
.expireAfterWrite(it.expireAfterWrite.seconds.toJavaDuration())
.maximumSize(property.maximumSize ?: 1)
.build()
)
}
/*.. ์ดํ CacheManager ์์ฑ๋ถ๋ถ ์๋ต ..*/
}
}
์ ์ฝ๋๋ ์์ ์ผ ๋ฟ์ด๊ณ , ์์์ ์ด์ง ์ธ๊ธํ @CondtionalOnProperty ๊ฐ์์คํ๋ง๋ถํธ์ ์กฐ๊ฑด๋ถ ๋น ๋ฑ๋ก ๊ณผ ํจ๊ป ์ฌ์ฉํ๋ฉด, yml ์ค์ ๋ง์ผ๋ก ๋์์ ๋ณ๊ฒฝํ๋๋ก ์ถ์ํ ํ ์ ์๋ค.
๋ง๋ค๊ธฐ ๋๋ฆ์ด๋ค. ๋ง์ฝ ์บ์๋งค๋์ ๊ฐ ์ฌ๋ฌ๊ฐ๋ผ๋ฉด ์์์ ์ธ๊ธํ CacheConfigurerSupport, CacheResolver๋ฑ์ ์ฌ์ฉํ์ฌ ์ ์ ํ๊ฒ ์ฒ๋ฆฌํ์
'๐ฑ Spring Framework' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
Cache#2 ์บ์์ ๋ํ ์ดํด (1) | 2022.08.28 |
---|
๋ธ๋ก๊ทธ์ ์ ๋ณด
JiwonDev
JiwonDev