동일한 데이터를 가져오면서 해당 데이터 호출하는 api 횟수가 많아 생기는 이슈가 발생하였다.
해당 이슈는 캐시 처리 해서 디비 커넥션을 줄일 수 있다고 판단하였다.
- 이슈상황
RDS 의 CPU가 갑작스럽게 치는 이슈 생김
- 가설
- RDS 매일 백업이 일어난다.
- 쿼리의 속도가 느리다.
- 커넥션이 많아졌다.
문제의 쿼리 확인
캐시 도입 ( Cacheable / EhCache / ElastiCache )
@Cacheable, @EhCache, 그리고 AWS ElastiCache는 모두 캐싱과 관련된 개념이지만, 각각의 적용 범위와 사용 목적이 다릅니다. 이를 비교하면서 설명하겠습니다.
1. @Cacheable
- 설명
- @Cacheable은 Spring Framework에서 제공하는 어노테이션으로, 메서드 레벨에서 캐싱을 설정하는 데 사용됩니다.
- 주요 특징
- Spring의 캐싱 추상화에 기반합니다.
- 메서드의 반환값을 캐시에 저장하고, 동일한 입력 값으로 메서드를 다시 호출하면 캐시된 값을 반환합니다.
- 실제 캐시 저장소는 다양한 캐시 제공자(EhCache, Redis, Caffeine 등)를 사용할 수 있으며, 이를 Spring Cache Manager로 관리합니다.
- 한계
- 자체적으로 캐시 저장소를 제공하지 않으며, 백엔드 캐시 솔루션이 필요합니다.
- 주로 애플리케이션 레벨에서 동작하며, 분산 캐싱 솔루션으로 사용하려면 추가적인 설정이 필요합니다.
2. @EhCache
- 설명
- @EhCache는 EhCache 라이브러리와 함께 사용되며, 캐싱을 위해 EhCache API 또는 설정을 활용하는 방식입니다.
- 주요 특징
- EhCache는 JVM 내부 메모리 기반의 캐싱 솔루션입니다.
- Spring과 독립적으로 동작하거나 Spring과 통합하여 사용할 수 있습니다.
- 강력한 캐시 관리 기능을 제공하며, XML이나 프로그래밍 방식으로 세부적인 캐시 설정이 가능합니다.
- 디스크 캐시, TTL(Time to Live), TTI(Time to Idle) 등 고급 설정을 지원합니다.
- 한계
- JVM 기반으로 동작하므로, 분산 캐시를 직접적으로 지원하지 않습니다.
- 분산 환경에서 캐싱하려면 Terracotta 같은 확장 솔루션이 필요합니다.
3. AWS ElastiCache
- 설명Redis와 Memcached의 두 가지 엔진을 지원합니다.
- AWS ElastiCache는 Amazon Web Services에서 제공하는 클라우드 기반 분산 캐시 서비스입니다.
- 주요 특징
- 클라우드 기반 분산 캐시로, 고가용성과 확장성이 뛰어납니다.
- 애플리케이션 서버 간 캐시 데이터를 공유할 수 있어 분산 환경에서 적합합니다.
- 캐시 데이터는 메모리에 저장되므로 읽기/쓰기 속도가 빠릅니다.
- AWS 관리형 서비스로, 복잡한 인프라 관리를 단순화합니다.
- Redis의 고급 기능(예: Pub/Sub, Lua scripting)과 Memcached의 단순성을 모두 제공합니다.
- 한계
- 네트워크를 통해 접근하므로, 레이턴시가 JVM 내부 캐싱 솔루션(EhCache)보다 높을 수 있습니다.
- 사용량에 따라 비용이 발생하며, 온프레미스 환경에서는 사용할 수 없습니다.
4. 차이점 비교
항목 @Cacheable @EhCache AWS ElastiCache
캐시 제공 방식 | Spring의 추상화, 백엔드 캐시 필요 | JVM 내부 메모리 기반 | 클라우드 기반 분산 캐시 |
적용 범위 | 애플리케이션 코드 레벨 | 애플리케이션 내부 | 클라우드/분산 환경 |
캐시 저장소 | EhCache, Redis, Memcached 등 지원 | EhCache (단일 JVM, 옵션에 따라 디스크) | Redis 또는 Memcached 엔진 사용 |
분산 지원 | 백엔드 캐시 의존 | 직접 지원하지 않음 | 분산 캐시 기본 지원 |
성능 | 메서드 호출 시 캐시 사용 | JVM 내에서 매우 빠름 | 네트워크를 통한 접근으로 약간의 레이턴시 |
운영 환경 | 코드 기반 관리 | 애플리케이션 내부에서만 동작 | AWS 클라우드 관리형 서비스 |
유연성 | 다양한 캐시 솔루션 연동 가능 | EhCache에 종속적 | Redis/Memcached의 다양한 기능 지원 |
사용 비용 | 오픈소스 기반 | 오픈소스 기반 | AWS 사용 요금 적용 |
5. 사용 추천 시나리오
- @Cacheable
- Spring 기반 애플리케이션에서 선언적으로 캐싱 로직을 간단히 추가하려는 경우.
- 다양한 캐시 제공자와 유연하게 연동하고 싶을 때.
- @EhCache
- 단일 JVM에서 작동하는 고성능 캐시가 필요할 때.
- 캐시 설정을 세밀하게 제어하고 싶을 때.
- AWS ElastiCache
- 클라우드 기반 분산 환경에서 캐싱을 구현해야 할 때.
- 고가용성과 확장성이 중요한 경우.
- AWS 인프라에서 애플리케이션을 운영 중일 때.
@Cacheable 도입
동일한 로직과 동일한 파라메터로 api 호출이 많을 경우엔 @Cacheable 도입이 적당하다고 판단.
1. 캐시 삭제를 위한 스케줄 도입 ( 30분 마다 초기화) - CacheResetScheduler.java
package com.lifefourcuts.api.task;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Slf4j
@RequiredArgsConstructor
@Component
public class CacheResetScheduler {
private boolean taskBatchEnabled = true;
private final CacheManager cacheManager;
@Scheduled(cron = "0 */30 * * * *") // 매 30분마다 실행
public void clearFrameResourceCache() {
if( taskBatchEnabled ) {
//초기화
Cache frameResourceCache = cacheManager.getCache("frameResource");
System.out.println("[CACHE-TASK] Clearing 'frameResource' cache start.");
if (frameResourceCache != null) {
frameResourceCache.clear(); // 캐시 초기화
System.out.println("[CACHE-TASK] Clearing 'frameResource' cache cleared successfully.");
}
Cache frameDetailCache = cacheManager.getCache("frameDetail");
System.out.println("[CACHE-TASK] Clearing 'frameDetail' cache start.");
if (frameDetailCache != null) {
frameDetailCache.clear(); // 캐시 초기화
System.out.println("[CACHE-TASK] Clearing 'frameResource' cache cleared successfully.");
}
Cache frameListCache = cacheManager.getCache("frameList");
System.out.println("[CACHE-TASK] Clearing 'frameList' cache start.");
if (frameListCache != null) {
frameListCache.clear(); // 캐시 초기화
System.out.println("[CACHE-TASK] Clearing 'frameList' cache cleared successfully.");
}
}
}
}
2.서비스 임플 단에서 캐시 데이터 도입 - VersionServiceImpl.java
// 버전 번호로 프레임 목록 조회
@Override
@Cacheable(value = "frameList", key = "#versionNo + '-' + #licenseDivision") // "id" 값을 key로 사용하여 캐시
public List<Frame> getAvailableFramesByVersion(Long versionNo, LicenseDivision licenseDivision) {
log.debug("### getAvailableFramesByVersion");
List<Frame> frames = new ArrayList<>();
QFrame qFrame = QFrame.frame;
QDtsAccntFrame qDtsAccntFrame = QDtsAccntFrame.dtsAccntFrame;
QDtsAccnt qDtsAccnt = QDtsAccnt.dtsAccnt;
QCtgryDtsAccnt qCtgryDtsAccnt = QCtgryDtsAccnt.ctgryDtsAccnt;
QCtgryAccnt qCtgryAccnt = QCtgryAccnt.ctgryAccnt;
QFrameAccnt qFrameAccnt = QFrameAccnt.frameAccnt;
QPpAccnt qPpAccnt = QPpAccnt.ppAccnt;
QMainAccnt qMainAccnt = QMainAccnt.mainAccnt;
QLicnsAccnt qLicnsAccnt = QLicnsAccnt.licnsAccnt;
QRcmdDtsCtntChoice qRcmdDtsCtntChoice = QRcmdDtsCtntChoice.rcmdDtsCtntChoice;
QRcmdDtsCtnt qRcmdDtsCtnt = QRcmdDtsCtnt.rcmdDtsCtnt;
QRcmdCtntAccnt qRcmdCtntAccnt = QRcmdCtntAccnt.rcmdCtntAccnt;
// 배포 중인 버전에 속한 프레임은 프레임의 운영기간, IP의 계약기간 및 삭제 유무는 체크하지 않음.
List<Frame> categoryAccountFrames = jpaQueryFactory.select(qFrame)
.from(qFrame)
.join(qDtsAccntFrame).on(qFrame.frameId.eq(qDtsAccntFrame.frameId))
.join(qDtsAccnt).on(qDtsAccntFrame.dtsAccntNo.eq(qDtsAccnt.dtsAccntNo))
.join(qCtgryDtsAccnt).on(qDtsAccnt.dtsAccntNo.eq(qCtgryDtsAccnt.dtsAccntNo))
.join(qCtgryAccnt).on(qCtgryDtsAccnt.ctgryAccntNo.eq(qCtgryAccnt.ctgryAccntNo))
.join(qMainAccnt).on(qCtgryAccnt.mainAccntNo.eq(qMainAccnt.mainAccntNo))
.join(qLicnsAccnt).on(qMainAccnt.licnsAccntNo.eq(qLicnsAccnt.licnsAccntNo))
.where(
qLicnsAccnt.licnsDiv.eq(licenseDivision.getCode())
.and(qLicnsAccnt.verNo.eq(versionNo))
.and(qDtsAccnt.accntTy.eq(DetailAccountType.FRAME.getCode()))
).fetch();
log.debug("### categoryAccountFrames:{}", categoryAccountFrames.size());
List<Frame> frameAccountFrames = jpaQueryFactory.select(qFrame)
.from(qFrame)
.join(qDtsAccntFrame).on(qFrame.frameId.eq(qDtsAccntFrame.frameId))
.join(qDtsAccnt).on(qDtsAccntFrame.dtsAccntNo.eq(qDtsAccnt.dtsAccntNo))
.join(qFrameAccnt).on(qDtsAccnt.dtsAccntNo.eq(qFrameAccnt.dtsAccntNo))
.join(qMainAccnt).on(qFrameAccnt.mainAccntNo.eq(qMainAccnt.mainAccntNo))
.join(qLicnsAccnt).on(qMainAccnt.licnsAccntNo.eq(qLicnsAccnt.licnsAccntNo))
.where(
qLicnsAccnt.licnsDiv.eq(licenseDivision.getCode())
.and(qLicnsAccnt.verNo.eq(versionNo))
.and(qDtsAccnt.accntTy.eq(DetailAccountType.FRAME.getCode()))
).fetch();
log.debug("### frameAccountFrames:{}", frameAccountFrames.size());
List<Frame> ppAccountFrames = jpaQueryFactory.select(qFrame)
.from(qFrame)
.join(qDtsAccntFrame).on(qFrame.frameId.eq(qDtsAccntFrame.frameId))
.join(qDtsAccnt).on(qDtsAccntFrame.dtsAccntNo.eq(qDtsAccnt.dtsAccntNo))
.join(qPpAccnt).on(qDtsAccnt.dtsAccntNo.eq(qPpAccnt.dtsAccntNo))
.join(qMainAccnt).on(qPpAccnt.mainAccntNo.eq(qMainAccnt.mainAccntNo))
.join(qLicnsAccnt).on(qMainAccnt.licnsAccntNo.eq(qLicnsAccnt.licnsAccntNo))
.where(
qLicnsAccnt.licnsDiv.eq(licenseDivision.getCode())
.and(qLicnsAccnt.verNo.eq(versionNo))
.and(qDtsAccnt.accntTy.eq(DetailAccountType.FRAME.getCode()))
).fetch();
log.debug("### ppAccountFrames:{}", ppAccountFrames.size());
List<Frame> recommendContentAccountFrames = jpaQueryFactory.select(qFrame)
.from(qFrame)
.join(qRcmdDtsCtntChoice).on(qFrame.frameId.eq(qRcmdDtsCtntChoice.mmtSrnId))
.join(qRcmdDtsCtnt).on(qRcmdDtsCtntChoice.rcmdDtsCtntNo.eq(qRcmdDtsCtnt.rcmdDtsCtntNo))
.join(qRcmdCtntAccnt).on(qRcmdDtsCtnt.rcmdCtntAccntNo.eq(qRcmdCtntAccnt.rcmdCtntAccntNo))
.join(qMainAccnt).on(qRcmdCtntAccnt.mainAccntNo.eq(qMainAccnt.mainAccntNo))
.join(qLicnsAccnt).on(qMainAccnt.licnsAccntNo.eq(qLicnsAccnt.licnsAccntNo))
.where(
qLicnsAccnt.licnsDiv.eq(licenseDivision.getCode())
.and(qLicnsAccnt.verNo.eq(versionNo))
.and(qRcmdDtsCtntChoice.mmtSrnTy.eq(RecommendNextScreenType.FRAME.getCode()))
).fetch();
log.debug("### recommendContentAccountFrames:{}", recommendContentAccountFrames.size());
Map<String,Frame> map = new HashMap<>();
if(CollectionUtils.isNotEmpty(categoryAccountFrames)) {
categoryAccountFrames.forEach(f -> {
log.debug("### CTGRY:{}", f.getFrameId());
if( !map.containsKey(f.getFrameId()))
map.put(f.getFrameId(),f);
});
}
if(CollectionUtils.isNotEmpty(frameAccountFrames)) {
frameAccountFrames.forEach(f -> {
log.debug("### FRAME:{}", f.getFrameId());
if( !map.containsKey(f.getFrameId()))
map.put(f.getFrameId(),f);
});
}
if(CollectionUtils.isNotEmpty(ppAccountFrames)) {
ppAccountFrames.forEach(f -> {
log.debug("### PP:{}", f.getFrameId());
if( !map.containsKey(f.getFrameId()))
map.put(f.getFrameId(),f);
});
}
if(CollectionUtils.isNotEmpty(recommendContentAccountFrames)) {
recommendContentAccountFrames.forEach(f -> {
log.debug("### RECNT:{}", f.getFrameId());
if( !map.containsKey(f.getFrameId()))
map.put(f.getFrameId(),f);
});
}
if(!map.isEmpty()) {
frames = new ArrayList<>(map.values());
}
return frames;
}
// 버전 번호로 프레임 목록 조회
@Override
@Cacheable(value = "frameDetail", key = "#versionNo + '-' + #licenseDivision") // "id" 값을 key로 사용하여 캐시
public List<FrameDetailDTO> getFrameDetailListByVersion(Long versionNo, LicenseDivision licenseDivision , String nationCode, LocalDateTime reqDate) {
log.debug("### getFrameDetailListByVersion");
List<FrameDetailDTO> returnDetailList = new ArrayList<>();
List<Map<String, Object>> frameInfoList = frameRepository.getFrameDetailListByVersion(versionNo, licenseDivision.getCode() , nationCode, reqDate);
for(int i = 0 ; i < frameInfoList.size() ; i++){
FrameDetailDTO frameDetail = new FrameDetailDTO(frameInfoList.get(i));
returnDetailList.add(frameDetail);
}
return returnDetailList;
}
@Override
@Cacheable(value = "frameResource", key = "#versionNo + '-' + #licenseDivision") // "id" 값을 key로 사용하여 캐시
public List<Map<String,Object>> getFrameResourceListByVersion(Long versionNo, LicenseDivision licenseDivision , String nationCode, LocalDateTime reqDate) {
log.debug("### getFrameResourceListByVersion");
//List<Map<String, Object>> returnDetailList = frameRepository.getFrameDetailListByVersion(versionNo, licenseDivision.getCode() , nationCode, reqDate);
List<Map<String, Object>> returnDetailList = new ArrayList<>();
if(bucket.equals("release-renewal-s3")){
// relase
returnDetailList = frameRepository.getFrameResourceListByVersionReplaceRelease(versionNo, licenseDivision.getCode() , reqDate);
}else if(bucket.equals("field-renewal-s3")){
// field
returnDetailList = frameRepository.getFrameResourceListByVersionReplaceFiled(versionNo, licenseDivision.getCode() , reqDate);
}else{
// local
returnDetailList = frameRepository.getFrameResourceListByVersion(versionNo, licenseDivision.getCode() , reqDate);
}
return returnDetailList;
}
'개발공부 > API' 카테고리의 다른 글
잘된 REST API 설계 vs 잘못된 REST API 설계 (0) | 2025.02.24 |
---|---|
REST API / REST / RESTful API 란 ? (0) | 2025.02.24 |
RESTful API 이란? (0) | 2025.02.13 |
REST, REST API, RESTful 특징 (0) | 2025.02.13 |