이전에 Redis에 대해 공부한 이유는 당연하겠지만, 프로젝트에 적용해보고 싶었기 때문입니다.
어떻게 적용했는지 작성해보고자 합니다.
저도 다른 분들의 코드를 참고하였습니다.(아래 링크 달아 놓겠습니다.)
게시글 조회수 구현
- 백엔드 개발자로서 제일 중요한 점을 꼽으면, "서버에 부하가 가지 않도록 여러 방법으로 분산시키는 법" 입니다.
- 조회수 구현을 Redis를 활용하게 된 것도, 그 이유에 부합했기 때문입니다.
- 게시글의 조회수를 구현하기 위해, 매번 게시글을 들어갈 때마다 조회수를 증가시키는 INSERT 쿼리가 발생했습니다.
- 요구사항은 이렇습니다.
- Redis에 [게시글번호(pk):조회수] 이렇게 저장한 후, DB에는 반영하지 않고 Redis에 조회수를 적재시킵니다.
- 3분 마다 캐시 데이터를 DB에 반영 후 삭제하는 식으로 구현하자.
Redis를 적용하기 위한 설정
Docker를 통해 간단하게 Redis를 실행합니다.
Docker Desktop을 통해 간단하게 Pull(실행할 수 있는 프로그램을 다운하는 것과 동일) 할 수도 있고.
Docker를 설치한 후, 명령어를 통해 Redis를 Pull 받아 실행할 수 있습니다.
docker pull redis
- 컨테이너 이름과, port, 환경변수 등(password)을 설정해주면 redis를 실행(RUN)시킬 수 있게 됩니다.
sudo docker run -p 6379:6379 redis
직접 레디스 명령어를 치고 데이터를 확인하려면, Redis 컨테이너 내부로 접속해야 합니다.
redis-cli
간단한 명령어로 key를 확인할 수 있습니다.
keys *
Spring Boot 설정
- Gradle 설정에 추가해줍니다.
// Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
- application.yml에 설정을 추가해줍시다.
spring:
data:
redis:
host: 127.0.0.1
port: 6379
timeout: 10
RedisConfig
- Redis를 활용하기 위해서 Redis 설정을 세팅할 RedisConfig를 만듭니다.
@EnableScheduling
@Configuration
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private int port;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}
}
- @EnableScheduling -> Redis와 상관 없는 어노테이션입니다.
- 추후 Redis에 있는 데이터들을 DB에 반영시키기 위한 설정입니다.
- 즉 스케줄링 기능을 사용하기 위해 달아준 어노테이션이기에, Redis 설정과는 무관합니다.
- @Configuration -> 컴포넌트로 등록하고 싱글톤을 보장해줍니다.
- @Value -> application.yml에 설정한 host와 port 정보를 주입해옵니다.
- @Bean -> 스프링 컨테이너의 관리를 받기 위해 빈을 등록합니다.
- 레디스의 구현체로는 Lettuce방식을 사용합니다. (찾아보시면 아시겠지만 Jedis 방식도 있습니다.)
RedisUtil
- RedisUtil을 구현합니다.
- Redis의 데이터를 적재하고, 가져올 수 있는 부분을 도와주는 Util class 입니다.
@Component
@RequiredArgsConstructor
public class RedisUtil {
private final StringRedisTemplate stringRedisTemplate;
public String getData(String key) {
ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue();
return valueOperations.get(key);
}
public void setData(String key, String value, Duration timeout) {
ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue();
valueOperations.set(key, value, timeout);
}
public void deleteData(String key) {
stringRedisTemplate.delete(key);
}
public void increment(String key) {
ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue();
valueOperations.increment(key);
}
public Set<String> keys(String pattern) {
return stringRedisTemplate.keys(pattern);
}
}
- 잘 보시면, setData를 할 때 만료시간을 지정해주는 것을 볼 수 있습니다.
- 인메모리 기반의 저장소이기 때문에, 만료 시간은 필수 입니다. (Redis의 장점 중 하나)
PostCacheService
- 이제 Redis에 데이터를 적재시키는 것을 도와줄 PostCacheService를 작성합니다.
@Component
@RequiredArgsConstructor
public class PostCacheService {
private final PostRepository postRepository;
private final RedisUtil redisUtil;
public void addViewCntToRedis(Long postId) {
String viewCntKey = createViewCntCacheKey(postId);
if (redisUtil.getData(viewCntKey) != null) {
redisUtil.increment(viewCntKey);
return;
}
redisUtil.setData(
viewCntKey,
String.valueOf(postRepository.findViewCount(postId) + 1),
Duration.ofMinutes(3)
);
}
/**
* 3분마다 캐시 데이터를 RDB 반영 후 삭제한다
*/
@Scheduled(cron = "0 0/3 * * * ?")
public void applyViewCountToRDB() {
Set<String> viewCntKeys = redisUtil.keys("postViewCount*");
if(Objects.requireNonNull(viewCntKeys).isEmpty()) return;
for (String viewCntKey : viewCntKeys) {
Long postId = extractPostIdFromKey(viewCntKey);
Long viewCount = Long.parseLong(redisUtil.getData(viewCntKey));
postRepository.applyViewCntToRDB(postId, viewCount);
redisUtil.deleteData(viewCntKey);
}
}
private Long extractPostIdFromKey(String key) {
return Long.parseLong(key.split("::")[1]);
}
private String createViewCntCacheKey(Long id) {
return createCacheKey("postViewCount", id);
}
private String createCacheKey(String cacheType, Long id) {
return cacheType + "::" + id;
}
}
- Key가 [postViewCount::postId]
- Value가 조회수
redisUtil.setData(
viewCntKey,
String.valueOf(postRepository.findViewCount(postId) + 1),
Duration.ofMinutes(3)
);
- key = postViewCount::1
- value = 1 // 조회수 증가
- 만료시간 3분
스케줄링
- 3분 마다 캐시 데이터를 DB에 반영한 후 삭제합니다.
@Scheduled(cron = "0 0/3 * * * ?")
- postViewCount로 시작하는 모든 키를 가지고 옵니다.
Set<String> viewCntKeys = redisUtil.keys("postViewCount*");
- 반복문을 돌면서 key에 있는 값들을 분해
- :: 뒤에 있는 값이 postId(게시글 PK) -> extractPostIdFromKey를 통해 postId를 가져옵니다.
Long postId = extractPostIdFromKey(viewCntKey);
- 조회수를 가지고 옵니다. value값 가지고 옵니다.
Long viewCount = Long.parseLong(redisUtil.getData(viewCntKey));
내부적으로는 GET postViewCount::1 이런 명령어가 실행되어 value값을 가지고 옵니다.
- 조회수를 반영합니다.
postRepository.applyViewCntToRDB(postId, viewCount);
- Redis에 있는 데이터를 삭제합니다. -> 만료시간이 있지만 db에 반영 된 이후로는 새로 반영된 조회수를 가지고 와야 하기 때문에
redisUtil.deleteData(viewCntKey);
- PostRepository
@Transactional
@Modifying
@Query("update Post p set p.viewCount = :viewCount where p.id = :id")
void applyViewCntToRDB(@Param("id") Long id, @Param("viewCount") Long viewCount);
- Controller에 이렇게 구현해두었습니다.
@GetMapping("/{postId}")
public ApiResponse<PostDetailResponseDto> getPost(
@PathVariable Long postId
) {
postCacheService.addViewCntToRedis(postId);
PostDetailResponseDto postDetailResponseDto = postService.find(postId);
return ApiResponse.ok(postDetailResponseDto);
}
PR
https://github.com/beginner0107/spring-react-blog/pull/107
참고자료
급 길어져서 다음 챕터에 accessToken과 refreshToken을 구현한 방법에 대해 작성하겠습니다.
'DB관련 > REDIS' 카테고리의 다른 글
Spring Boot + Redis 조회수, 회원 기능(acessToken, refreshToken) 구현한 후기 - (2) (0) | 2024.02.15 |
---|---|
REDIS - 숫자 다루기 (DECR, DECRBY, INCRBY, INCR) (0) | 2024.01.27 |
REDIS - DEL, GETRANGE, SETRANGE (1) | 2024.01.27 |
REDIS - MGET (1) | 2024.01.27 |
REDIS - SETNX / SETEX / MEST / MSETNX (0) | 2024.01.26 |