hanker

Redis - Spring Boot + Redis를 사용하여 실시간 검색어 순위 만들기 (3) 화면에 데이터 넘겨주기 본문

SPRING

Redis - Spring Boot + Redis를 사용하여 실시간 검색어 순위 만들기 (3) 화면에 데이터 넘겨주기

hanker 2025. 4. 8. 06:35
반응형

https://hanke-r.tistory.com/entry/Redis-Spring-Boot-Redis%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EA%B2%80%EC%83%89%EC%96%B4-%EC%88%9C%EC%9C%84-%EB%A7%8C%EB%93%A4%EA%B8%B0-2-Spring-Redis-%EA%B2%80%EC%83%89%EC%96%B4-%EC%A0%80%EC%9E%A5

 

Redis - Spring Boot + Redis를 사용하여 실시간 검색어 순위 만들기 (2) Spring Redis 검색어 저장

https://hanke-r.tistory.com/entry/Redis-Spring-Boot-Redis%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EA%B2%80%EC%83%89%EC%96%B4-%EC%88%9C%EC%9C%84-%EB%A7%8C%EB%93%A4%EA%B8%B0-1-%ED%99%94%EB%A9%B4-%EA%B5%AC%EC%84%B1-%EB%B0%8F

hanke-r.tistory.com

이전 글에서 redis에 저장시키는 방법에 대해서 알아봤다.

 

이번 글에서는 저장된 redis 데이터를 화면으로 가져와서 보여주자.

 


1. View 단 Script 추가

 

rate.html 화면

새로고침과 새로고침이 되었을 때 업데이트 시간을 변경하기 위해 이전에 작성했던 rate.html에 script를 추가해 준다.

