Redis는 초저지연(1ms 미만)과 다양한 자료구조 지원 덕분에 캐시, 세션 관리, 분산 락, 작업 큐, 실시간 순위 집계 등 정말 다양한 패턴을 소화합니다.
저는 프로젝트에서 주로 사용자 토큰 관리 같은 특정 기간이 지나면 사라지는 데이터들을 저장하는 용도로 자주 사용했던 것 같습니다.
많은 개발자가 Redis를 선호하고 있는데 왜 redis를 사용할까요?
이번 글에서는 자주 쓰이는 Redis 활용 패턴 7가지 중 Cache-Aside, 분산 락, Rate Limiting에 대해 정리하려 합니다.
언제 이 패턴을 쓰고, 왜 필요한지, 어떻게 구현하는지를 예제 코드와 함께 살펴보겠습니다. 예제는 모두 Spring Boot + Spring Data Redis 사용을 기본으로 합니다.
DB를 원본 (Source of Truth)으로 두고, 조회할 때만 캐시에 적재하는 가장 표준적인 캐싱 전략

기본 동작 흐름
1. 캐시에서 데이터 조회
2. 없으면 DB에서 조회
3. 조회 결과를 캐시에 저장 (TTL 포함)
4. 결과 반환
[의존성 + 설정]
implementation("org.springframework.boot:spring-boot-starter-data-redis")
// JPA
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("com.fasterxml.jackson.core:jackson-databind")
// RedisConfig.java
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory cf) {
RedisTemplate<String, Object> tpl = new RedisTemplate<>();
tpl.setConnectionFactory(cf);
var serializer = new GenericJackson2JsonRedisSerializer();
tpl.setKeySerializer(new StringRedisSerializer());
tpl.setValueSerializer(serializer);
tpl.setHashKeySerializer(new StringRedisSerializer());
tpl.setHashValueSerializer(serializer);
tpl.afterPropertiesSet();
return tpl;
}
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory cf) {
return new StringRedisTemplate(cf);
}
}
[서비스 : Cache-Aside]
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final RedisTemplate<String, Object> redisTemplate;
private static final Duration TTL = Duration.ofMinutes(10);
private String keyForUser(Long id) { return "user:" + id; }
@Transactional(readOnly = true)
public UserDto getUser(Long id) {
String key = keyForUser(id);
// 1) 캐시 조회
Object cached = redisTemplate.opsForValue().get(key);
if (cached != null) return (UserDto) cached;
// 2) DB 조회
User user = userRepository.findById(id)
.orElseThrow(() -> new NotFoundException("user not found"));
UserDto dto = UserDto.from(user);
// 3) 캐시에 저장 + TTL
redisTemplate.opsForValue().set(key, dto, TTL);
// 4) 응답
return dto;
}
}
1) 캐시 스탬피드 (Thundering Herd)
TTL이 동시에 만료되면 다수의 요청이 한번에 DB로 몰립니다. 이를 해결하기 위해 잠금 / Redission 락, Soft TTL + Hard TTL을 활용할 수 있습니다.
// Redisson 사용 예
@Service
@RequiredArgsConstructor
public class UserServiceWithLock {
private final UserRepository userRepository;
private final RedisTemplate<String, Object> redisTemplate;
private final RedissonClient redisson;
private static final Duration TTL = Duration.ofMinutes(10);
@Transactional(readOnly = true)
public UserDto getUser(Long id) {
String key = "user:" + id;
Object cached = redisTemplate.opsForValue().get(key);
if (cached != null) return (UserDto) cached;
RLock lock = redisson.getLock("lock:" + key);
boolean locked = false;
try {
locked = lock.tryLock(200, 5_000, TimeUnit.MILLISECONDS); // 대기 200ms, 보유 5s
// 잠금 획득 후 다시 캐시 확인(중복 DB 접근 방지)
cached = redisTemplate.opsForValue().get(key);
if (cached != null) return (UserDto) cached;
User user = userRepository.findById(id).orElseThrow();
UserDto dto = UserDto.from(user);
redisTemplate.opsForValue().set(key, dto, TTL);
return dto;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
} finally {
if (locked) lock.unlock();
}
}
}
fetchedAt을 같이 저장한 후 Soft TTL 비동기 리프레시 수행2) 캐시 관통 (Cache Penetration)
존재하지 않는 키를 계속 요청 : DB 성능 타격
NULL도 짧은 TTL로 캐시redisTemplate.opsForValue().set(key, NullMarker.INSTANCE, Duration.ofSeconds(30));
Cache-Aside에선 Write 시 캐시를 직접 갱신하지 않고 무효화를 하는 것이 일반적입니다.
@Transactional
public void updateUserName(Long id, String name) {
userRepository.updateName(id, name); // DB 정본
redisTemplate.delete("user:" + id); // 캐시 무효화
}
@Transactional
public void updateUserName(Long id, String name) {
userRepository.updateName(id, name);
String key = "user:" + id;
redisTemplate.delete(key); // 1차 삭제
// 약간 지연 후 2차 삭제 (스케줄러/메시지 큐/간단한 sleep 등)
CompletableFuture.runAsync(() -> {
try { Thread.sleep(200); } catch (InterruptedException ignored) {}
redisTemplate.delete(key); // 2차 삭제
});
}
| 항목 | @Cacheable | 수동 구현 |
|---|---|---|
| 구현 속도 | 빠름 | 느림 |
| 세밀 제어 | 제한적 | 자유 |
| 스탬피드 방지 | 직접 구현 필요 | 가능 |
| 무효화 제어 | 제한적 | 가능 |
분산 락은 여러 서버 인스턴스가 동시에 접근하는 공유 자원을 한번에 하나의 작업만 처리하도록 제어하는 기술입니다. 쇼핑몰 재고 관리, 예약 시스템 좌석 배정, 결제 중복 방지 등에 사용됩니다.
Redis 기반 분산 락 특징

