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

좋아요 기능 버그 해결을 위한 캐싱 자료구조 및 로직 변경 #788

Merged
merged 33 commits into from
Feb 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
ce62e0f
Merge pull request #683 from woowacourse-teams/hotfix/#682
dladncks1217 Oct 7, 2023
0b53fb4
Merge pull request #735 from woowacourse-teams/develop
LJW25 Oct 19, 2023
8fa3188
Update README.md
hgo641 Dec 9, 2023
891814a
Merge pull request #783 from woowacourse-teams/develop
mcodnjs Jan 23, 2024
c033764
fix: LikeCount 업데이트 로직 수정
mcodnjs Jan 26, 2024
52b1d06
refactor: updateMemberLikeCache 메서드 분리
mcodnjs Jan 26, 2024
ccbc23b
fix: 커뮤니티 여행 전체 조회 시 Redis 사용하도록 수정
mcodnjs Jan 26, 2024
968ca26
feat: RedisTemplate 빈 등록
mcodnjs Jan 27, 2024
5689976
feat: 좋아요 업데이트 로직 구현
mcodnjs Jan 28, 2024
12d48c8
feat: db와 redis 동기화 스케줄러 구현
mcodnjs Jan 28, 2024
5373b30
feat: 좋아요 조회 로직 구현
mcodnjs Jan 28, 2024
23378f7
refactor: 사용하지 않는 메서드 삭제
mcodnjs Jan 28, 2024
7109d2d
refactor: LikeElement 필드 변경
mcodnjs Jan 29, 2024
d1c4364
refactor: Likes 테이블에 없는 tripId에 default 값 할당
mcodnjs Jan 29, 2024
418b3e6
refactor: LikeRedisKeyConstants 생성
mcodnjs Jan 29, 2024
64dbb6f
refactor: 사용하지 않는 dto 삭제
mcodnjs Jan 29, 2024
90984b3
refactor: like ttl 상수화
mcodnjs Jan 29, 2024
ee61595
fix: likes 테이블 소문자로 변경
mcodnjs Jan 29, 2024
79335e8
fix: 커스텀 쿼리로 변경
mcodnjs Jan 29, 2024
56bb511
fix: 캐시된 tripId가 하나라도 있을 경우만 db 조회하도록 변경
mcodnjs Jan 29, 2024
7043b79
fix: likes 조회 시 memberIds 파싱 로직 수정
mcodnjs Jan 29, 2024
2ea5b03
fix: redis에 가변인자로 추가하도록 변경
mcodnjs Jan 29, 2024
4583c0e
fix: like key prefix 수정
mcodnjs Jan 29, 2024
68882fe
fix: like key prefix 수정
mcodnjs Jan 30, 2024
2378fa4
fix: empty_marker 타입 변경
mcodnjs Jan 30, 2024
7c2e8ff
fix: 업데이트 시 캐시가 없는 경우 DB에서 조회해오도록 수정
mcodnjs Jan 31, 2024
d33ccad
refactor: 업데이트 메서드 인자 수정 및 로직 리팩토링
mcodnjs Jan 31, 2024
59edb00
refactor: toLikeInfo로 메서드 네이밍 변경
mcodnjs Jan 31, 2024
c658576
refactor: likeElements로 변수명 변경
mcodnjs Jan 31, 2024
4a62d4c
refactor: 메서드 위치 변경
mcodnjs Jan 31, 2024
babb205
refactor: 메서드명 변경 및 LikeInfo dto 패키지로 이동
mcodnjs Jan 31, 2024
8530e26
test: LikeService 테스트 추가
mcodnjs Jan 31, 2024
339be7b
test: LikeSyncScheduler 테스트 추가
mcodnjs Jan 31, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 85 additions & 49 deletions backend/src/main/java/hanglog/community/service/CommunityService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

import static hanglog.community.domain.recommendstrategy.RecommendType.LIKE;
import static hanglog.global.exception.ExceptionCode.NOT_FOUND_TRIP_ID;
import static hanglog.like.domain.LikeRedisConstants.EMPTY_MARKER;
import static hanglog.like.domain.LikeRedisConstants.LIKE_TTL;
import static hanglog.like.domain.LikeRedisConstants.generateLikeKey;
import static hanglog.trip.domain.type.PublishedStatusType.PUBLISHED;
import static java.lang.Boolean.TRUE;

