Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#123 - 레디스를 통한 조회수 및 인기 아카이브 관리 구현 #124

Merged
merged 12 commits into from
Jan 7, 2025
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.palettee.archive.controller.dto.response;

import java.util.List;

public record ArchiveRedisList(
List<ArchiveRedisResponse> archives
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.palettee.archive.controller.dto.response;

import com.palettee.archive.domain.Archive;
import java.io.Serializable;

public record ArchiveRedisResponse(
Long archiveId,
String title,
String description,
String introduction,
String username,
String type,
boolean canComment,
long likeCount,
boolean isLiked,
String imageUrl,
String createDate
) implements Serializable {

public static ArchiveRedisResponse toResponse(Archive archive) {
String imageUrl = archive.getArchiveImages().isEmpty() ? "" : archive.getArchiveImages().get(0).getImageUrl();

return new ArchiveRedisResponse(
archive.getId(),
archive.getTitle(),
archive.getDescription(),
archive.getIntroduction(),
archive.getUser().getName(),
archive.getType().name(),
archive.isCanComment(),
archive.getLikeCount(),
false,
imageUrl,
archive.getCreateAt().toLocalDate().toString()
);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.palettee.archive.domain.Archive;
import com.palettee.likes.repository.LikeRepository;
import java.io.Serializable;

public record ArchiveSimpleResponse(
Long archiveId,
Expand All @@ -15,10 +16,9 @@ public record ArchiveSimpleResponse(
boolean isLiked,
String imageUrl,
String createDate
) {
) implements Serializable {

public static ArchiveSimpleResponse toResponse(Archive archive, Long userId, LikeRepository likeRepository) {

String imageUrl = archive.getArchiveImages().isEmpty() ? "" : archive.getArchiveImages().get(0).getImageUrl();

return new ArchiveSimpleResponse(
Expand All @@ -29,11 +29,26 @@ public static ArchiveSimpleResponse toResponse(Archive archive, Long userId, Lik
archive.getUser().getName(),
archive.getType().name(),
archive.isCanComment(),
likeRepository.countArchiveLike(archive.getId()),
archive.getLikeCount(),
likeRepository.existByUserAndArchive(archive.getId(), userId).isPresent(),
imageUrl,
archive.getCreateAt().toLocalDate().toString()
);
}

public static ArchiveSimpleResponse changeToSimpleResponse(ArchiveRedisResponse it, Long userId, LikeRepository likeRepository) {
return new ArchiveSimpleResponse(
it.archiveId(),
it.title(),
it.description(),
it.introduction(),
it.username(),
it.type(),
it.canComment(),
it.likeCount(),
likeRepository.existByUserAndArchive(it.archiveId(), userId).isPresent(),
it.imageUrl(),
it.createDate()
);
}
}
10 changes: 8 additions & 2 deletions src/main/java/com/palettee/archive/domain/Archive.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public class Archive extends BaseEntity {

private int hits;
private int archiveOrder;
private int likeCount;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
Expand All @@ -55,6 +56,7 @@ public Archive(String title, String description, String introduction,
this.canComment = canComment;
this.hits = 0;
this.archiveOrder = 0;
this.likeCount = 0;

this.user = user;
this.user.addArchive(this);
Expand All @@ -81,8 +83,8 @@ public Archive update(ArchiveUpdateRequest req) {
return this;
}

public void hit() {
this.hits++;
public void like() {
this.likeCount++;
}

public void setOrder() {
Expand All @@ -96,4 +98,8 @@ public void updateOrder(Integer order) {
public boolean isNotOpenComment() {
return !this.canComment;
}

public void setHit(long totalHits) {
this.hits = (int) totalHits;
}
}
7 changes: 7 additions & 0 deletions src/main/java/com/palettee/archive/event/HitEvent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.palettee.archive.event;

public record HitEvent(
Long archiveId,
String email
) {
}
36 changes: 36 additions & 0 deletions src/main/java/com/palettee/archive/event/HitEventListener.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.palettee.archive.event;

import java.time.Duration;
import lombok.RequiredArgsConstructor;
import org.springframework.context.event.EventListener;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class HitEventListener {

private static final String SET_KEY_PREFIX = "hit:archiveId:";
private static final String VALUE_KEY_PREFIX = "incr:hit:archiveId:";

private final RedisTemplate<String, String> redisTemplate;

@Async
@EventListener(value = HitEvent.class)
public void onHit(HitEvent event) {

String setKey = SET_KEY_PREFIX + event.archiveId();
Long addResult = redisTemplate.opsForSet().add(setKey, event.email());

if (addResult == null || addResult != 1L) {
return;
}

redisTemplate.expire(setKey, Duration.ofDays(1));

String valueKey = VALUE_KEY_PREFIX + event.archiveId();
redisTemplate.opsForValue().increment(valueKey);
}

}
7 changes: 7 additions & 0 deletions src/main/java/com/palettee/archive/event/LikeEvent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.palettee.archive.event;

public record LikeEvent(
Long archiveId,
Long userId
) {
}
23 changes: 23 additions & 0 deletions src/main/java/com/palettee/archive/event/LikeEventListener.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.palettee.archive.event;

import lombok.RequiredArgsConstructor;
import org.springframework.context.event.EventListener;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class LikeEventListener {

private static final String LIKE_KEY = "like:archiveId:";

private final RedisTemplate<String, String> redisTemplate;

@Async
@EventListener(value = LikeEvent.class)
public void onLike(LikeEvent event) {
redisTemplate.opsForValue().increment(LIKE_KEY + event.archiveId());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package com.palettee.archive.repository;

import com.palettee.archive.controller.dto.response.ArchiveRedisList;
import com.palettee.archive.controller.dto.response.ArchiveRedisResponse;
import com.palettee.archive.domain.Archive;
import io.jsonwebtoken.io.SerializationException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

@Repository
@Slf4j
@RequiredArgsConstructor
public class ArchiveRedisRepository {

private static final String INCR_PATTERN = "incr:hit:archiveId:*";
private static final String STD_PATTERN = "std:hit:archiveId:";
private static final String TOP_ARCHIVE = "top_archives";
private static final String DELIMITER = ":";

private final RedisTemplate<String, String> redisTemplate;
private final RedisTemplate<String, Object> redisTemplateForArchive;

private final ArchiveRepository archiveRepository;

@Transactional
public void settleHits() {
Set<String> incrKeys = redisTemplate.keys(INCR_PATTERN);
if (incrKeys == null || incrKeys.isEmpty()) {
return;
}

for (String incrKey : incrKeys) {
String archiveId = extractId(incrKey);

String incrHits = redisTemplate.opsForValue().get(incrKey);
long incrCount = incrHits == null ? 0 : Long.parseLong(incrHits);

String stdKey = STD_PATTERN + archiveId;
String stdHits = redisTemplate.opsForValue().get(stdKey);
kcsc2217 marked this conversation as resolved.
Show resolved Hide resolved
long stdCount = stdHits == null ? 0 : Long.parseLong(stdHits);

long totalHits = stdCount + incrCount;

updateHitCount(Long.parseLong(archiveId), totalHits);

redisTemplate.opsForValue().set(stdKey, String.valueOf(totalHits));
redisTemplate.delete(incrKey);
}
}

private void updateHitCount(Long archiveId, long totalHits) {
Archive archive = archiveRepository.findById(archiveId).orElseThrow();
archive.setHit(totalHits);
}

private String extractId(String incrKey) {
return incrKey.split(DELIMITER)[3];
}

@Transactional(readOnly = true)
public void updateArchiveList() {
List<Long> top4IncrKeys = getTop4IncrKeys();
List<Archive> result = archiveRepository.findArchivesInIds(top4IncrKeys);

int redisSize = result.size();
if (redisSize < 4) {
int remaining = 4 - redisSize;
List<Archive> additionalFromDB = archiveRepository.findTopArchives(remaining);
result.addAll(additionalFromDB);
}

List<ArchiveRedisResponse> redis = result.stream()
.map(ArchiveRedisResponse::toResponse)
.toList();
log.info("Redis에 저장될 데이터: {}", redis);
redisTemplateForArchive.opsForValue().set(TOP_ARCHIVE, new ArchiveRedisList(redis), 1, TimeUnit.HOURS);
}

private List<Long> getTop4IncrKeys() {
String hitPattern = "incr:hit:archiveId:*";
String likePattern = "like:archiveId:*";

// Redis에서 키 가져오기
Set<String> hitKeys = redisTemplate.keys(hitPattern);
Set<String> likeKeys = redisTemplate.keys(likePattern);

if (hitKeys == null || likeKeys == null) {
return Collections.emptyList();
}

// 점수 계산
Map<Long, Integer> archiveScores = new HashMap<>();
for (String hitKey : hitKeys) {
Long archiveId = extractArchiveId(hitKey);
Integer hitCount = getValueAsInt(hitKey);

String likeKey = "like:archiveId:" + archiveId;
Integer likeCount = getValueAsInt(likeKey);

int score = hitCount + (likeCount * 5);
archiveScores.put(archiveId, score);
}

// 정렬 및 상위 4개 추출
return archiveScores.entrySet().stream()
.sorted(Map.Entry.<Long, Integer>comparingByValue().reversed())
.limit(4)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
}

private Long extractArchiveId(String key) {
return Long.valueOf(key.replace("incr:hit:archiveId:", ""));
}

private Integer getValueAsInt(String key) {
String value = redisTemplate.opsForValue().get(key);
return value != null ? Integer.parseInt(value) : 0;
}

public ArchiveRedisList getTopArchives() {
try {
ArchiveRedisList result = (ArchiveRedisList) redisTemplateForArchive.opsForValue().get(TOP_ARCHIVE);
return result == null ? new ArchiveRedisList(new ArrayList<>()) : result;
} catch (SerializationException e) {
log.error("Redis 역직렬화 실패: {}", e.getMessage());
redisTemplateForArchive.delete(TOP_ARCHIVE);
return new ArchiveRedisList(new ArrayList<>());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@ public interface ArchiveRepository extends JpaRepository<Archive, Long>, Archive
@Query("select a from Archive a where a.id in :ids order by a.archiveOrder desc")
Slice<Archive> findAllInIds(@Param("ids") List<Long> ids, Pageable pageable);

@Query("select a from Archive a order by a.hits desc, a.id desc limit 5")
List<Archive> getMainArchives();

@Query("SELECT a.type AS type, COUNT(a) AS count FROM Archive a GROUP BY a.type")
List<ColorCount> countByArchiveType();

Expand All @@ -29,4 +26,14 @@ public interface ArchiveRepository extends JpaRepository<Archive, Long>, Archive

@Query("SELECT a FROM Archive a where a.user.id = :userId")
List<Archive> findAllByUserId(Long userId);

@Modifying
@Query("UPDATE Archive a SET a.hits = :hitCount WHERE a.id = :archiveId")
void updateHitCount(@Param("archiveId") Long archiveId, @Param("hitCount") Long hitCount);

@Query("select a from Archive a order by (a.hits + a.likeCount * 5) desc, a.id desc limit :limit")
List<Archive> findTopArchives(@Param("limit") int limit);

@Query("select a from Archive a where a.id in :ids")
List<Archive> findArchivesInIds(@Param("ids") List<Long> ids);
}
20 changes: 20 additions & 0 deletions src/main/java/com/palettee/archive/service/ArchiveScheduler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.palettee.archive.service;

import com.palettee.archive.repository.ArchiveRedisRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class ArchiveScheduler {

private final ArchiveRedisRepository archiveRedisRepository;

@Scheduled(cron = "0 * * * * *")
public void updateMainArchive() {
archiveRedisRepository.updateArchiveList();
archiveRedisRepository.settleHits();
}

}
Loading
Loading