스프링부트 @Cacheable을 활용한 redis Cache 전략 적용하기
들어가기
캐시란?
데이터 접근 속도를 높이기 위해 자주 사용하는 데이터를 메모리에 임시로 저장하는 기술이다. 즉, DB 조회 부화를 줄이고 시스템 성능을 크게 향상할 수 있다.
Redis - Cache
Redis는 인메모리 데이터 스토어로, 데이터의 읽기/쓰기 속도가 매우 빠르다.
또한, 다양한 데이터 구조(문자열, 해시, 리스트 등)를 지원하고 확장성이 뛰어나며, TTL(만료 시간) 설정으로 자동 캐시 관리가 가능하다는 이점이 있다.
Redis 기반 Cache 전략
1. Read-Through 캐싱
: 캐시에서 데이터를 조회하고, 데이터가 없으면 DB에서 조회한 후 Redis에 캐싱하는 방식.
2. Write-Through 캐싱
: 데이터를 DB에 쓰기 전에 Redis에 먼저 저장하는 방식.
3. Write-Behind (Write-Back) 캐싱
: 데이터를 Redis에 먼저 쓰고, 일정 시간 후 비동기적으로 DB에 반영하는 방식.
4. Cache Aside (Lazy Loading)
: 애플리케이션이 캐시에서 데이터를 조회하고 없으면 DB에서 가져와 캐시에 저장하는 방식.
Cache Aside (Lazy Loading)를 활용한 캐싱
- redis에 데이터(캐싱)가 없을 경우에는 DB로부터 최초 데이터를 응답받은 후, redis에 데이터를 insert 한다.
- redis에 데이터(캐싱)가 있을 경우 redis데이터를 사용한다.
Cache Aside (Lazy Loading) 전략의 장단점
[장점]
1. 캐시에 저장된 데이터를 사용함으로써 db나 외부 api의 호출을 줄이고, 응답속도를 크게 개선.
-> 특히 읽기 요청이 많고, 데이터가 자주 변경되지 않는 경우에 효과적.
2. redis는 분산 캐시를 지원함으로, 대규모 트래픽 환경에서도 안정적으로 캐시를 사용할 수 있음.
-> 캐시로 인하여 db부하가 감소함.
3. @Cacheable을 사용하여 캐시 키를 동적으로 생성할 수 있음.
4. Redis는 메모리 기반의 저장소이므로, 읽기/쓰기 속도가 매우 빠름.
[단점]
1. DB의 데이터가 변경되었을 때 캐시를 동기화하거나 무효화하는 로직이 필요함.
2. redis는 메모리 기반 캐시이기 때문에, 저장된 데이터가 많아지면 메모리 사용량이 급증하게 됨.
3. 캐시 키가 적절하지 않으면, 불필요한 데이터 증가로 캐시 효율이 떨어질 수 있음.
4. 캐시와 데이터베이스 간의 일관성 유지가 어려움.
5. 최초 호출 시 캐시에 데이터가 없기 때문에, 캐싱되지 않는 데이터에 대해 더 느릴 수 있음.
실습하기
1. 로컬에서 redis 환경 세팅하기.
2024.12.18 - [AWS] - [Redis] Mac에서 redis 설치 및 redis tool 연동
2. h2 DB연동하기.
2024.03.28 - [Spring(boot)] - [Spring Boot] H2 DB 연동하기
3. springboot redis 연동 관련 설정하기.
4. Cache Aside (Lazy Loading) 캐싱전략을 활용한 api 실습개발하기.
5. Cache Aside 적용 전/후 성능 테스트하기
위의 순서대로 'Cache Aside (Lazy Loading) 캐싱전략'을 실습해 보자!
SpringBoot에 Redis 연동 관련 설정
dependencies {
//redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}
- build.gradle에 springBoot의 redis 의존성을 추가.
spring:
profiles:
default: local
datasource:
driver-class-name: org.h2.Driver
url: jdbc:h2:tcp://localhost/~/tool/h2/bin/redis
username: sa
password: 1111
jpa:
hibernate:
generate-ddl: false
properties:
hibernate:
ddl-auto: create
show_sql: true
data:
redis:
host: localhost
port: 6379
- application.yml에 redis에 대한 설정 추가.
@Configuration
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private int port;
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
//Lettuce 라이브러리를 활용하여 redis 연결관리 객체 생성.
//redis의 host, port를 설정.
return new LettuceConnectionFactory(new RedisStandaloneConfiguration(host, port));
}
}
- Lettuce 라이브러리를 활용하여 redis config객체를 생성.
@Configuration
@EnableCaching //springboot 캐시 설정 활성화.
public class RedisCacheConfig {
@Bean
public CacheManager boardCacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
//redis에서 Key를 저장할 때 String으로 직렬화(변환)해서 저장.
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
//redis에서 Value를 저장할 때 Json으로 직렬화(변환)해서 저장.
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(
new Jackson2JsonRedisSerializer<Object>(Object.class)))
//ttl 설정(3분)
.entryTtl(Duration.ofMinutes(3L));
return RedisCacheManager
.RedisCacheManagerBuilder
.fromConnectionFactory(redisConnectionFactory)
.cacheDefaults(redisCacheConfiguration)
.build();
}
}
Cache Aside (Lazy Loading) 캐싱전략을 활용하기
dummy data insert Query
-- board 테이블에 더미 데이터 삽입
WITH RECURSIVE cte (n) AS (
SELECT 1
UNION ALL
SELECT n + 1 FROM cte WHERE n < 10000 -- 생성하고 싶은 더미 데이터의 개수
)
SELECT
'Title' || LPAD(n, 7, '0') AS title, -- 'Title' 다음에 7자리 숫자로 구성된 제목 생성
'Content' || LPAD(n, 7, '0') AS content, -- 'Content' 다음에 7자리 숫자로 구성된 내용 생성
DATEADD('SECOND', FLOOR(RAND() * 86400),
DATEADD('DAY', -FLOOR(RAND() * 3650 + 1), CURRENT_TIMESTAMP)) AS created_at -- 최근 10년 내의 임의 날짜와 시간 생성
FROM cte;
-- INSERT 문을 실행하려면 위 결과를 boards 테이블에 추가
INSERT INTO board (title, content, created_at)
WITH RECURSIVE cte (n) AS (
SELECT 1
UNION ALL
SELECT n + 1 FROM cte WHERE n < 10000
)
SELECT
'Title' || LPAD(n, 7, '0'),
'Content' || LPAD(n, 7, '0'),
DATEADD('SECOND', FLOOR(RAND() * 86400),
DATEADD('DAY', -FLOOR(RAND() * 3650 + 1), CURRENT_TIMESTAMP))
FROM cte;
- 테스트를 하기 위해서 H2 DB에 dummy데이터를 insert.
BoardRepository.java
public interface BoardRepository extends JpaRepository<Board, Long> {
//Board 조회
Page<Board> findAllByOrderByCreatedAtDesc(Pageable pageable);
}
BoardService.java
@Service
@RequiredArgsConstructor
public class BoardService {
public final BoardRepository boardRepository;
/**
* Get board list.
*
* @param page the page
* @param size the size
* @return the list
*/
@Cacheable(cacheNames = "boardList", key = "'page' + #page + 'size' + #size", cacheManager = "boardCacheManager")
public List<Board> getBoardList(int page, int size){
Pageable pageable = PageRequest.of(page, size);
Page<Board> pageOfBoards = boardRepository.findAllByOrderByCreatedAtDesc(pageable);
return pageOfBoards.getContent();
}
}
1. @Cacheable
- 어노테이션을 사용하여, 해당 메서드의 결과를 캐시에 저장하도록 지시.
- 동일한 매개변수로 호출하면, 메서드를 실행하지 않고, 캐시 된 값을 반환.
2. cacheNames = "boardList"
- 캐시 이름을 지정.
- 실제 캐시 키는 지정한 key 값과 결합하여 생성.
3. key = "'page' + #page + 'size' + #size"
- 캐시에 저장될 고유 키를 정의.
- 여기에 #page와 #size는 메서드의 파라미터 값을 참조
4. cacheManager = "boardCacheManager"
- 캐시를 관리하는 cacheManager를 지정.
- 위의 설정의 RedisCacheConfig클래스의 boardCacheManager를 지정함.
BoardController.java
@RestController
@RequiredArgsConstructor
@RequestMapping("board")
public class BoardController {
private final BoardService boardService;
/**
* Gets board list.
*
* @param page the page
* @param size the size
* @return the board list
*/
@GetMapping("/v1/find/list")
public List<Board> getBoardList(@RequestParam int page, @RequestParam int size) {
return boardService.getBoardList(page, size);
}
}
테스트
- 캐시전략을 사용하지 않을 경우
- 캐시전략을 사용하는 경우
- 전/후를 비교하였을 때 응답이 4초 정도 적어진 것을 확인할 수 있음.
정리하기
무엇을 시도하거나 도입할 때는 항상 장점과 단점이 공존한다.
이번에 Redis Cache 전략을 간단히 실습해 보며 느낀 점은, 이를 무작정 적용한다면 오히려 예상치 못한 부작용이 발생할 수 있다는 것이다.
그러나 적용하려는 API의 특성과 구조를 충분히 분석하고 이를 기반으로 운영에 도입한다면, Redis Cache 전략이 기대 이상의 효과를 발휘할지 않을까!!?