백엔드 성능 최적화 - Redis를 활용한 리모트 캐싱
들어가며
백엔드 애플리케이션에서 성능 병목 현상이 발생하는 가장 흔한 원인 중 하나는 데이터베이스 조회입니다. 특히 동일한 데이터를 반복적으로 조회하거나, 복잡한 연산이 필요한 데이터를 매번 새로 계산하는 것은 시스템 전체의 성능을 크게 저하시킬 수 있습니다. 이런 문제를 해결하기 위한 핵심 기술이 바로 캐싱입니다.
캐싱이란 무엇인가
캐싱은 자주 사용되거나 계산 비용이 높은 데이터를 임시로 저장해두었다가, 동일한 요청이 들어올 때 빠르게 응답할 수 있도록 하는 기술입니다. 데이터베이스 조회 대신 메모리에서 데이터를 가져오므로 응답 속도가 현저히 빨라집니다.
Redis란
Redis(Remote Dictionary Server)는 메모리 기반의 키-값 저장소로, 다음과 같은 특징을 가지고 있습니다.
- 인메모리 데이터 구조 저장소: 모든 데이터를 메모리에 저장하여 빠른 읽기/쓰기 성능을 제공합니다
- 다양한 데이터 타입 지원: 문자열, 해시, 리스트, 집합, 정렬된 집합 등을 지원합니다
- 영속성 옵션: RDB 스냅샷이나 AOF 로그를 통해 데이터를 디스크에 저장할 수 있습니다
- 분산 환경 지원: 여러 애플리케이션 서버가 동일한 캐시를 공유할 수 있습니다
리모트 캐싱의 장점
1. 성능 향상
데이터베이스 조회 시간을 크게 단축시킬 수 있습니다. 일반적으로 메모리 접근은 디스크 접근보다 수백 배 빠릅니다.
2. 데이터베이스 부하 감소
반복적인 쿼리를 캐시로 처리하여 데이터베이스 서버의 부하를 줄일 수 있습니다.
3. 확장성
여러 애플리케이션 서버가 동일한 캐시를 공유하여 일관된 데이터를 제공할 수 있습니다.
실제 구현 예시
Spring Boot + Redis 설정
먼저 의존성을 추가합니다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Redis 설정을 구성합니다.
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public RedisConnectionFactory redisConnectionFactory() {
LettuceConnectionFactory factory = new LettuceConnectionFactory(
new RedisStandaloneConfiguration("localhost", 6379)
);
return factory;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.build();
}
}
서비스 클래스에서 캐싱 적용
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Cacheable(value = "users", key = "#userId")
public User getUserById(Long userId) {
// 데이터베이스에서 사용자 조회
return userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException("사용자를 찾을 수 없습니다"));
}
@CacheEvict(value = "users", key = "#user.id")
public User updateUser(User user) {
User updatedUser = userRepository.save(user);
return updatedUser;
}
@Cacheable(value = "user-posts", key = "#userId")
public List<Post> getUserPosts(Long userId) {
// 복잡한 쿼리나 연산이 필요한 경우
return postRepository.findPostsWithCommentsAndLikes(userId);
}
}
수동 캐시 제어
더 세밀한 제어가 필요한 경우 RedisTemplate을 직접 사용할 수 있습니다.
@Service
public class ProductService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private ProductRepository productRepository;
public Product getProduct(Long productId) {
String cacheKey = "product:" + productId;
// 캐시에서 조회
Product cachedProduct = (Product) redisTemplate.opsForValue().get(cacheKey);
if (cachedProduct != null) {
return cachedProduct;
}
// 데이터베이스에서 조회
Product product = productRepository.findById(productId)
.orElseThrow(() -> new ProductNotFoundException("상품을 찾을 수 없습니다"));
// 캐시에 저장 (30분 TTL)
redisTemplate.opsForValue().set(cacheKey, product, Duration.ofMinutes(30));
return product;
}
public void invalidateProductCache(Long productId) {
String cacheKey = "product:" + productId;
redisTemplate.delete(cacheKey);
}
}
캐싱 전략
1. Cache-Aside (Lazy Loading)
애플리케이션이 캐시를 직접 관리하는 방식입니다.
public Product getProduct(Long id) {
// 1. 캐시 확인
Product product = getFromCache(id);
if (product != null) {
return product;
}
// 2. 데이터베이스 조회
product = getFromDatabase(id);
// 3. 캐시에 저장
saveToCache(id, product);
return product;
}
2. Write-Through
데이터를 저장할 때 캐시와 데이터베이스에 동시에 저장하는 방식입니다.
public Product saveProduct(Product product) {
// 1. 데이터베이스 저장
Product savedProduct = productRepository.save(product);
// 2. 캐시 업데이트
saveToCache(savedProduct.getId(), savedProduct);
return savedProduct;
}
3. Write-Behind (Write-Back)
캐시에만 먼저 저장하고, 나중에 데이터베이스에 비동기적으로 저장하는 방식입니다.
주의사항
1. 캐시 무효화 전략
데이터가 변경될 때 캐시를 적절히 무효화하지 않으면 일관성 문제가 발생할 수 있습니다.
2. 메모리 사용량 관리
Redis는 메모리 기반이므로 적절한 TTL 설정과 메모리 사용량 모니터링이 필요합니다.
3. 캐시 워밍
애플리케이션 시작 시 중요한 데이터를 미리 캐시에 로드하는 전략을 고려해야 합니다.
4. 장애 대응
Redis 서버가 다운되었을 때의 fallback 전략을 마련해야 합니다.
public Product getProduct(Long id) {
try {
// Redis 캐시 조회 시도
Product product = getFromCache(id);
if (product != null) {
return product;
}
} catch (Exception e) {
// Redis 장애 시 로그 기록하고 데이터베이스로 fallback
log.warn("Redis 캐시 조회 실패, 데이터베이스에서 조회합니다", e);
}
// 데이터베이스에서 조회
return getFromDatabase(id);
}
마무리
Redis를 활용한 리모트 캐싱은 백엔드 애플리케이션의 성능을 크게 향상시킬 수 있는 핵심 기술입니다. 적절한 캐싱 전략과 함께 구현하면 사용자 경험 개선과 서버 자원 절약이라는 두 마리 토끼를 모두 잡을 수 있습니다. 다만 데이터 일관성과 장애 대응에 대한 충분한 고려가 필요하며, 지속적인 모니터링을 통해 캐시 효율성을 관리해야 합니다.