[Spring Boot + Redis] Sorted Set(ZSet) 이용하여 최근 본 아이템 목록 조회 구현 방법

enjoy89·2024년 5월 7일
0
post-custom-banner

사용자가 아이템 정보를 클릭하여 확인을 할 때마다 히스토리를 저장하고, 이를 최신순으로 정렬하여 보여주는 기능을 Spring Boot와 Redis를 활용하여 구현하는 방법에 대해 알아보겠습니다.

Redis Sorted Set(ZSet)

Redis의 Sorted Set(ZSet) 자료구조는 중복 없이 유니크한 String Value를 저장하는 Set과 유사하지만, 각 데이터에 점수(Score)를 부여하여 Score를 통해 데이터를 정렬할 수 있는 기능을 제공합니다.

즉, Set 자료구조 특성에 Score라는 속성을 추가로 갖고 있어 데이터 순서를 정렬할 수 있습니다. 이는 사용자가 최근 본 아이템 목록을 구현하는 데 매우 유용한 구조입니다. Socre를 활용하여 시간 순 또는 다른 기준에 따라 요소를 정렬할 수도 있습니다.

아이템 ID를 Key로, 아이템 상세 조회 시간을 Score로 저장하면, 최근 본 아이템 순으로 정렬할 수 있습니다.

이제 Redis의 Sorted Set와 Spring Boot를 이용해서 구현하는 방법을 알아보겠습니다.


Spring Boot 의존성 추가

build.gradle 혹은 pom.xml파일에 redis에 대한 의존성 라이브러리를 추가합니다.

  • build.gradle
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
  • pom.xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

Redis 설정

application.yml 설정 추가

  • redis의 기본 포트는 6379 입니다.
spring:
  redis:
    host: localhost
    port: 6379

RedisConfig 추가

  • Spring Boot에서 Redis를 사용하기 위한 기본 설정을 정의합니다.
  • @EnableRedisRepositories : Spring Data Redis 리포지토리를 활성화
  • redisTemplate() : Redis 서버와의 통신을 위한 RedisTemplate 객체를 설정하며, 이 객체를 사용하여 Redis의 다양한 데이터 조작 기능을 쉽고 효율적으로 사용할 수 있도록 합니다.
@Configuration
@EnableRedisRepositories
public class RedisConfig {

	// application.yml 파일에서 설정한 값을 각 필드에 주입
    @Value("${spring.redis.host}")
    private String redisHost;

    @Value("${spring.redis.port}")
    private int redisPort;

	// Redis 서버에 연결하기 위한 설정
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(redisHost, redisPort);
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        redisTemplate.setKeySerializer(new StringRedisSerializer()); // key 직렬화 방식 정의
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); // 저장되는 데이터값의 JSON 직렬화 방식을 정의
        return redisTemplate;
    }
}

서비스 구현

이제 사용자가 아이템을 조회할 때마다 해당 아이템의 ID와 조회 시간을 Socre로 사용하여 Sorted Set에 저장하고, 이를 조회하는 코드를 살펴보겠습니다.

  • 저는 최근 7일 동안 30개의 데이터를 저장하도록 설계하였습니다.

RedisService.class

@Service
@RequiredArgsConstructor
public class RedisService {

	private static final String USER_VIEW_ITEMS = "user:%s:view_items";
    private static final long SECONDS_IN_A_WEEK = 7L * 24 * 60 * 60;    // 최근 7일 동안 저장

    private final RedisTemplate<String, String> redisTemplate;
		
	// 사용자별로 아이템의 ID와 조회 시간을 저장
    public void addItemRecentlyViewed(Long userId, Long itemId) {
        String key = getUserViewedKey(userId);
        double score = getCurrentTimeInSeconds();
        redisTemplate.opsForZSet().add(key, String.valueOf(itemId), score);
    }
    
