@Repository
@RequiredArgsConstructor
public class ExampleContentRepository implements ExampleContentAdapter {
private final ExampleContentJpaRepository jpaRepository;
private final RedisTemplate<String, Object> redisTemplate;
private final long CACHE_TTL = TimeUnit.HOURS.toSeconds(24); // 24시간 TTL
@Override
public List<ExampleDto> findByTypeAndAge(ContentType type, int age) {
String cacheKey = "example:content:type:" + type.name() + ":age:" + age;
// 1. 캐시에서 조회
List<ExampleDto> cached = (List<ExampleDto>) redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
return cached;
}
// 2. 캐시에 없으면 DB 조회
List<ExampleEntity> entities = jpaRepository.findByTypeAndAge(type, age);
List<ExampleDto> result = entities.stream()
.map(e -> new ExampleDto(e.getId(), e.getType(), e.getAge(), e.getRank(), e.getName()))
.toList();
// 3. 캐시에 저장 (Lazy Loading)
redisTemplate.opsForValue().set(cacheKey, result, CACHE_TTL, TimeUnit.SECONDS);
return result;
}
}
예를 들어, 어떤 사용자 행동을 저장하는 API에서 동시 요청이 발생할 경우 다음과 같은 문제가 발생할 수 있습니다:
동의서 제출 여부
를 true로 변경이메일 인증 여부
를 true로 변경false, false
)true, false
를 저장false, true
를 저장true, false
또는 false, true
)WATCH
를 사용하여 값이 변경되었는지 확인 후 저장MULTI/EXEC
를 사용하여 트랜잭션처럼 실행public void updateUserAction(UUID userId, boolean consent, boolean emailVerified) {
String key = "example:user-action:" + userId;
redisTemplate.execute((RedisCallback<Void>) connection -> {
connection.watch(key.getBytes());
byte[] raw = connection.get(key.getBytes());
ExampleUserAction userAction = raw == null
? jpaRepository.findByUserId(userId)
: deserialize(raw);
userAction.setConsent(consent || userAction.isConsent());
userAction.setEmailVerified(emailVerified || userAction.isEmailVerified());
connection.multi();
connection.set(key.getBytes(), serialize(userAction));
connection.exec();
return null;
});
jpaRepository.save(userAction);
}
WATCH
및 MULTI/EXEC
방식은 단일 Redis 노드 환경에서는 유용하지만, 클러스터링된 Redis 환경에서는 적용이 어렵다.public void updateUserAction(UUID userId, boolean consent, boolean emailVerified) {
RLock lock = redissonClient.getLock("lock:user-action:" + userId);
try {
if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {
ExampleUserAction action = jpaRepository.findByUserId(userId);
if (consent) action.setConsent(true);
if (emailVerified) action.setEmailVerified(true);
jpaRepository.save(action);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
Look-Aside + Write-Around 전략은 대량의 정적 데이터를 다룰 때 효율적이며, 읽기 성능을 최적화하는 데 매우 유리합니다. 하지만 캐시 전략만으로는 모든 문제를 해결할 수 없습니다. 사용자 상태나 변경 정보처럼 동시에 변경될 수 있는 데이터는 Optimistic Lock 또는 분산 락과 같은 명시적 동시성 제어가 필수입니다.
이 두 가지를 적절히 조합하면 캐시 성능과 데이터 정합성을 모두 만족하는 설계를 만들 수 있습니다.