캐시
의 사전적 정의는 데이터를 미리 복사해 놓는 임시 장소를 의미합니다.
데이터를 미리 복사해 놓으면 계산이나 접근 시간없이 빠른 속도로 데이터에 접근할 수 있습니다.
현재 프로젝트에서 MySQL
데이터베이스를 사용하고 있는데, MySQL
은 데이터를 디스크에 저장합니다. 디스크 I/O는 메모리 접근보다 훨씬 느리기 때문에 조회 성능을 개선하기 위해 캐시를 사용할 수 있습니다.
캐시를 사용하여 클라이언트가 동일한 읽기 요청에 대해 데이터베이스를 매번 접근하지 않고, 메모리에 저장된 사본 데이터를 가져와 성능 개선
을 할 수 있습니다.
많은 사용자가 이용하는 서비스라면 캐시를 도입해 병목 현상을 개선할 수 있어 캐시를 도입하는 것이 좋다고 생각합니다.
캐시 관리 전략을 선택할 때, 캐시 데이터를 스토리지가 서버 안에 소유하고 있을지, 외부 서버에 캐시 저장소를 따로 둘지 선택해야 합니다.
특징 | Local Cache | Global Cache |
---|---|---|
범위 | 특정 서버/애플리케이션 인스턴스 내 | 여러 서버/애플리케이션 인스턴스 간 공유 |
속도 | 매우 빠름 | 네트워크 지연 발생 가능 |
운영 | 단순함 | 복잡함 |
일관성 | 인스턴스 간 동기화 어려움 | 일관성 보장 |
확장성 | 제한적 | 좋음 |
사용 사례 | 애플리케이션 내부 캐싱 | 분산 환경의 데이터 캐싱 |
Local Cache
는 캐시 데이터를 서버 메모리 상에 두기 때문에 속도가 빠릅니다. 외부에 캐시 데이터를 두게 될 경우, 데이터를 가저오는 과정에서 발생하는 오버헤드가 발생할 수 있습니다.
현재 진행중인 프로젝트는 분산 서버
로 구성될 예정이고 프로젝트 내에 데이터의 일관성
을 고려해야 하는 부분도 있어 Global Cache
를 적용할 것입니다.
Global Cache
의 종류로는 Memcached
와 Redis
가 있습니다.
Memcached
와 Redis
모두 In Memory 저장소 이며 Key-Value 저장방식을 지니고 있습니다.
또한, 무료 오픈소스이고 높은 응답 속도를 보여주기 때문에 대용량 트래픽을 고려하는 프로젝트에 적용하기 좋습니다.
특징 | Memcached | Redis |
---|---|---|
데이터구조 | 문자열 | 문자열, List, Set, Hash 등 |
성능 | 멀티 스레드, 높은 성능 | 단일 스레드, 비동기 I/O |
영속성 | 지원 X | 지원(RDB, AOF) |
복제 및 고가용성 | 지원 X (추가 설정 필요) | 마스터-슬레이브 복제, Multi-Master Replication 방식 지원 |
사용 사례 | 웹 페이지, 데이터베이스 캐싱 | 다양한 데이터 구조 캐싱, 메시지 브로커, 실시간 분석 등 |
Redis
는 Memcached
에 비해 쓰기 성능은 떨어집니다.
하지만 다양한 데이터 타입 지원
과 세션 저장소
, 분산 락
과 같은 다양한 기능을 활용할 수 있기 때문에 Redis
를 활용할 것입니다.
@Configuration
public class RedisConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new StringRedisSerializer());
redisTemplate.setDefaultSerializer(new StringRedisSerializer());
return redisTemplate;
}
}
@EnableCaching
@Configuration
public class RedisCacheConfig {
@Bean
public CacheManager rcm(RedisConnectionFactory cf) {
RedisCacheConfiguration redisCacheConfiguration = generateCacheConfiguration()
.entryTtl(Duration.ofSeconds(120));
return RedisCacheManager
.RedisCacheManagerBuilder
.fromConnectionFactory(cf)
.cacheDefaults(redisCacheConfiguration)
.build();
}
private RedisCacheConfiguration generateCacheConfiguration() {
PolymorphicTypeValidator typeValidator = BasicPolymorphicTypeValidator
.builder()
.allowIfSubType(Object.class)
.build();
ObjectMapper objectMapper = new ObjectMapper();
JavaTimeModule javaTimeModule = new JavaTimeModule();
objectMapper.registerModule(javaTimeModule);
objectMapper.activateDefaultTyping(typeValidator, ObjectMapper.DefaultTyping.NON_FINAL);
return RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer(objectMapper)));
}
}
@SpringBootApplication
@EnableCaching
public class MyBoardApplication {
public static void main(String[] args) {
SpringApplication.run(MyBoardApplication.class, args);
}
}
# yml
spring:
redis:
host: #서버 호스트
port: 6379
RedisConfig
클래스를 통해 Redis
서버와 연결을 설정하고 Redis
데이터 처리를 위한 템플릿을 설정합니다.
RedisCacheConfig
클래스에서 Redis
캐시 매니저를 설정하고 ObjectMapper
를 이용하여 Java 객체의 직렬화와 역직렬화를 설정했습니다. 또한, DTO
의 LocalDateTime
타입을 String
형태로 변환하기 위한 과정입니다.
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class BoardService {
...
@Cacheable(value = "boardByIdCache", key = "#boardId", cacheManager = "rcm")
public BoardTotalInfoResponse findBoard(final Long boardId) {
...
}
@Cacheable(value = "pagedBoardCache", key = "#pageable", cacheManager = "rcm")
public List<BoardTotalInfoResponse> findLimitedBoardList(Pageable pageable) {
...
}
}
캐시를 적용할 곳에 @Cacheable
을 사용하고, 캐시를 삭제해야 한다면 @CacheEvict
를 사용합니다.
value
: 캐시의 이름을 지정함
key
: 캐시에 저장할 때, 사용할 키를 지정함
cacheManager
: Config에서 설정한 캐시 매니저를 지정함
스프링이 AOP를 이용하여 간편하게 캐싱을 적용할 수 있도록 도와줍니다.
조회하기 전에는 key가 비어있었지만, 조회 후에는 key가 Redis
에 성공적으로 저장된 것을 확인할 수 있습니다.
첫 번째 조회
두 번째 조회
첫 번째 조회에는 Redis
에 캐시가 저장되었고 두 번째 조회에는 캐시를 통해서 조회 성능이 1519ms
에서 79ms
로 19.22배
향상되었습니다.
Redis
를 사용하면 메모리 비용이 증가하고 데이터 손실에 대한 위험이 있습니다.
하지만 캐싱으로 조회 성능 개선, 다양한 데이터 구조 지원, 메시지 브로커, 분산 락 등 다양한 장점을 가지고 있습니다. 따라서 Redis
는 대용량 트래픽
이 발생하는 환경과 분산 환경
에서 활용하기 좋은 인메모리 데이터베이스 시스템
입니다.