<script>
    refreshTrending();

    // 새로고침 아이콘 클릭 시 실행
    function refreshTrending() {
        // 여기에 실시간 검색어 갱신 로직 추가
        $.ajax({
            url: "/api/v1/getTopKeywords",
            type: "POST",
            dataType: "json",
            success: function(data) {
                const keywords = data.list;

                console.log(keywords)
                const trendingList = document.getElementById('trending-list');
                trendingList.innerHTML = '';

                for (let i = 0; i < keywords.length; i++) {
                    const keyword = keywords[i];

                    // 변동 상태에 따른 클래스와 텍스트 설정
                    let changeClass = '';
                    let changeText = '';

                    if (keyword.change == 'UP') {
                        changeClass = 'up';
                        changeText = `▲ ${keyword.change}`;
                    } else if (keyword.change == 'DOWN') {
                        changeClass = 'down';
                        changeText = `▼ ${keyword.change}`;
                    } else if (keyword.change == 'NEW') {
                        changeClass = 'new';
                        changeText = 'NEW';
                    } else {
                        changeText = '-';
                    }

                    // 리스트 아이템 생성
                    const listItem = document.createElement('li');
                    listItem.className = 'trending-item';
                    listItem.onclick = function() { searchTrending(keyword.keyword); };

                    // 항목 내용 구성
                    listItem.innerHTML = `
                    <span class="rank">${i + 1}</span>
                    <span class="keyword">${keyword.keyword}</span>
                    <span class="change ${changeClass}">${changeText}</span>
                `;

                    // 리스트에 추가
                    trendingList.appendChild(listItem);

                    updateCurrentTime();
                }

            }
        })

        // 시간 업데이트
        updateCurrentTime();
    }

    // 현재 시간 업데이트
    function updateCurrentTime() {
        const now = new Date();
        const hours = String(now.getHours()).padStart(2, '0');
        const minutes = String(now.getMinutes()).padStart(2, '0');
        const formattedDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${hours}:${minutes}`;

        $('.trending-footer').text(`업데이트: ${formattedDate}`);
    }
</script>

 

 


2. Controller / Service 추가

 

* KeywordRank.java (VO Class)

public class KeywordRank {

    private static final long serialVersionUID = 1L;

    private Object keyword;
    private int rank;
    private RankChange change; // 상승, 하락, 유지, 신규
    
    
    // getter / setter / Constructor 생성
    // ...
    
}

 

 

* Controller

    @PostMapping("/api/v1/getTopKeywords")
    public String getTopKeywords(Model model) throws IOException {
        List<KeywordRank> keywordRanks = redisService.getKeywordRankingWithChanges(10);
        model.addAttribute("list", keywordRanks);
        return "jsonView";
    }

 

* Service (ZSET이용)
- Redis ZSet(정렬된 집합)을 이용하여 검색어 순위(랭킹)를 가져오고, 이전 순위와 비교하여 순위 변화(상승, 하락, 신규 등)를 판단하는 메서드이다.

private static final String HOT_KEYWORDS = "hot_keywords";
public List<KeywordRank> getKeywordRankingWithChanges(int topN) {

    // HOT_KEYWORDS 기준으로 현재 상위 검색어 추출 (ZSET은 점수 높은 순)
    // Redis는 점수가 같을 경우 알파벳 순 정렬
    Set<Object> currentTop = redisTemplate.opsForZSet()
            .reverseRange(HOT_KEYWORDS, 0, topN - 1);

    List<KeywordRank> result = new ArrayList<>();
    int currentRank = 1;

    for (Object keyword : currentTop) {
        // 이전 순위(hot_keywords:previous) 찾기
        Long prevRank = redisTemplate.opsForZSet()
                .reverseRank("hot_keywords:previous", keyword);

        RankChange change;
        if (prevRank == null) {
            change = RankChange.NEW;
        } else if (prevRank + 1 > currentRank) {
            change = RankChange.UP;
        } else if (prevRank + 1 < currentRank) {
            change = RankChange.DOWN;
        } else {
            change = RankChange.NO_CHANGE;
        }

        result.add(new KeywordRank(keyword, currentRank++, change));
    }

    return result;
}

- int topN : 상위 몇 개의 검색어를 가져올지 정한다.

- KeywordRank 객체 리스트 (검색어, 현재 순위, 순위 변화 상태 포함)

- HOT_KEYWORDS라는 Redis ZSet에서 점수가 높은 순으로 상위 topN개의 검색어를 가져온다.

- ZSet은 (검색어, 점수) 쌍으로 구성되어 있음

- 점수가 같을 경우 Redis는 사전순 정렬

- hot_keywords:previous라는 이전 검색어 ZSet에서 현재 키워드의 순위 조회 후 순위 지정 (이전 순위는 스케쥴러에 구현)

 

 

JobScheduler

@Component
public class Job {

    // Redis에 저장된 현재 검색어 순위의 key
    private static final String HOT_KEYWORDS = "hot_keywords";

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // 2시간마다 실행되는 스케줄러 메서드
    @Scheduled(cron = "0 0 0/2 * * *") // cron 표현식: 매 2시간마다 실행
    public void backupPreviousRanking() {
        // 현재 HOT_KEYWORDS의 모든 요소와 해당 점수를 가져옵니다.
        Set<ZSetOperations.TypedTuple<Object>> currentTop =
                redisTemplate.opsForZSet().reverseRangeWithScores(HOT_KEYWORDS, 0, -1);

        // 이전 순위 데이터를 저장하는 key("hot_keywords:previous")를 삭제하여 초기화합니다.
        redisTemplate.delete("hot_keywords:previous");

        // currentTop이 null이 아닌 경우(즉, HOT_KEYWORDS에 데이터가 존재하면)
        if (currentTop != null) {
            // 현재 순위의 각 항목을 순회하면서 hot_keywords:previous에 동일하게 추가합니다.
            // 이로써 현재 순위를 이전 순위로 백업하는 효과를 얻습니다.
            for (ZSetOperations.TypedTuple<Object> entry : currentTop) {
                redisTemplate.opsForZSet().add("hot_keywords:previous", entry.getValue(), entry.getScore());
            }
        }
    }
}

* 스케쥴러를 사용하려면 @EnableScheduling을 추가해야 한다.

@SpringBootApplication
@EnableScheduling // 추가
public class HkApp {

    public static void main(String[] args) {
        SpringApplication.run(HkApp.class, args);
    }

}

 

 

여기까지 구현이 완료되면, 구현이 끝이 났다.

 

화면을 보자

실시간 검색어 순위 화면 구현 완료

 

UI 적으로 보완해야 할 부분이 보이긴 하지만 간단하게 구현이 완료됐다.

 

반응형