이전 포스팅에서 Cache 를 구현할 수 있는 다양한 방법에 대해서 알아보았다.
각 방법의 특징을 서술하고 본 프로젝트에 HashMap 을 적용해 Cache 를 구현하는 이유에 대해서도 설명하였다.
이번 포스팅에는 HashMap 으로 어떻게 Cache 를 구현하는지에 대해 알아보겠다.
https://daisyit.tistory.com/43 : 캐시 전략 비교
🌊 Key 설정
우선 캐시에서 가장 중요한 건 key 값의 설정이다.
key 값을 통해 원하는 value 에 접근하기 때문에, 어떻게 key 값을 설정할 것인지 명확하게 구분지어야 한다.
해당 프로젝트는 바라보는 데이터베이스 소스인 datasourceKey 값과 실행할 쿼리 sql 값에 따라서 데이터가 달라지기 때문에 이 두 변수를 key 값으로 지정해야했다.
@DependsOn(value = {"dataSourceServiceImpl"})
public class DataSourceCacheServiceImpl implements DataSourceCacheService {
static final long EXPIRE_5_MIN = 1000 * 60 * 5;
static final long DEFAULT_EXPIRE_TIME = EXPIRE_5_MIN;
DataSourceServiceImpl dataSourceServiceImpl;
ConcurrentHashMap<String, CacheData> cacheMap;
public DataSourceCacheServiceImpl(DataSourceServiceImpl dataSourceServiceImpl) {
this.dataSourceServiceImpl = dataSourceServiceImpl;
this.cacheMap = new ConcurrentHashMap<>();
}
private String getHashKey(String datasourceKey, String sql) {
return MD5Util.MD5(datasourceKey + sql); //주목
}
}
가져오는 데이터에 영향이 있는 모든 변수를 key 값으로 사용해야 한다.
때문에 본 프로젝트에서는 datasourceKey 값과 sql 을 합쳐서 MD5 암호화 알고리즘에 의해서 새로운 키 값을 생성했다.
이 키 값은 datasourceKey 값과 sql 값 둘 중 토씨 하나라도 바뀔 경우 완전 다른 값으로 인식하기 때문에 Cache Memory 가 아닌, 데이터베이스에 직접 쿼리를 날려서 데이터를 가져와 새로운 key 값을 생성하고 value를 정의한다.
이전 포스팅에서 설명했지만, 일반적인 HashMap 컬렉션이 아닌, ConcurrentHashMap 컬렉션을 사용한 이유는 Thread-Safe 특성 때문이다. 자세한 내용은 이전 포스팅을 확인하기 바란다.
참고로 @DependsOn 어노테이션은 임의로 빈 설정 순서를 지정할 수 있게 도와주는 어노테이션이다.
dataSourceServiceImpl 객체를 주입받고 있기 때문에 해당 객체보다 늦게 생성되어야 하기 때문에 따로 설정을 해주었다
🌊 Value 값 설정
이제 해당 key 값으로 value 값에 접근하는 메소드를 정의하겠다.
@Override
public CacheData getCacheData(String key) {
if (cacheMap == null || cacheMap.isEmpty()) {
return null;
}
CacheData data = cacheMap.get(key);
if (data != null && data.getExpireTimeMillis() > System.currentTimeMillis()) {
return data;
}
return null;
}
크게 3가지 로직으로 나뉜다.
- ConcurrentHashMap 이 정의되지 않았을 경우 => null 반환.
- ConcurrentHashMap 이 정의되어 있고, key 값에 해당하는 데이터가 있으며 현재 시간이 캐시 데이터 유효시간보다 작을 경우 => data 반환.
- 이외에는 모두 null 반환.
해당 메소드의 반환 값에 따른 처리에 대해서 마저 보겠다.
if (cacheData == null) {
Map<String, List<List<Object>>> result = //쿼리를 작성해 데이터를 불러들이는 메소드를 호출해 결과 값을 반환 받는다.
if (result != null || result.size() != 0) { //정상적으로 데이터를 가져왔다면
cacheData = CacheData.builder() //원하는 형식에 맞춰서 value 객체를 생성하고 builder 패턴을 통해 생성해준다.
.cachedKst(DateUtil.getNowKST())
.expireTimeMillis(nextTtlCalc(param.getTtl())) //해당 프로젝트에서는 유효시간을 데이터마다 다르게 설정해야 했기 때문
.value(result)
.build();
cacheMap.put(key, cacheData); // 생성한 value 값을 이전에 생성한 key 값과 함께 HashMap 컬렉션에 넣어준다.
} // else 문에 예외처리를 진행해야 하지만 생략하겠다.
return result; // 데이터베이스에서 가져온 데이터를 반환한다.
}
return (Map<String, List<List<Object>>>) cacheData.getValue(); //이전 메소드에서 null 이 아니면 캐시 데이터를 반환한다.
만약 이전 메소드의 결과 값이 null 이라면, 캐시 데이터는 유효하지 않다는 뜻이고 이 말은 새롭게 쿼리를 보내 데이터를 가져와야 한다는 뜻이다. 때문에 쿼리를 보내 트랜잭션을 설정하고, 데이터베이스에서 가져온 값을 새로운 결과 값으로 넣는다.
그 후 캐시 메모리에 업데이트 한다. 과정 처리가 끝나면 데이터를 반환한다.
이 과정을 거치게 되면, 같은 datasourceKey 와 같은 sql 문의 요청은, 매번 쿼리를 날려 데이터베이스에서 가져오지 않고 Memory 에서 값을 가져오게 된다. key 값으로 접근하기 때문에 매우 빠르고 효율적이다. 다만 캐시 key-value 쌍의 종류가 매우 많아지거나 해당 데이터가 너무 많아져 메모리에 부담을 주게 된다면 사용하기 어려운 방식이다. 외부 데이터캐시 서버를 사용하던 다른 방안을 생각해야할 거 같다.
🌊 Cache 데이터 삭제
HashMap 에 캐시 데이터를 조회하고, 가져오고, 등록하는 방법을 알아보았다. 이제 데이터 유효시간이 지났을 때 삭제하는 방법에 대해서 알아보겠다. 메모리에 저장하기 때문에 유효시간을 길게 가져가거나, 삭제하지 않으면 서버가 뻗을 수 있기 때문에 잘 설정해야한다.
@Override
public void clear(CacheParam param) {
String key = getHashKey(param.getDatasourceKey(), param.getSql());
cacheMap.remove(key);
}
@Override
public void clearAll() {
this.cacheMap.keySet().removeAll(cacheMap.keySet());
}
@Override
public void clearExpire() {
long now = System.currentTimeMillis();
this.cacheMap.entrySet().removeIf(entry -> now >= entry.getValue().getExpireTimeMillis());
}
단순 컬렉션을 사용하기 때문에 삭제하는 건 어렵지 않다.
특정 캐시 데이터를 삭제하고 싶다면, key 값을 통해 remove 함수를 호출하면 된다.
전체 데이터를 삭제하고 싶다면, removeAll 함수를 호출하면 된다.
다만 특정 시간마다 일일히 돌릴 수 없기 때문에 배치 프로그램을 돌려야한다.
스프링에서는 스케쥴 어노테이션을 지원한다.
@DependsOn(value = {"dataSourceCacheServiceImpl"})
public class DataSouceCacheClearBatch {
DataSourceCacheServiceImpl dataSourceCacheServiceImpl;
public DataSouceCacheClearBatch(DataSourceCacheServiceImpl dataSourceCacheServiceImpl) {
this.dataSourceCacheServiceImpl = dataSourceCacheServiceImpl;
}
@Scheduled(cron = "0 * * * * *") // 1분마다
public void clearCacheExpire() {
if (dataSourceCacheServiceImpl == null) {
log.error("dataSourceCacheService is null");
return;
}
dataSourceCacheServiceImpl.clearExpire();
//dataSourceCacheServiceImpl.printCache();
}
}
스케줄 어노테이션에 cron 을 적용하면 원하는 시간 만큼 해당 함수를 실행시킬 수 있다.
본 프로젝트에서는 모든 캐시 데이터의 유효 시간이 분 단위였기 때문에 분 단위로 검사해 캐시를 삭제하게끔 설정해두었다.
이외에도 다양한 방법으로 Cache 메모리를 구현할 수 있을 것이다.
본인 프로젝트에 Cache 를 적용하려는 이유와, 요구사항을 분석한 후 그에 맞는 적절한 캐시 전략을 선택하는게 중요하다.
'개인 공부 > Spring' 카테고리의 다른 글
[Spring] Bean Scope (0) | 2022.11.22 |
---|---|
[Spring] Multi Thread (2) | 2022.11.21 |
[Spring] Redis vs EHcache vs HashMap (0) | 2022.11.09 |
[Spring] @Transactional 파해치기 (0) | 2022.11.08 |
[Spring] JDBC 에서 Transaction 관리하는 법 (0) | 2022.11.08 |