	// 사용자의 최근 본 아이템 목록 조회
	// 최근 7일 동안 count 개수만큼 조회
    public Set<Long> getViewData(Long userId, int count) {
        String key = getUserViewedKey(userId);
        double minScore = getCurrentTimeInSeconds() - SECONDS_IN_A_WEEK;
        return convertSet(redisTemplate.opsForZSet().reverseRangeByScore(key, minScore, Double.MAX_VALUE, 0, count));
    }
    
    // Object Set -> Long Set 변환
    private Set<Long> convertSet(Set<Object> itemIds) {
        return itemIds.stream()
                .map(itemId -> Long.parseLong((String) itemId))
                .collect(Collectors.toCollection(LinkedHashSet::new));
    }
    
    // userId를 사용하여 고유한 Redis 키를 생성
    private String getUserViewedKey(Long userId) {
        return String.format(USER_VIEW_ITEMS, userId);
    }

	// 현재 시간을 밀리초에서 초 단위로 변환
    private double getCurrentTimeInSeconds() {
        return System.currentTimeMillis() / 1000.0;
    }
}
  • addItemRecentlyViewed() : 사용자가 아이템을 조회할 때마다 해당 아이템의 ID와 조회 시간을 Socre로 사용하여 Sorted Set에 저장합니다. key는 사용자 별로 고유한 ID를 의미하며, Socre는 현재 시간을 초 단위로 변환한 값입니다. Redis는 데이터를 저장할 때 키(key)와 값(value) 모두 문자열 형태로 저장하는 것이 일반적이기 때문에 Long 타입의 itemId를 문자열로 변환해주었습니다.
  • getRecentlyViewedItems() : 사용자의 최근 본 아이템 목록을 조회합니다. minScore로 최소 점수(시간)와 조회할 아이템의 개수(count)를 지정하여, 특정 기간 동안 조회한 아이템을 가져올 수 있습니다. 조회된 아이템 ID들은 Long 타입으로 변환 후 LinkedHashSet에 저장하여 순서를 유지합니다.

저는 아이템 목록 조회 반환 타입을 Set<Long>으로 지정하여 itemId 목록을 반환하였습니다. 이때 주의할 점은 Redis에서 데이터를 조회할 때, 반환되는 집합(Set)의 원소들은 Object 타입입니다. 이때 Long 타입의 아이템 ID 목록을 사용하려면, 명시적으로 Object에서 Long으로 변환하는 과정이 필요합니다. 저는 이 과정을 convertSet 메서드에 작성했습니다.

ItemsService.class

@Service
@RequiredArgsConstructor
public class ItemsService {

    private static final int MAX_COUNT = 30;    // 최대 30개
    private final RedisService redisService;

    public void addViewItem(Long itemId, Long userId) {
        redisService.addItemRecentlyViewed(userId, itemId);
    }

    public Set<Long> getViewItems(Long userId) {
        return redisService.getViewData(userId, MAX_COUNT); // 30개 조회
    }
}
  • addViewItem() : 사용자가 조회한 아이템 Id와 사용자 Id를 통해 최근 본 목록에 추가합니다.
  • getViewItems() : 최대 30개를 지정하여 원하는 개수만큼의 데이터를 조회합니다.

이제 실제로 아이템을 조회하여 최신순으로 목록이 정렬되는지 확인해보겠습니다.

Redis 실행 화면

  • 차례대로 Id가 1인 아이템부터 5인 아이템까지 조회했을 때, 아래와 같이 [5,4,3,2,1] 순서로 데이터가 저장된 것을 볼 수 있습니다.

  • 여기서 2번과 3번의 아이템을 도중에 조회해보면, 아래와 같이 [3,2,5,4,1] 순서로 변경되는 것을 확인할 수 있습니다.

정리

Redis의 Sorted Set(ZSet)은 데이터에 점수(Score)를 부여하여 Score를 통해 데이터를 정렬할 수 있는 기능을 제공합니다.

Socre를 활용하여 시간 순 또는 다른 기준에 따라 요소를 정렬할 수도 있습니다.

profile
Backend Developer 💻 😺
post-custom-banner

0개의 댓글