[Spring + Redis] Redis 사용 및 활용하여 게시글 조회수 구현하기

Dev_ch·2023년 2월 28일
3
post-thumbnail

🤔 Redis를 이용해 유저 관련 로직을 짜보고 싶다면?
Redis를 이용한 유저 관리 서비스 시리즈

저번 포스팅에선 Redis를 이용해 토큰을 관리하여 로그아웃과 같은 서비스를 구현했다. 사실 Redis를 사용하는 이유는 단순한 key-value 값을 이용해 인메모리 데이터베이스에 저장한다는 이점도 있지만 실질적인 서비스 성능을 개선할 수 있다는 점이 크다.

이번 포스팅은 인메모리 데이터베이스에 데이터를 저장하는 것을 중점으로 일반적인 DB를 이용해도 충분히 구현 할 수 있지만 이를 redis를 통해 구현하여 성능이 어떻게, 왜 개선되는지 알아보자.


문제 상황

어떠한 게시글이 한번 눌릴때 조회수가 누적되고, 이미 해당 게시글을 한번 조회한 유저라면 더 이상 조회수가 올라가지 않도록 구현해야한다.

1. Redis 세팅 및 설정

  • build.gradle에 해당 의존성을 추가해준다
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

아래로는 Redis Config 관련 클래스이다.

RedisConfig.java

@Configuration
@EnableRedisRepositories
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, String> redisTemplate() {
        RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        return redisTemplate;
    }
}

RestTemplateConfig.java


@Configuration
public class RestTemplateConfig {

    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
        return restTemplateBuilder
                .requestFactory(
                        () -> new BufferingClientHttpRequestFactory(
                                new SimpleClientHttpRequestFactory()
                        )
                ).additionalMessageConverters(
                        new StringHttpMessageConverter(
                                StandardCharsets.UTF_8
                        )).build();
    }

}

해당 클래스들을 통해 기본적인 Redis 설정 및 RestTemplate을 사용하기 위한 기타 설정을 해준다. 또한 redis를 메서드를 통해 조금 더 코드를 분리하여 사용하기 위한 DAO를 하나 구현하자.

RedisDao.java

@Component
@RequiredArgsConstructor
public class RedisDao {
    private final RedisTemplate<String, String> redisTemplate;

    public void setValues(String key, String data) {
        ValueOperations<String, String> values = redisTemplate.opsForValue();
        values.set(key, data);
    }

    public void setValuesList(String key, String data) {
        redisTemplate.opsForList().rightPushAll(key,data);
    }

    public List<String> getValuesList(String key) {
        Long len = redisTemplate.opsForList().size(key);
        return len == 0 ? new ArrayList<>() : redisTemplate.opsForList().range(key, 0, len-1);
    }

    public void setValues(String key, String data, Duration duration) {
        ValueOperations<String, String> values = redisTemplate.opsForValue();
        values.set(key, data, duration);
    }

    public String getValues(String key) {
        ValueOperations<String, String> values = redisTemplate.opsForValue();
        return values.get(key);
    }

    public void deleteValues(String key) {
        redisTemplate.delete(key);
    }
}

해당 DAO는 필수가 아닌, 조금 더 편하게 사용하기 위해 구현하는 클래스이며 자신이 사용하려하는 Redis의 Value 값에 따라 커스텀화 해주면 된다. 필자의 경우 List 형식을 추가로 구현하였다.

2. 조회수 구현하기

    @Transactional
    public ContentDto.detailDto detailContent(UserDetails userDetails, Long contentId) {
        Content content = getContent(contentId);

        String redisKey = contentId.toString(); // 해당 글의 ID를 key값으로 선언
        String values = redisDao.getValues(redisKey); // 현재 글의 조회수를 가져온다.
        int views = Integer.parseInt(values); // 가져온 조회수를 Integer로 형변환

        views = Integer.parseInt(values) + 1; // 변환된 값을 +1 해준다.
        redisDao.setValues(redisKey, String.valueOf(views)); // 해당 글 ID로 다시 값을 저장


        return ContentDto.detailDto.response(
				...
        );
    }

해당 서비스 코드에서 조회수를 저장하는 부분을 보면 해당 글의 ID를 Redis의 Key값으로 잡고 Value 값을 조회수로 잡아 게시글을 상세조회하는 API가 호출될때마다 1씩 늘어나도록 구현하였다.

해당 로직의 문제점은 이미 게시글을 조회한 사람이 한번 더 조회하더라도 조회수는 증가하게 된다. 즉, 해당 API를 호출할때마다 조회수가 늘어나기 때문에 이미 조회한 사용자라면 더 이상 해당 게시글의 조회수가 증가하지 않도록 하는 조치가 필요했다.

3. 조회수 필터 구현하기

    @Transactional
    public ContentDto.detailDto detailContent(UserDetails userDetails, Long contentId) {
        Content content = getContent(contentId);

        String redisKey = contentId.toString(); // 조회수 key
        String redisUserKey = getUser(userDetails).getNickName(); // 유저 key
        String values = redisDao.getValues(redisKey); // 기존 조회수 값 가져오기
        int views = Integer.parseInt(values); // 가져온 값은 Integer로 변환
		
        // 유저를 key로 조회한 게시글 ID List안에 해당 게시글 ID가 포함되어있지 않는다면,
        if (!redisDao.getValuesList(redisUserKey).contains(redisKey)) {
            redisDao.setValuesList(redisUserKey, redisKey); // 유저 key로 해당 글 ID를 List 형태로 저장
            views = Integer.parseInt(values) + 1; // 조회수 증가
            redisDao.setValues(redisKey, String.valueOf(views)); // 글ID key로 조회수 저장
        }


        return ContentDto.detailDto.response(
				...
        );
    }

2번 로직에서 이미 조회한 사람이 글을 한번 더 조회했을때 더 이상 조회수가 올라가지 않도록, Redis를 이용해 구현하였다. 해당 유저의 이름을 Key로 잡은 후 게시글을 조회할때마다 해당 게시글의 ID를 List 형태로 저장하였고,
조회수가 올라가는 로직은 If문을 통해 해당 List 안에 조회한 게시글의 ID가 존재하지 않는다면 올라가는 형태로 구현하였다.


🤔 Redis로 해당 데이터를 저장하면 왜 성능개선이 있나요?

Redis는 인메모리 데이터베이스로 I/O가 많이 발생하는 데이터를 처리할때 많은 이점을 얻어갈 수 있다. 예를 들어, 조회수와 같은 경우도 게시글이 조회될때마다, 빠른 시간안에 DB에 계속 query가 들어가며 계속해서 업데이트 해줘야 한다.

이런 경우, 서버에는 당연히 무리가 갈 것 이고 이를 인메모리에 저장하여 성능에 이점을 챙겨가는 것이다 💻

결론은,,

Redis는 인메모리를 활용하며 데이터를 저장하는 경우에도 활용하지만 무거운 API를 캐싱하여 성능을 개선하는 이점도 존재한다. Redis의 캐싱 활용은 데이터를 저장하는 것 보다 더욱 중요하고 성능 개선을 위한 방법이므로 다음 포스팅에는 Redis에 캐싱 기능을 활용하여 API 속도를 개선해보자.

profile
내가 몰입하는 과정을 담은 곳

0개의 댓글