캐시 레이어
데이터를 찾는다고 하면 대부분은 DB에서 조회하게 됩니다.
하지만 동일한 데이터에 대해 반복적인 읽기를 요청할 경우 DB에 부하를 주는 문제가 발생합니다.
그렇다면 데이터를 미리 기억하고 빠르게 데이터에 접근할 수 있다면 성능을 높일 수 있지 않을까요?
이렇게 데이터베이스에 접근하지 않고 빠른 접근이 가능한 저장소를 캐시 레이어라고 합니다.
캐시 종류
카페인 캐시 : 애플리케이션 프로세스 내부 메모리에 데이터를 저장하는 라이브러리
Redis : 애플리케이션 외부의 별도 서버에 데이터를 저장하는 메모리 기반 저장소
AWS 엘라스틱 캐시 : AWS에서 제공하는 관리형 캐시 서비스로 AWS가 대신 운영, 백업을 해주는 서비스
UpStach
캐시 데이터를 여러 서버에서 접근해야 한다면 외부 캐시 (Redis, 엘라스틱 캐시)를 사용해야 합니다.
하지만 엘라스틱 캐시는 비용이 발생하기 때문에 비용을 아끼기 위해서 다른 서비스를 찾아보았습니다.
UpStash는 레디스 기능을 엔드 포인트로 제공합니다.
하루 1만 건의 쓰기, 읽기를 제공하기 때문에 간단한 프로젝트에 쓰기에 적합하다고 생각했습니다.
Redis 생성
회원 가입 후 프리티어를 선택하고 Redis DB를 생성합니다.
리전은 한국이 없으므로 가까운 도쿄로 선택하였습니다.

데이터베이스를 생성하면 엔드 포인트를 제공합니다.
엔드 포인트 중 '****' 제공된 부분이 비밀번호이며 연결을 위해 설정 파일에 작성하면 됩니다.

Redis 연결 설정
키는 String 값은 Json으로 직렬화 하였습니다.
@EnableCaching
@Configuration
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private int port;
@Value("${spring.data.redis.password}")
private String password;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(host, port);
config.setPassword(password);
LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
.useSsl()
.build();
return new LettuceConnectionFactory(config, clientConfig);
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory cf) {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(objectMapper);
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer))
.entryTtl(Duration.ofMinutes(1));
return RedisCacheManager.RedisCacheManagerBuilder
.fromConnectionFactory(cf)
.cacheDefaults(redisCacheConfiguration)
.build();
}
}
캐싱 전략
[ Cache Aside ( 읽기 ) + Write Around ( 쓰기 ) ]
대표적으로 쓰는 캐시 전략 중 하나입니다.
읽을 때는 캐시를 먼저 확인 후 없다면 DB에서 읽은 후 해당 데이터를 캐시에 저장합니다.
쓸때는 DB에 바로 저장하는 방식입니다. 해당 전략의 장점은 캐시 서버에 문제가 발생하여도 DB에서
데이터를 읽으면 되기 때문에 장애 전파가 되지 않는다는 점이 장점입니다.
단점은 캐시와 DB간의 정합성이 맞지 않을 수 있기 때문에 캐시 무효화 작업이 필요합니다.


[ Read Through ( 읽기 ) + Write Through ( 쓰기 ) ]
읽을 때는 캐시를 통해서만 읽고 데이터를 쓸 때는 캐시에 저장한 후 DB에 저장합니다.
장점은 DB에 저장하기 전에 캐시에 저장하기 때문에 읽을 때 정합성이 맞습니다.
정합성이 맞기 때문에 따로 캐시 무효화 작업을 해주지 않아도 됩니다.
단점은 캐시를 통해서만 읽기 때문에 캐시 서버에 장애가 발생 시 서버 장애로 이어질 수 있습니다.


프로젝트 캐시 적용
프로젝트에서 사용자는 모임 목록들을 조회할 수 있습니다.

첫페이지는 조회가 빈번하므로 첫 페이지를 캐시 한다면 성능이 좋아진다고 판단하여 적용하였습니다.
캐싱 전략은 서버의 안정성을 위해 Cache Aside + Write Around 전략을 사용하고
캐시와 DB 정합성이 맞지 않는 상황을 대비하기 위해 모임 등록, 수정, 삭제 시 캐시를 무효화하였습니다.
캐시 정합성 문제
레디스 캐시를 사용할 때 @Cacheable, @CachePut 등 어노테이션을 사용하게 되는데 멀티스레드 환경에서
정합성 문제가 발생할 수 있습니다. 한 회원은 조회하고 한 회원은 엔티티 수정을 요청한 경우 입니다.
예시) @Cacheable ( 조회 ), @CacheEvict ( 엔티티 수정 )

조회는 @Cacheable을 통해 캐시를 조회한 후 캐시 미스가 발생 시 DB 조회 후 결과 값을 캐시에 저장합니다.
수정은 @CacheEvict을 사용하며 엔티티를 수정하고 캐시를 삭제하는 상황입니다.
만약 멀티스레드 환경에서 두 메서드가 같이 실행된다면 캐시에는 이름이 "A"인 엔티티가 저장 되지만
실제 DB에는 이름이 "B"인 엔티티가 있는 정합성 문제가 발생합니다.
해결 방안
1. 레디스 분산 락
키에 대한 락을 획득 후 차례대로 실행한다면 정합성 문제를 해결 가능하지만
락 획득을 위한 네트워크 비용이 발생하여 성능이 좋지 않습니다.
2. TTL ( Time To Live ) 시간 단축
TTL 시간을 짧게 설정하여 캐시 갱신을 자주 하는 방법입니다.
레디스 분산락은 마지막 수단이라 생각하여 TTL (1분)을 짧게 단축하여 정합성 문제를 해결하고자 하였습니다.
결과
캐시 전 0.13초 성능에서 0.05초로 성능이 개선된 것을 확인할 수 있습니다.



[ 🌱 참고 블로그 ]
잘못된 캐싱 전략이 당신의 서비스를 망치고 있습니다
캐싱을 활용한 확실한 성능 개선 비법 대공개
maily.so
[REDIS] 📚 캐시(Cache) 설계 전략 지침 💯 총정리
Redis - 캐시(Cache) 전략 캐싱 전략은 웹 서비스 환경에서 시스템 성능 향상을 기대할 수 있는 중요한 기술이다. 일반적으로 캐시(cache)는 메모리(RAM)를 사용하기 때문에 데이터베이스 보다 훨씬 빠
inpa.tistory.com
카카오페이 온라인 결제 서비스 2.5배 성능 개선기 | 카카오페이 기술 블로그
카카오페이 온라인 결제 시스템의 성능 개선 기록을 공유합니다.
tech.kakaopay.com
캐시 문제 해결 가이드 - DB 과부하 방지 실전 팁
대용량 트래픽 환경에서 캐시를 사용할 때 주의해야할 위험 상황과 예방법을 소개합니다.
toss.tech
'DB' 카테고리의 다른 글
| [ MySQL ] FullText Index (0) | 2025.09.16 |
|---|---|
| [ DB ] 인덱스를 통한 성능 개선 (3) | 2025.08.13 |
