Skip to content

Commit

Permalink
Merge pull request #788 from woowacourse-teams/hotfix/#784-like-caching
Browse files Browse the repository at this point in the history
좋아요 조회 및 업데이트 버그 수정
  • Loading branch information
mcodnjs authored Feb 3, 2024
2 parents 78c0d36 + 339be7b commit 58a90de
Show file tree
Hide file tree
Showing 18 changed files with 452 additions and 216 deletions.
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;
}
}
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

0 comments on commit 58a90de

Please sign in to comment.