ย ์ด์ ํฌ์คํ
์ ํตํด, ์บ์์ ๋ํ ์ดํด๋ ์ด๋์ ๋ ๊ฐ๋ฅ์ด ์กํ๋ค.
ย ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ์คํ๋ง์์ ์บ์๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด ํ์ํ ๊ฐ๋
๋ค์ ํ๋ํ๋ ์์ธํ๊ฒ ์ดํด๋ณด์.
| ๋ฉ์๋ | ์ค๋ช |
|---|---|
get(Object key) | ํค๋ก ์กฐํ, ๊ฐ์ด ์์ผ๋ฉด null ๋ฐํ. ์บ์ hit/miss ํ์ ๊ฐ๋ฅ |
get(Object key, Class<T> type) | ํ์ ์ง์ ์กฐํ |
get(Object key, Callable<T> valueLoader) | ์บ์ ๋ฏธ์ค ์ Callable ์คํ ํ ๊ฐ ์ ์ฅ. ๋์์ฑ ์ ์ด ํต์ฌ. |
put(Object key, Object value) | ์บ์์ ๊ฐ ์ ์ฅ |
putIfAbsent(Object key, Object value) | ์ด๋ฏธ ๊ฐ์ด ์์ ๋๋ง ์ ์ฅ |
evict(Object key) | ํน์ ํค ์ญ์ |
clear() | ์บ์ ์ ์ฒด ์ญ์ |
๐ get(Object key, Callable<T> valueLoader ์ฌ์ฉ ์์
Cache cache = cacheManager.getCache("user");
User user = cache.get(userId, () -> userRepository.findById(userId));
| ๋ฉ์๋ | ์ค๋ช |
|---|---|
getCache(String name) | ์ด๋ฆ์ผ๋ก Cache ์ธ์คํด์ค ๋ฐํ |
getCacheNames() | ๋ฑ๋ก๋ Cache ์ด๋ฆ ๋ชฉ๋ก ๋ฐํ |
| ๋ฉ์๋ | ์ค๋ช |
|---|---|
Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) | ํธ์ถ ์ปจํ ์คํธ์์ ์บ์ ๊ฒฐ์ |
@Override
public Object generate(Object target, Method method, Object... params) {
return method.getName() + "#" + Arrays.deepHashCode(params);
}| ๋ฉ์๋ | ์ค๋ช |
|---|---|
handleCacheGetError(RuntimeException exception, Cache cache, Object key) | get ์ ์์ธ ์ฒ๋ฆฌ |
handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) | put ์ ์์ธ ์ฒ๋ฆฌ |
handleCacheEvictError(RuntimeException exception, Cache cache, Object key) | evict ์ ์์ธ ์ฒ๋ฆฌ |
handleCacheClearError(RuntimeException exception, Cache cache) | clear ์ ์์ธ ์ฒ๋ฆฌ |
@Override
public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
log.error("Redis ์๋ฌ: DB ํด๋ฐฑ", exception);
// DB ์กฐํ
}CachingConfigurationSelector ๋ฅผ ํตํด ์บ์ ์ธํ๋ผ ๊ตฌ์ฑ์ importํ์ฌ ์ด๋
ธํ
์ด์
๊ธฐ๋ฐ ์บ์ฑ ํ์ฑํpublic class CacheMain {
@Cacheable("user")
public User getUser(Long id) { ... }
public User getUser2(Long id) {
return getUser(id); // ๋ด๋ถ ํธ์ถ, ํ๋ก์๋ฅผ ์ ๊ฑฐ์นจ -> ์บ์ ๋ฏธ์ ์ฉ
}
}@Cacheable ๊ธฐ์ค, ํธ์ถ ํ๋ฆํด๋ผ์ด์ธํธ ์์ฒญ
โ
Service (ํ๋ก์)
โ (Advisor / Interceptor ๊ฐ๋ก์ฑ๊ธฐ)
CacheInterceptor
โโ CacheOperation ์กฐํ (OperationSource)
โโ CacheManager๋ก Cache ์กฐํ
โโ Cache Hit โ ๋ฐ๋ก ๋ฐํ
โโ Cache Miss โ ์ค์ Service ๋ฉ์๋ ์คํ โ ๊ฒฐ๊ณผ ์บ์์ ์ ์ฅ โ ๋ฐํ
| ๊ตฌ์ฑ ์์ | ์ญํ |
|---|---|
| Advisor | ์ด๋ค ๋ฉ์๋์ ์บ์ ์ด๋๋ฐ์ด์ค๋ฅผ ์ ์ฉํ ์ง ๊ฒฐ์ ( @Cacheable , @CachePut , @CacheEvict ๋ฑ ) |
| Interceptor (=Advice) | ์ค์ ์บ์ ๋์ ์ํ ( CacheInterceptor ) |
| OperationSource | @Cacheable ๋ฑ์ ์ด๋
ธํ
์ด์
์ CacheOperation ๊ฐ์ฒด๋ก ๋ณํ -> ์บ์ ๋์ ์ ์ ์์ฑ |
| CacheManager / KeyGenerator / CacheResolver / ErrorHandler | ์ค์ ์บ์ ์กฐํ, ํค ์์ฑ, ์ ํ, ์์ธ ์ฒ๋ฆฌ ๋ฑ์ ๋ด๋น |
@Cacheable ์ด ์์ผ๋ฉด CacheInterceptor ์คํ์ ์๋ ค์ฃผ๋ ์ค๊ฐ ๊ด๋ฆฌ์ ์ญํ @Cacheable , @CachePut , @CacheEvict ๊ฐ์ ์ด๋
ธํ
์ด์
์ ๋ด๋ถ์ ์ผ๋ก CacheOperation ๊ฐ์ฒด๋ก ๋ณํ@Cacheable(value = "users", key = "#id")
public User find(Long id)๋ค์๊ณผ ๊ฐ์ ๊ตฌ์กฐ๋ก ๋ณํCacheOperation:
- cacheName: users
- key: #id
- condition: null
- unless: null1. ์บ์ ํค ์์ฑ
2. CacheManager์์ ์บ์ ๊ฐ์ ธ์ค๊ธฐ
3. ์บ์์ ๊ฐ์ด ์๋์ง ์ฒดํฌ
โ ์์ผ๋ฉด ๋ฐ๋ก ๋ฐํ (๋ฉ์๋ ๋ฏธ์คํ)
โ ์์ผ๋ฉด ์ค์ ๋ฉ์๋ ์คํ
4. ๊ฒฐ๊ณผ๋ฅผ ์บ์์ ์ ์ฅ
5. ๋ฐํ@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Reflective
public @interface Cacheable {
// cacheNames: ์ฌ์ฉํ ์บ์ ์ด๋ฆ๋ค ์ง์
String[] cacheNames() default {};
// value: cacheNames์ ๋์ผ.cacheNames์ ๋ณ์นญ
String[] value() default {};
// key: ์บ์ ํค๋ฅผ SpEL๋ก ์ง์
String key() default "";
// keyGenerator: KeyGenerator ๋น ์ด๋ฆ ์ง์
String keyGenerator() default "";
// cacheManager: CacheManager ๋น ์ด๋ฆ ์ง์ (Resolver ๋์ ์ง์ ์ง์ ํ๋ ๋๋)
String cacheManager() default "";
// cacheResolver: CacheResolver ๋น ์ด๋ฆ ์ง์ (์ฌ๋ฌ ์บ์์์ ๋์ ์ ํ ์)
String cacheResolver() default "";
// condition: ์บ์ ์ ์ฉ ์ฌ๋ถ(SpEL). true์ผ ๋๋ง ์บ์ ๋ก์ง ๋์
String condition() default "";
// unless: ์บ์ ์ ์ฅ ์ฌ๋ถ(SpEL). true๋ฉด put ์คํต
// ์ผ๋ฐ์ ์ผ๋ก '๊ฒฐ๊ณผ ๊ธฐ๋ฐ'์ผ๋ก ํ๊ฐ
String unless() default "";
// sync: stampede ๋ฐฉ์ง์ฉ ๋๊ธฐํ ๋ชจ๋(๊ฐ์ key์ ๋ํ ๋์ ๋ก๋ฉ ๋ฐฉ์ง)
boolean sync() default false;
}
cacheNameskeyconditiontrue ์ผ ๋๋ง ์บ์ ๋ก์ง ์ ์ฉfalse ๋ฉด ์บ์ ๋ฌด์ ํ ๋ฉ์๋ ํธ์ถunless#result )์ ๋ณด๊ณ ์บ์ฑํ์ง ์์ ๊ฒฐ๊ณผ ๊ฒฐ์ true ์ผ ์ ์บ์์ ์ ์ฅํ์ง ์์sync=trueunless ์๋ ๊ฐ์ด ์ฌ์ฉ ๋ถ๊ฐ๋ฅ@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Reflective
public @interface CachePut {
String[] cacheNames() default {};
String[] value() default {};
String key() default "";
String keyGenerator() default "";
String cacheManager() default "";
String cacheResolver() default "";
String condition() default "";
String unless() default "";
}
@CacheEvict ๊ฐ ๋ ์ ํธ๋๊ธฐ๋ ํจ@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Reflective
public @interface CacheEvict {
String[] cacheNames() default {};
String[] value() default {};
String key() default "";
String keyGenerator() default "";
String cacheManager() default "";
String cacheResolver() default "";
String condition() default "";
// allEntries=true ์, ํด๋น ์บ์์ ๋ชจ๋ ์ํธ๋ฆฌ drop
boolean allEntries() default false;
// beforeInvocation=true ์ ๋ฉ์๋ ์คํ ์ ์ evict
// ๋ฐ๋ผ์, ๋ฉ์๋๊ฐ ์์ธ๋ฅผ ๋์ ธ๋ evict ์ํ
boolean beforeInvocation() default false;
}
allEntries=true ๋ ๋งค์ฐ ์ํํ ์ ์์ผ๋ฏ๋ก, ์ ๋ง ํ์ํ ๋๋ง ์ฌ์ฉbeforeInvocation=true ๋ ์คํจํ๋๋ผ๋ ๋ฐ๋์ ์บ์๋ฅผ ๋ฌดํจํํด์ผ ํ๋ ์ํฉ์๋ง ์ฌ์ฉ@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Reflective
public @interface Caching {
Cacheable[] cacheable() default {};
CachePut[] put() default {};
CacheEvict[] evict() default {};
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CacheConfig {
String[] cacheNames() default {};
String keyGenerator() default "";
String cacheManager() default "";
String cacheResolver() default "";
}
@CacheConfig ๋ง ๋ถ์ธ๋ค๊ณ ์บ์๊ฐ ์ผ์ง๋ ๊ฒ ์๋๊ณ , ์ค์ง ๊ธฐ๋ณธ๊ฐ๋ง ์ ๊ณต@CacheConfig(cacheNames = "user", keyGenerator = "userKeyGenerator")
public class UserService {
@Cacheable // ๋ฉ์๋๋ณ ์ ์ฉ
public User get(long id) { ... }
}
@Cacheable(
cacheNames = "items",
key = "#id",
condition = "#id > 0", // (1) ๋จผ์ ํ๊ฐ
unless = "#result == null" // (2) ๋ฉ์๋ ์คํ ํ ํ๊ฐ
)
public Product find(long id) {
return productRepository.findById(id).orElse(null);
}#result ์์ฒด๊ฐ ์์unless ๋ Cache Hit ํ๋จ ์กฐ๊ฑด์์ ์ ์ธ

@CachePut(
cacheNames = "products",
key = "#result.id", // ์์ฑ ํ id ์๊ธฐ๋ ๊ฒฝ์ฐ ์ ์ฉ
condition = "#result != null", // ๊ฒฐ๊ณผ ๊ธฐ๋ฐ condition ๊ฐ๋ฅ
unless = "#result.delete == true" // ๊ฒฐ๊ณผ์ ๋ฐ๋ผ ์บ์ put ๊ฑฐ๋ถ
)
public Product updateProduct(ProductUpdateDto response) {
return productService.update(response);
}@CachePut ์ ๋ฉ์๋๊ฐ ํญ์ ์คํcondition , unless ๋ชจ๋ ์ฌ์ฉ ๋ฐ ์ ์ด ๊ฐ๋ฅ@CachePut ์์๋ condition ๋ํ ๋ฉ์๋ ํธ์ถ ํ ํ๊ฐ๋๋ฉฐ, ์ฌ๊ธฐ์ #result ์ฐธ์กฐ ๋ํ ๊ฐ๋ฅ
beforeInvocation = false )@CacheEvict(cacheNames = "products", key = "#id")
public void deleteProduct(Long id) {
productRepository.deleteById(id); // ์ฑ๊ณต ํ evict
}#result ์ฌ์ฉ ๊ฐ๋ฅ
beforeInvocation = true )@CacheEvict(cacheNames = "products", key = "#id", beforeInvocation = true)
public void deleteProduct(Long id) {
productRepository.deleteById(id); // ์คํ ์ evict
}#result ์ฌ์ฉ ๋ถ๊ฐ1. #root.methodName
key = "#root.methodName + ':' + #id"2. #root.method
key = "#root.method.name + ':' + #id"3. #root.target
key = "#root.targetClass.simpleName + ':' + #id"4. #root.targetClass
key = "#root.targetClass.name + ':' + #id"5. #root.args
key = "#root.args[0]"6. #root.caches
key = "#root.caches[0].name + ':' + #id"7. ํ๋ผ๋ฏธํฐ ์ด๋ฆ ๊ธฐ๋ฐ ์ฐธ์กฐ ( #id , #request.userId )
key = "#id"
key = "#request.userId + ':' + #request.page"
-parameters ์ปดํ์ผ ์ต์
ํ์8. ์ธ๋ฑ์ค ๊ธฐ๋ฐ ์ฐธ์กฐ ( #a0 , #p0 , #root.args[0] )
#a0 , #p0 ๊ฐ์ ์ธ๋ฑ์ค ์ ๊ทผ์ ์ฐ๋ผ๊ณ ์๋ดkey = "#a0" // ์ฒซ ๋ฒ์งธ ์ธ์
key = "#p1" // ๋ ๋ฒ์งธ ์ธ์9. #result
@Cacheable : unless ์์ ์ฌ์ฉ ๊ฐ๋ฅ@CachePut : key , condition , unless ์์ ๊ฒฐ๊ณผ ๊ธฐ๋ฐ์ผ๋ก ์ฌ์ฉ ๊ฐ๋ฅ@CacheEvict : beforeInvocation=false ์ผ ๋ ์ฌ์ฉ ๊ฐ๋ฅ@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository productRepository;
/ **
* id๊ฐ 0 ์ดํ๋ฉด ์บ์ ๋ฏธ์ฌ์ฉ (condition)
* ์กฐํ ๊ฒฐ๊ณผ๊ฐ null์ด๋ฉด ์บ์์ ๋ฏธ์ ์ฅ (unless)
*/
@Cacheable(
cacheNames = "products",
key = "#id",
condition = "#id != null && #id > 0",
unless = "#result == null"
)
public Product findById(Long id) {
// ์บ์ miss์ผ ๋๋ง ์คํ
return findByIdFromDb(id);
}
private Product findByIdFromDb(Long id) {
return productRepository.findById(id).orElse(null);
}
}
@Service
@RequiredArgsConstructor
public class ProductSearchService {
private final ProductSearchRepository productSearchRepository;
/**
* ๊ฒ์ ์บ์๋ ํญ๋ฐํ๊ธฐ ์ฌ์
* page๊ฐ ๋๋ฌด ํฌ๋ฉด ์บ์ ์ ์ธ (condition)
* ๊ฒฐ๊ณผ๊ฐ ๋น์ด์์ผ๋ฉด ์บ์ ์ ์ฅ ์ ์ธ (unless)
* keyword๋ trim๊ณผ lowercase๋ก ํค ์ ๊ทํ
*/
@Cacheable(
cacheNames = "searchProducts",
key = "'keyword=' + #keyword.trim().toLowerCase() + ':page=' + #page + ':size=' + #size",
condition = "#page >= 0 && #page < 20 && #size <= 100",
unless = "#result == null || #result.isEmpty()"
)
public List<Product> searchProduct(String keyword, int page, int size) {
return searchProductFromDb(keyword, page, size);
}
private List<Product> searchProductFromDb(String keyword, int page, int size) {
return productSearchRepository.search(keyword, page, size);
}
}
@Service
@RequiredArgsConstructor
public class ProudctService {
private final ProductRepository productRepository;
/**
* @CachePut์ ํญ์ ๋ฉ์๋ ์คํ
* ์ ์ฅ/์์ ํ ๋ฐํ๋ ๊ฒฐ๊ณผ(result)๋ฅผ ๊ธฐ์ค์ผ๋ก ์บ์์ ์ ์ฅ
*/
@CachePut(
cacheNames = "products",
key = "#result.id", // ๊ฒฐ๊ณผ id ๊ฐ์ผ๋ก ํค ์ถ์ถ
condition = "#result != null", // ๊ฒฐ๊ณผ๊ฐ ์์ด์ผ๋ง put
unless = "#result.notSale" // notSale ์ํ๋ฉด put ๊ธ์ง
)
public Product save(ProductCreateDto request) {
return saveProduct(request);
}
private Product saveProduct(ProductCreateDto request) {
return productRepository.save(request.id, request.notSale);
}
}
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository productRepository;
/**
* ๊ธฐ๋ณธ๊ฐ (beforeInvocation = false)
* ๋ฉ์๋๊ฐ ์ฑ๊ณตํด์ผ๋ง ๊ทธ ํ์ ์บ์ ์ ๊ฑฐ
* ์์ธ ๋ฐ์ ์ ์บ์๊ฐ ์ญ์ ๋์ง ์์
- ํธ๋์ญ์
๋กค๋ฐฑ ๊ฐ๋ฅ์ฑ์ด ์กด์ฌํ ๋, ์ผ๊ด์ฑ ์ธก๋ฉด์์ ๋ ์์ ํ ํธ
*/
@CacheEvict(cacheNames = "products", key = "#id")
public void deleteProduct(Long id) {
deleteProductFromDb(id);
}
/**
* beforeInvocation = false
* ๋ฉ์๋ ํธ์ถ ์ , ๋ฏธ๋ฆฌ ์บ์ ์ ๊ฑฐ
* ์์ธ๊ฐ ๋ฐ์ํ๋๋ผ๋ ๊ทธ ์ ์ ์บ์๋ ์ ๊ฑฐ๋ ์ํ
*/
@CacheEvict(cacheNames = "products", key = "#id", beforeInvocation = true)
public void deleteProductForce(Long id) {
deleteProductFromDb(id);
}
private void deleteProductFromDb(Long id) {
productRepository.delete(id);
}
}
์บ์๋ "๊ฐ์ ํค๋ฉด ๊ฐ์ ๊ฐ" ์ด๋ผ๋ ๊ฐํ ๊ฐ์ ์ ์ ์ ๋ก ๋์
ํค๊ฐ ์๋ชป๋๋ฉด ์๋์ ๊ฐ์ ๋ฌธ์ ๋ฐ์ ๊ฐ๋ฅ
1. ์๋ชป๋ ๋ฐ์ดํฐ ๋ฐํ(์ค์ผ): ์๋ก ๋ค๋ฅธ ์์ฒญ์ด ๊ฐ์ ํค๋ก ๋งคํ(์ถฉ๋)
2. ์ ๋ณด ๋์ถ: ๊ถํ/ํ
๋ํธ๋ณ ๊ฒฐ๊ณผ๊ฐ ๋ค๋ฆ์๋, ํค์ scope๊ฐ ์์ด์ ๋จ์ ๋ฐ์ดํฐ ๋
ธ์ถ ๊ฐ๋ฅ
3. ์บ์ ๋ฌด์ฉ์ง๋ฌผ: ๋์ผ ์์ฒญ์์๋ ํค๊ฐ ๊ณ์ ๋ฌ๋ผ์ ธ Cache Miss ๋ฐ์
4. ์ด์ ํต์ ๋ถ๊ฐ: ํค์ prefix๋ ๋ฒ์ ์ด ์์ผ๋ฉด ๋กค๋ฐฑ/๋ฐฐํฌ ๋ ์ ์ฒด ์บ์๋ฅผ ์ ๊ฑฐํด์ผ ํจ
[solo, male]
[male, solo][male, solo]games:tags=male,soloplayon:v2:partyDetail:{partyId}tenantIdroleuserId๐ Redis ์ฌ์ฉ ์ค์ด๋ผ๊ณ ๊ฐ์
1. ๋คํธ์ํฌ ๋น์ฉ ์ฆ๊ฐ
2. ๋ฉ๋ชจ๋ฆฌ ์ค๋ฒํค๋
3. ํํฐ ํญ๋ฐ ๋ฌธ์ ๋ฐ์ ๋ฐ ์ด์ ๋์ด๋ ์ฆ๊ฐ
๐ฏ ํค ์ค๊ณ ๊ท์น์ ๋จ๊ณ์ ์ผ๋ก ๊ณ ๋ํํด๊ฐ๋ฉฐ, ์บ์ ํค ์ค๊ณ ๊ท์น์ ํ์ฅํด๋ณด์.
โ@Bean
public KeyGenerator globalKeyGenerator() {
return (target, method, params) ->
method.getName() + "::" + Arrays.deepToString(params);
}
KeyGenerator ๊ตฌํ ๋ฐฉ์๐ ํ์ฌ ํค ์ค๊ณ ์์น ๊ด์ ์์๋ ์ฌ๋ฌ ๋ฌธ์ ์กด์ฌ
1. ํค๊ฐ ๊ฒฐ์ ์ ์ด์ง ์์
toString() ํฌ๋งท ๋ฑ์ด ๋ฐ๋๋ฉด ํค ๋ณ๊ฒฝ2. ์ถฉ๋์ ๋ฐฉ์งํ์ง ๋ชปํจ
method.getName() ์ ๊ฒฝ์ฐ, ๋์ผํ ๋ฉ์๋๋ช
(์ค๋ฒ๋ก๋ ๋ฑ)์ผ๋ก ์ธํ ์ถฉ๋ ์ํ ์กด์ฌArrays.deepToString() ์ ๊ฒฝ์ฐ, ์๋ก ๋ค๋ฅธ ํ๋ผ๋ฏธํฐ๊ฐ ๋์ผํ ๋ฌธ์์ด์ด ๋์ค๋ ๋ฑ ์ถฉ๋ ์ํ ์กด์ฌ3. ๋ฒ์ ๋ณ ์บ์ ๋ฌดํจํ ์ด๋ ค์
4. ์บ์ ์ค์ฝํ ๋๋ฝ ๊ฐ๋ฅ์ฑ ์กด์ฌ
5. ํค ๊ธธ์ด๊ฐ ๊ธธ์ด์ง ์ ์์
โ@Bean
public KeyGenerator hashKeyGenerator() {
return (target, method, params) -> {
String signature = method.getDeclaringClass().getSimpleName() + "#" + method.getName();
String normalized = Canonicalizer.canonicalize(params); // ์ปฌ๋ ์
์ ๊ทํ
String digest = sha256Hex(signature + ":" + normalized);
return signature + ":" + digest;
}
}
static class Canonicalizer {
static String canonicalize(Object... params) {
if (params == null || params.length == 0) {
return "[]";
}
return Arrays.stream(params)
.map(Canonicalizer::normalize)
.collect(Collectors.joining(",", "[", "]"));
}
// ์ ๊ทํ
static String normalize(Object p) {
if (p == null) {
return "null";
}
if (p instanceof CharSequence s) {
return s.toString().trim();
}
if (p instanceof Number || p instanceof Boolean) {
return String.valueOf(p);
}
if (p instanceof Enum<?> e) {
return e.name();
}
// Map -> key ๊ธฐ์ค์ผ๋ก ์ ๋ ฌ
if (p instanceof Map<?, ?> m) {
return m.entrySet().stream()
.map(e -> normalize(e.getKey()) + "=" + normalize(e.getValue()))
.sorted()
.collect(Collectors.joining("&", "{", "}"));
}
// Set -> ์ ๋ ฌ
if (p instanceof Set<?> s) {
return s.stream()
.map(Canonicalizer::normalize)
.sorted()
.collect(Collectors.joining(",", "S[", "]"));
}
// List -> ๊ฐ ์์ ์ ์ง
if (p instanceof List<?> l) {
return l.stream()
.map(Canonicalizer::normalize)
.collect(Collectors.joining(",", "L[", "]"));
}
return p.toString().trim();
}
}
๐ ์ฌ์ ํ ๋จ์์๋ ๋ฌธ์
1. ๋๋ฉ์ธ๋ณ ํค ์ ์ฑ ์ฐจ์ด
2. ์บ์ ์ค์ฝํ ๋๋ฝ ๊ฐ๋ฅ์ฑ ์กด์ฌ
3. ๋ฒ์ ๋ณ ์บ์ ๋ฌดํจํ ์ด๋ ค์
4. DTO ๊ตฌ์กฐ ๋ณ๊ฒฝ์ ๋ฐ๋ฅธ ๋ฌธ์
Prefix ๋ Redis ์ค์ ์ผ๋ก, ์ ๊ทํ ๋ฑ์ ํค ์ ์ฑ
์ CacheKeys ๋ก ์ค์์ง์คํ1. Prefix๋ RedisCacheConfiguration์ผ๋ก ๋ฑ๋ก
โ@Bean
public RedisCacheConfiguration redisCacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig()
.computePrefixWith(cacheName -> "playon:" + cacheName + "::");
}
2. CacheNames์ ์บ์ ์ด๋ฆ ์์ํ
โ@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class CacheNames {
public static final String PARTY_LIST = "partyList";
public static final String PARTY_DETAIL = "partyDetail";
}
3. CacheKeys๋ก ๋๋ฉ์ธ๋ณ ํค ์ ์ฑ ์ ์บ์๋ณ๋ก ๋ช ์
โ@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class CacheKeys {
private static final String VERSION = "v1";
public static String partyListKey(
int page, int pageSize, String orderBy, boolean isMacSupported, String partyAt,
Long appId, Collection<String> genres, Collection<String> tags
) {
// 1. ์ ๊ทํ ์งํ
Set<String> g = normalize(genres);
Set<String> t = normalize(tags);
// 2. ํํฐ ๋ฐ ๋๋ฉ์ ์ถ๊ฐ
String payload =
"page=" + page +
"&size=" + pageSize +
"&orderBy=" + trim(orderBy) +
"&mac=" + isMacSupported +
"&partyAt=" + trim(partyAt) +
"&appId=" + (appId == null ? "" : appId) +
"&genres=" + String.join(",", g) +
"&tags=" + String.join(",", t);
// 3. ํด์๋ก ์์ถ
return VERSION + ":" + sha256Hex(payload);
}
// ์ปฌ๋ ์
-> ์ ๋ ฌ์ ํตํ ์ ๊ทํ
private static Set<String> normalize(Collection<String> c) {
TreeSet<String> ts = new TreeSet<>();
if (c == null) {
return ts;
}
for (String v : c) {
String trimV = trim(v);
if (!trimV.isEmpty()) {
ts.add(trimV);
}
}
return ts;
}
// SHA-256 ์๊ณ ๋ฆฌ์ฆ์ ํตํ ํด์ฑ
private static String sha256Hex(String s) {
try {
byte[] digest = MessageDigest.getInstance("SHA-256")
.digest(s.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder(digest.length * 2);
for (byte b : digest) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (Exception e) {
throw new IllegalArgumentException(e);
}
}
// ๋น ๊ฐ ์ฒดํฌ ๋ฐ ๊ณต๋ฐฑ ์ ๊ฑฐ + ๋์๋ฌธ์ ํต์ผ
private static String trim(String s) {
return s == null ? "" : s.trim().toLowerCase(Locale.ROOT);
}
}
4. @Cacheable์์ CacheKeys ํธ์ถ
โ@Cacheable(
cacheNames = CacheNames.PARTY_LIST,
key = "T(ํจํค์ง๋ช
.CacheKeys).partyListKey(#page, #pageSize, #orderBy, #isMacSupported, #partyAt, #request.appId(), #request.genres(), #request.tags())"
)
public PartyListResponse list(int page, int pageSize, String orderBy, boolean isMacSupported, String partyAt, GetAllPartiesRequest request) {
// ๊ตฌํ ์๋ต
}
playon:partyList::v1:{sha256hex}