-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* feat: 레디스를 통한 조회수 및 인기 아카이브 관리 구현 * fix: 조회수 카운팅 로직 수정 * fix: 조회수 반영 주기 수정 * fix: 아카이브 캐싱 방식 수정 * fix: 필요한 어노테이션 추가 * refactor: 코드 정리 * refactor: 임시 구현 * feat: 인기 아카이브에 실시간성 부여 * fix: 이게 맞나 싶은 수준의 구현... * feat: 임시 구현 * feat: 아카이브 레디스 저장 오류 수정 * fix: 테스트 수정
- Loading branch information
Showing
16 changed files
with
507 additions
and
21 deletions.
There are no files selected for viewing
8 changes: 8 additions & 0 deletions
8
src/main/java/com/palettee/archive/controller/dto/response/ArchiveRedisList.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) { | ||
} |
38 changes: 38 additions & 0 deletions
38
src/main/java/com/palettee/archive/controller/dto/response/ArchiveRedisResponse.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
36
src/main/java/com/palettee/archive/event/HitEventListener.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
23
src/main/java/com/palettee/archive/event/LikeEventListener.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
|
||
} |
142 changes: 142 additions & 0 deletions
142
src/main/java/com/palettee/archive/repository/ArchiveRedisRepository.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
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<>()); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
20 changes: 20 additions & 0 deletions
20
src/main/java/com/palettee/archive/service/ArchiveScheduler.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
|
||
} |
Oops, something went wrong.