import hanglog.auth.domain.Accessor;
import hanglog.city.domain.City;
Expand All @@ -14,26 +18,27 @@
import hanglog.community.dto.response.CommunityTripResponse;
import hanglog.community.dto.response.RecommendTripListResponse;
import hanglog.global.exception.BadRequestException;
import hanglog.like.domain.LikeCount;
import hanglog.like.domain.LikeInfo;
import hanglog.like.domain.MemberLike;
import hanglog.like.dto.LikeInfo;
import hanglog.like.domain.repository.CustomLikeRepository;
import hanglog.like.dto.LikeElement;
import hanglog.like.dto.LikeElements;
import hanglog.like.domain.repository.LikeCountRepository;
import hanglog.like.domain.repository.LikeRepository;
import hanglog.like.domain.repository.MemberLikeRepository;
import hanglog.trip.domain.Trip;
import hanglog.trip.domain.repository.TripCityRepository;
import hanglog.trip.domain.repository.TripRepository;
import hanglog.trip.dto.TripCityElements;
import hanglog.trip.dto.response.TripDetailResponse;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Objects;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Pageable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SetOperations;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -45,14 +50,13 @@ public class CommunityService {

private static final int RECOMMEND_AMOUNT = 5;

private final LikeRepository likeRepository;
private final TripRepository tripRepository;
private final TripCityRepository tripCityRepository;
private final CityRepository cityRepository;
private final RecommendStrategies recommendStrategies;
private final PublishedTripRepository publishedTripRepository;
private final LikeCountRepository likeCountRepository;
private final MemberLikeRepository memberLikeRepository;
private final CustomLikeRepository customLikeRepository;
private final RecommendStrategies recommendStrategies;
private final RedisTemplate<String, Object> redisTemplate;

@Transactional(readOnly = true)
public CommunityTripListResponse getCommunityTripsByPage(final Accessor accessor, final Pageable pageable) {
Expand All @@ -78,38 +82,17 @@ private List<CommunityTripResponse> getCommunityTripResponses(final Accessor acc
tripCityRepository.findTripIdAndCitiesByTripIds(tripIds)
);
final Map<Long, List<City>> citiesByTrip = tripCityElements.toCityMap();

final LikeElements likeElements = new LikeElements(likeRepository.findLikeCountAndIsLikeByTripIds(
accessor.getMemberId(),
tripIds
));
final Map<Long, LikeInfo> likeInfoByTrip = likeElements.toLikeMap();
final Map<Long, LikeInfo> likeInfoByTrip = getLikeInfoByTripIds(accessor.getMemberId(), tripIds);

return trips.stream()
.map(trip -> CommunityTripResponse.of(
trip,
citiesByTrip.get(trip.getId()),
isLike(likeInfoByTrip, trip.getId()),
getLikeCount(likeInfoByTrip, trip.getId())
likeInfoByTrip.get(trip.getId()).isLike(),
likeInfoByTrip.get(trip.getId()).getLikeCount()
)).toList();
}

private boolean isLike(final Map<Long, LikeInfo> likeInfoByTrip, final Long tripId) {
final LikeInfo likeInfo = likeInfoByTrip.get(tripId);
if (likeInfo == null) {
return false;
}
return likeInfo.isLike();
}

private Long getLikeCount(final Map<Long, LikeInfo> likeInfoByTrip, final Long tripId) {
final LikeInfo likeInfo = likeInfoByTrip.get(tripId);
if (likeInfo == null) {
return 0L;
}
return likeInfo.getLikeCount();
}

private Long getLastPageIndex(final int pageSize) {
final Long totalTripCount = tripRepository.countTripByPublishedStatus(PUBLISHED);
final long lastPageIndex = totalTripCount / pageSize;
Expand All @@ -119,6 +102,43 @@ private Long getLastPageIndex(final int pageSize) {
return lastPageIndex + 1;
}

private Map<Long, LikeInfo> getLikeInfoByTripIds(final Long memberId, final List<Long> tripIds) {
final Map<Long, LikeInfo> likeInfoByTrip = new HashMap<>();

final List<Long> nonCachedTripIds = new ArrayList<>();
for (final Long tripId : tripIds) {
final String key = generateLikeKey(tripId);
if (TRUE.equals(redisTemplate.hasKey(key))) {
likeInfoByTrip.put(tripId, readLikeInfoFromCache(key, memberId));
} else {
nonCachedTripIds.add(tripId);
}
}

if (!nonCachedTripIds.isEmpty()) {
final List<LikeElement> likeElements = customLikeRepository.findLikeElementByTripIds(nonCachedTripIds);
likeElements.addAll(getEmptyLikeElements(likeElements, nonCachedTripIds));
likeElements.forEach(this::storeLikeInCache);
likeInfoByTrip.putAll(new LikeElements(likeElements).toLikeInfo(memberId));
}
return likeInfoByTrip;
}

private List<LikeElement> getEmptyLikeElements(
final List<LikeElement> likeElements,
final List<Long> nonCachedTripIds
) {
return nonCachedTripIds.stream()
.filter(tripId -> doesNotContainTripId(likeElements, tripId))
.map(LikeElement::empty)
.toList();
}

private boolean doesNotContainTripId(final List<LikeElement> likeElements, final Long tripId) {
return likeElements.stream()
.noneMatch(likeElement -> likeElement.getTripId().equals(tripId));
}

@Transactional(readOnly = true)
public TripDetailResponse getTripDetail(final Accessor accessor, final Long tripId) {
final Trip trip = tripRepository.findById(tripId)
Expand All @@ -128,30 +148,46 @@ public TripDetailResponse getTripDetail(final Accessor accessor, final Long trip
.orElseThrow(() -> new BadRequestException(NOT_FOUND_TRIP_ID))
.getCreatedAt();

final LikeElement likeElement = getLikeElement(accessor.getMemberId(), tripId);
final LikeInfo likeInfo = getLikeInfoByTripId(accessor.getMemberId(), tripId);
final Boolean isWriter = trip.isWriter(accessor.getMemberId());

return TripDetailResponse.publishedTrip(
trip,
cities,
isWriter,
likeElement.isLike(),
likeElement.getLikeCount(),
likeInfo.isLike(),
likeInfo.getLikeCount(),
publishedDate
);
}

private LikeElement getLikeElement(final Long memberId, final Long tripId) {
final Optional<LikeCount> likeCount = likeCountRepository.findById(tripId);
final Optional<MemberLike> memberLike = memberLikeRepository.findById(memberId);
if (likeCount.isPresent() && memberLike.isPresent()) {
final Map<Long, Boolean> tripLikeStatusMap = memberLike.get().getLikeStatusForTrip();
if (tripLikeStatusMap.containsKey(tripId)) {
return new LikeElement(tripId, likeCount.get().getCount(), tripLikeStatusMap.get(tripId));
}
return new LikeElement(tripId, likeCount.get().getCount(), false);
private LikeInfo getLikeInfoByTripId(final Long memberId, final Long tripId) {
final String key = generateLikeKey(tripId);
if (TRUE.equals(redisTemplate.hasKey(key))) {
return readLikeInfoFromCache(key, memberId);
}

final LikeElement likeElement = customLikeRepository.findLikesElementByTripId(tripId)
.orElse(LikeElement.empty(tripId));
storeLikeInCache(likeElement);
return new LikeInfo(likeElement.getLikeCount(), likeElement.isLike(memberId));
}

private LikeInfo readLikeInfoFromCache(final String key, final Long memberId) {
final SetOperations<String, Object> opsForSet = redisTemplate.opsForSet();
final boolean isLike = TRUE.equals(opsForSet.isMember(key, memberId));
final long count = Objects.requireNonNull(opsForSet.size(key)) - 1;
return new LikeInfo(count, isLike);
}

private void storeLikeInCache(final LikeElement likeElement) {
final SetOperations<String, Object> opsForSet = redisTemplate.opsForSet();
final String key = generateLikeKey(likeElement.getTripId());
opsForSet.add(key, EMPTY_MARKER);
final Set<Long> memberIds = likeElement.getMemberIds();
if (!memberIds.isEmpty()) {
opsForSet.add(key, likeElement.getMemberIds().toArray());
}
return likeRepository.findLikeCountAndIsLikeByTripId(memberId, tripId)
.orElseGet(() -> new LikeElement(tripId, 0, false));
redisTemplate.expire(key, LIKE_TTL);
}
}
12 changes: 12 additions & 0 deletions backend/src/main/java/hanglog/global/config/RedisConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.data.redis.serializer.GenericToStringSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
@EnableRedisRepositories
Expand All @@ -20,4 +23,13 @@ public class RedisConfig {
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}

@Bean
public RedisTemplate<String, Object> redisTemplate() {
final RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericToStringSerializer<>(Long.class));
return redisTemplate;
Comment on lines +32 to +33
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

야무지네요

}
}
17 changes: 0 additions & 17 deletions backend/src/main/java/hanglog/like/domain/LikeCount.java

This file was deleted.

16 changes: 16 additions & 0 deletions backend/src/main/java/hanglog/like/domain/LikeRedisConstants.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package hanglog.like.domain;

import java.time.Duration;

public class LikeRedisConstants {

public static final String LIKE_KEY_PREFIX = "like:";
public static final String WILD_CARD = "*";
public static final String KEY_SEPARATOR = ":";
public static final Long EMPTY_MARKER = -1L;
public static final Duration LIKE_TTL = Duration.ofMinutes(90L);

public static String generateLikeKey(final Long tripId) {
return LIKE_KEY_PREFIX + tripId;
}
}
18 changes: 0 additions & 18 deletions backend/src/main/java/hanglog/like/domain/MemberLike.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package hanglog.like.domain.repository;

import hanglog.like.domain.Likes;
import hanglog.like.dto.LikeElement;
import java.util.List;
import java.util.Optional;

public interface CustomLikeRepository {

void saveAll(final List<Likes> likes);

Optional<LikeElement> findLikesElementByTripId(final Long tripId);

List<LikeElement> findLikeElementByTripIds(final List<Long> tripIds);
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,40 +1,14 @@
package hanglog.like.domain.repository;

import hanglog.like.domain.Likes;
import hanglog.like.dto.LikeElement;
import hanglog.like.dto.TripLikeCount;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface LikeRepository extends JpaRepository<Likes, Long> {

@Query("""
SELECT new hanglog.like.dto.LikeElement
(l.tripId, COUNT(l.memberId), EXISTS(SELECT 1 FROM Likes l_1 WHERE l_1.memberId = :memberId AND l_1.tripId = l.tripId))
FROM Likes l
WHERE l.tripId in :tripIds
GROUP BY l.tripId
""")
List<LikeElement> findLikeCountAndIsLikeByTripIds(@Param("memberId") final Long memberId,
@Param("tripIds") final List<Long> tripIds);

@Query("""
SELECT new hanglog.like.dto.LikeElement
(l.tripId, COUNT(l.memberId), EXISTS(SELECT 1 FROM Likes l_1 WHERE l_1.memberId = :memberId AND l_1.tripId = l.tripId))
FROM Likes l
WHERE l.tripId = :tripId
GROUP BY l.tripId
""")
Optional<LikeElement> findLikeCountAndIsLikeByTripId(@Param("memberId") final Long memberId,
@Param("tripId") final Long tripId);

@Query("""
SELECT new hanglog.like.dto.TripLikeCount(l.tripId, COUNT(l.memberId))
FROM Likes l
GROUP BY l.tripId
""")
List<TripLikeCount> findCountByAllTrips();
@Modifying
@Query("DELETE FROM Likes WHERE tripId IN :tripIds")
void deleteByTripIds(final Set<Long> tripIds);
}

This file was deleted.

12 changes: 11 additions & 1 deletion backend/src/main/java/hanglog/like/dto/LikeElement.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package hanglog.like.dto;

import java.util.Collections;
import java.util.Set;
import lombok.AllArgsConstructor;
import lombok.Getter;

Expand All @@ -9,5 +11,13 @@ public class LikeElement {

private final Long tripId;
private final long likeCount;
private final boolean isLike;
private final Set<Long> memberIds;

public boolean isLike(final Long memberId) {
return memberIds.contains(memberId);
}

public static LikeElement empty(final Long tripId) {
return new LikeElement(tripId, 0, Collections.emptySet());
}
}
Loading
Loading