
캐쉬에는 일반적으로 2가지 종류로 분류할 수 있다.
로컬 캐쉬 - 글로벌 캐쉬 와 같이 말이다.
해당 포스팅에서는 Spring Boot에서 로컬 캐쉬로 가장 많이 사용되고 빠르고 편한 Caffeine Cache 사용 내용을 작성해보고자 한다.
영화 리뷰 플랫폼을 개발할 때 차별화 콘텐츠인 리워드 콘텐츠 서비스를 개발하고자 했다.
여기서 주안점은 2가지이다.
1. 뱃지 필요 포인트는 정적이기 때문에 DB에 일일이 접근하는 것은 비효율적이라고 판단.
2. 리뷰 작성, 매치업 승리 시 지급되는 포인트도 정적이기 때문에 DB 접근은 비효율적이다.
그래서 뱃지 필요 포인트 와 액션 포인트 를 로컬 캐쉬에 저장하여 빠르게 접근하고 포인트를 지급하고, 뱃지를 지급하는 로직을 구성해야했다.
여기서 일단 가장 중요한 것은 캐쉬 사용 용도가 글로벌로 전체 사용 되는 것인지 아닌지 판단을 해야한다.
서버가 2대일 때 싱크의 정합성을 위해 Redis를 사용하는 것이 맞다.
하지만, 우리 서비스는 단일 서버였기 때문에 Redis를 고려하지 않았다.
그래서 로컬 캐쉬로 충분히 대안이 가능하다는 판단하에 Caffeine Cache를 선택했다.
위 포스팅을 참고하면 유용할 것이다 😃
로직은 이렇다.
유저가 액션을 취하면 해당 유저의 현재 포인트를 DB에서 조회해서 액션 포인트를 합산하여 업데이트하고, 뱃지 필요 포인트와 비교하여 지급 처리 여부 로직을 수행한다.

유저 100명이 동시에 리뷰 작성을 한다면 모두 DB를 조회해야하니 부하가 우려되고 성능에도 악영향이 미칠 것이다.
먼저 카페인 캐쉬 의존성을 설정해주고 Cache Confing를 작성했다.
@Configuration
public class CacheConfig {
@Bean(name = "pointLivedCacheManager")
public CacheManager pointCacheManager(@Qualifier("pointLivedCache") Caffeine<Object, Object> caffeine) {
CaffeineCacheManager cacheManager = new CaffeineCacheManager("pointLivedCache");
cacheManager.setCaffeine(caffeine);
return cacheManager;
}
}
그 다음 Service 레이어에서 각각 포인트를 조회하는 로직을 작성했다.
@Slf4j
@Service
@RequiredArgsConstructor
public class RewardCacheService {
private final BadgeActionRepository badgeActionRepository;
private final BadgeRepository badgeRepository;
@Cacheable(value = "pointLivedCache", key = "'action:' + #actionName", cacheManager = "pointLivedCacheManager")
public Long getActionPointByCache(String actionName) {
log.info("최초 DB 에서 조회 -> actionName: {} from DB", actionName);
return badgeActionRepository.findBadgeActionByName(actionName)
.map(BadgeAction::getActionPoint)
.orElseThrow(() -> new IllegalArgumentException("해당 action name 이 존재하지 않습니다."));
}
@Cacheable(value = "pointLivedCache", key = "'badge:' + #grade", cacheManager = "pointLivedCacheManager")
public Long getBadgeNeedPointByGrade(String grade) {
log.info("최초 DB 에서 조회 -> badge: {} from DB", grade);
return badgeRepository.findBadgeByGrade(grade)
.map(Badge::getNeedPoint)
.orElseThrow(() -> new IllegalArgumentException("해당 뱃지 등급이 존재하지 않습니다."));
}
}
위 코드에서는 하나의 캐쉬 매니저에서 관리하며, key를 던져 검증하는 로직이다.
cache가 존재하면, 바로 key값의 value를 return한다.
cache가 없으면 DB에서 조회하고, 캐쉬에 값을 저장한다.
예를들어보자.
key값이 action일 때 actionName 이 review 일 경우 해당 포인트를 조회한다.
action:review = 100
action:match = 300
이런식으로 말이다.
정말 간단하게 작성할 수 있었다.
자 그럼 이제 뱃지 포인트가 업데이트 됐을 때 검증 로직이 필요하지 않겠는가?
@Service
@Slf4j
@RequiredArgsConstructor
public class RewardPointService {
private final RewardCacheService rewardCacheService;
private final RewardHistoryService rewardHistoryService;
private final UserBadgeRepository userBadgeRepository;
private final BadgeAccumulationPointRepository badgeAccumulationPointRepository;
private final BadgeRepository badgeRepository;
@Transactional
public void updateBadgePointAndBadgeObtain(User user, String actionName, String movieName) {
Long actionPoint = rewardCacheService.getActionPointByCache(actionName);
BadgeAccumulationPoint userPoint = badgeAccumulationPointRepository.findByUserId(user.getId());
Long currentUserPoint = userPoint.getAccPoint();
Long sumPoint = currentUserPoint + actionPoint;
userPoint.updateAccPoint(sumPoint);
rewardHistoryService.createRewardHistoryByUser(user, actionName, actionPoint, movieName);
checkAndAssignBadge(user, sumPoint);
}
private void checkAndAssignBadge(User user, Long sumPoint) {
Map<String, Long> badgeNeedPoints = Map.of(
"normal", rewardCacheService.getBadgeNeedPointByGrade("normal"),
"rare", rewardCacheService.getBadgeNeedPointByGrade("rare"),
"epic", rewardCacheService.getBadgeNeedPointByGrade("epic")
);
for (Map.Entry<String, Long> entry : badgeNeedPoints.entrySet()) {
String grade = entry.getKey();
Long needPoint = entry.getValue();
if (sumPoint >= needPoint && !hasBadge(user.getId(), grade)) {
assignBadgeToUser(user, grade);
break;
}
}
}
private boolean hasBadge(Long userId, String grade) {
return userBadgeRepository.findByUserIdAAndBadgeGrade(userId, grade).isPresent();
}
private void assignBadgeToUser(User user, String grade) {
Badge badge = badgeRepository.findBadgeByGrade(grade)
.orElseThrow(() -> new IllegalArgumentException("해당 등급의 뱃지가 존재하지 않습니다."));
UserBadge userBadge = UserBadge.builder()
.user(user)
.badge(badge)
.isMain(false)
.build();
userBadgeRepository.save(userBadge);
}
}
먼저 해당 유저의 DB를 조회해 포인트를 확인하고, 캐쉬에서 액션 포인트를 가져온다.
합산 후 업데이트 한 뒤에 비교를 수행한다.
각각 뱃지 별 필요 포인트를 캐쉬로 조회한다. ( normal, rare, epic )
지급 포인트가 충족 됐는지? 아니면 이미 보유중인지? 각 판단 후 지급 처리한다.
자 그럼 이제 리뷰를 작성하면 log에 최초 DB 조회라는 로그가 발생할 것이다.

최초 리뷰 작성 시 DB에 접근한다.
그리고 재 작성한다면 이제 cache에서 접근할 것이니 해당 로그는 출력되지 않는다.

로컬 캐쉬인 Caffeine Cache 로 서비스에 적용하는 것을 포스팅 해봤다.
아마 서버가 2대 이상이거나 글로벌하게 관리하고 싱크의 정합성을 위해서라면 Redis를 도입해야할 것이다.
충분히 로컬로 서비스에 사용할 수 있다면 개발 비용을 높이지 않도록 로컬 캐쉬를 사용하여 좋은 서비스를 개발하는게 중요한거 같다 ✅