동작 흐름
1. 락 획득 시도 -> 키 없으면 생성 (SET NX) + 만료 시간 설정 (EX)
2. 작업 수행
3. 락 해제 -> 락 소유자만 해제
[서비스 - 분산 락 SET NX EX]
@Service
@RequiredArgsConstructor
public class InventoryService {
private final RedisTemplate<String, String> redisTemplate;
private static final String LOCK_KEY = "lock:inventory";
private static final Duration LOCK_TTL = Duration.ofSeconds(5);
public boolean tryLock(String lockValue) {
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(LOCK_KEY, lockValue, LOCK_TTL);
return Boolean.TRUE.equals(success);
}
public void unlock(String lockValue) {
String currentValue = redisTemplate.opsForValue().get(LOCK_KEY);
if (lockValue.equals(currentValue)) {
redisTemplate.delete(LOCK_KEY);
}
}
public void processOrder(Long productId) {
String lockValue = UUID.randomUUID().toString();
if (!tryLock(lockValue)) throw new RuntimeException("다른 서버에서 처리 중입니다.");
try {
// 재고 차감 로직
} finally {
unlock(lockValue);
}
}
}
[Redisson 기반 구현]
@Service
@RequiredArgsConstructor
public class InventoryServiceWithRedisson {
private final RedissonClient redisson;
public void processOrder(Long productId) {
RLock lock = redisson.getLock("lock:inventory:" + productId);
boolean acquired = false;
try {
// 최대 200ms 대기, 락 점유 5초, Watchdog이 자동 연장
acquired = lock.tryLock(200, 5, TimeUnit.SECONDS);
if (!acquired) throw new RuntimeException("다른 서버에서 처리 중입니다.");
// 재고 차감 로직
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
} finally {
if (acquired) lock.unlock();
}
}
}
락 해제 시 반드시 락 소유자만 삭제해야한다. (다른 스레드 락 오염 방지)
lock:{resourceId}Rate Limiting은 일정 시간 동안 허용되는 요청 횟수를 제한하는 기법이다. 서버 자원 보호, API 남용 방지, 보안 강화 등에 활용된다.

효도란 무엇일까요. 그건 임영웅 콘서트 예매입니다. 물론 278469번째는 효도를 할 수 없습니다.
이런 상황에서 Rate Limiting이란 기법을 사용할 수 있습니다. 수 많은 사람이 콘서트 예매를 위해 몰린다 생각하면 백엔드 개발자는 머리가 아플 수 밖에 없습니다.
로그인 시도 제한, 구매 API 초당 호출 수 제한, SMS 인증 요청 제한 등 모든 과정의 API를 얼마나 제한해야할지를 고민해야합니다.
Redis 기반 Rate Limiting 특징
1. Fixed Window
// 예: user:login:12345 → 60초 동안 5회 제한
Boolean exists = redisTemplate.hasKey(key);
Long count = redisTemplate.opsForValue().increment(key);
if (Boolean.FALSE.equals(exists)) {
redisTemplate.expire(key, Duration.ofSeconds(60));
}
if (count > 5) {
throw new RuntimeException("요청 횟수 초과");
}
2. Sliding Widnow (Sorted Set)
public boolean isAllowed(String userId) {
String key = "rate:" + userId;
long now = System.currentTimeMillis();
long windowMillis = 60_000;
long limit = 5;
ZSetOperations<String, String> zSetOps = redisTemplate.opsForZSet();
// 현재 요청 시간 기록
zSetOps.add(key, String.valueOf(now), now);
// 윈도우 밖 데이터 삭제
zSetOps.removeRangeByScore(key, 0, now - windowMillis);
// 현재 윈도우 요청 수 확인
Long count = zSetOps.zCard(key);
redisTemplate.expire(key, Duration.ofMillis(windowMillis));
return count <= limit;
}
3. Token Bucket
SET key value NX EX 또는 Lua 스크립트로 원자화rate:{userId}:{endPoint} 또는 rate:{IP}
그저 토큰 저장소로 사용했던 Redis가 서비스를 고도화할수록 얼마나 다양한 역할을 할 수 있는지 살펴봤습니다.
다음 글에서는 세션/토큰 스토어, 작업 큐, Pub/Sub, 리더보드 등 이번에 다루지 못한 나머지 패턴도 살펴보겠습니다.