Skip to content

Commit

Permalink
좋아요 수 및 좋아요 변경 이력 캐싱 (#752)
Browse files Browse the repository at this point in the history
* refactor: 공개된 여행 단건 조회 로직 변경

* refactor: 좋아요 여부 변경 기능 redis 사용한로직으로 변경

* feat: 게시물 좋아요 여부 cache write back 스케줄링 기능 추가

* feat: 게시물 좋아요 수 캐싱 및 스케줄링 기능 추가

* feat: 게시물 좋아요 변경 시 캐싱된 좋아요 수 업데이트 로직 추가

* test: 게시물 좋아요 변경 테스트 수정

* chore: Like Repository 패키지 위치 변경

* refactor: 변수 네이밍 변경
  • Loading branch information
jjongwa authored Nov 20, 2023
1 parent 1f59ee8 commit 7017f5c
Show file tree
Hide file tree
Showing 13 changed files with 262 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,14 @@
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.LikeElement;
import hanglog.like.dto.LikeElements;
import hanglog.like.repository.LikeRepository;
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;
Expand All @@ -25,6 +30,7 @@
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Pageable;
Expand All @@ -45,6 +51,8 @@ public class CommunityService {
private final CityRepository cityRepository;
private final RecommendStrategies recommendStrategies;
private final PublishedTripRepository publishedTripRepository;
private final LikeCountRepository likeCountRepository;
private final MemberLikeRepository memberLikeRepository;

@Transactional(readOnly = true)
public CommunityTripListResponse getCommunityTripsByPage(final Accessor accessor, final Pageable pageable) {
Expand Down Expand Up @@ -119,20 +127,31 @@ public TripDetailResponse getTripDetail(final Accessor accessor, final Long trip
final LocalDateTime publishedDate = publishedTripRepository.findByTripId(tripId)
.orElseThrow(() -> new BadRequestException(NOT_FOUND_TRIP_ID))
.getCreatedAt();
final LikeElements likeElements = new LikeElements(likeRepository.findLikeCountAndIsLikeByTripIds(
accessor.getMemberId(),
List.of(tripId)
));
final Map<Long, LikeInfo> likeInfoByTrip = likeElements.toLikeMap();

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

return TripDetailResponse.publishedTrip(
trip,
cities,
isWriter,
isLike(likeInfoByTrip, tripId),
getLikeCount(likeInfoByTrip, tripId),
likeElement.isLike(),
likeElement.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);
}
return likeRepository.findLikeCountAndIsLikeByTripId(memberId, tripId)
.orElseGet(() -> new LikeElement(tripId, 0, false));
}
}
17 changes: 17 additions & 0 deletions backend/src/main/java/hanglog/like/domain/LikeCount.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package hanglog.like.domain;

import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;

@Getter
@AllArgsConstructor
@RedisHash(value = "likeCount")
public class LikeCount {

@Id
private Long tripId;

private Long count;
}
18 changes: 18 additions & 0 deletions backend/src/main/java/hanglog/like/domain/MemberLike.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package hanglog.like.domain;

import java.util.Map;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;

@Getter
@AllArgsConstructor
@RedisHash(value = "memberLike", timeToLive = 5400)
public class MemberLike {

@Id
private Long memberId;

private Map<Long, Boolean> likeStatusForTrip;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package hanglog.like.domain.repository;

import hanglog.like.domain.Likes;
import java.util.List;

public interface CustomLikeRepository {

void saveAll(final List<Likes> likes);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package hanglog.like.domain.repository;

import hanglog.like.domain.LikeCount;
import java.util.List;
import org.springframework.data.repository.CrudRepository;

public interface LikeCountRepository extends CrudRepository<LikeCount, Long> {

List<LikeCount> findLikeCountsByTripIdIn(final List<Long> tripIds);
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
package hanglog.like.repository;
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 org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface LikeRepository extends JpaRepository<Likes, Long> {

boolean existsByMemberIdAndTripId(final Long memberId, final Long tripId);

void deleteByMemberIdAndTripId(final Long memberId, final Long tripId);

@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))
Expand All @@ -22,4 +20,21 @@ public interface LikeRepository extends JpaRepository<Likes, Long> {
""")
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();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package hanglog.like.domain.repository;

import hanglog.like.domain.MemberLike;
import org.springframework.data.repository.CrudRepository;

public interface MemberLikeRepository extends CrudRepository<MemberLike, Long> {
}
12 changes: 12 additions & 0 deletions backend/src/main/java/hanglog/like/dto/TripLikeCount.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package hanglog.like.dto;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public class TripLikeCount {

private final long tripId;
private final long count;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package hanglog.like.infrastrcutrue;

import hanglog.like.domain.Likes;
import hanglog.like.domain.repository.CustomLikeRepository;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Repository;

@RequiredArgsConstructor
@Repository
public class CustomLikeRepositoryImpl implements CustomLikeRepository {

private final NamedParameterJdbcTemplate namedParameterJdbcTemplate;

@Override
public void saveAll(final List<Likes> likes) {
final String sql = """
INSERT INTO likes (trip_id, member_id)
VALUES (:tripId, :memberId)
""";
namedParameterJdbcTemplate.batchUpdate(sql, getLikesToSqlParameterSources(likes));
}

private MapSqlParameterSource[] getLikesToSqlParameterSources(final List<Likes> likes) {
return likes.stream()
.map(this::getLikeToSqlParameterSource)
.toArray(MapSqlParameterSource[]::new);
}

private MapSqlParameterSource getLikeToSqlParameterSource(final Likes likes) {
return new MapSqlParameterSource()
.addValue("tripId", likes.getTripId())
.addValue("memberId", likes.getMemberId());
}
}
59 changes: 53 additions & 6 deletions backend/src/main/java/hanglog/like/service/LikeService.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
package hanglog.like.service;

import hanglog.like.domain.LikeCount;
import hanglog.like.domain.Likes;
import hanglog.like.domain.MemberLike;
import hanglog.like.dto.TripLikeCount;
import hanglog.like.dto.request.LikeRequest;
import hanglog.like.repository.LikeRepository;
import hanglog.like.domain.repository.CustomLikeRepository;
import hanglog.like.domain.repository.LikeCountRepository;
import hanglog.like.domain.repository.LikeRepository;
import hanglog.like.domain.repository.MemberLikeRepository;
import hanglog.member.domain.repository.MemberRepository;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -13,15 +26,49 @@
public class LikeService {

private final LikeRepository likeRepository;
private final MemberLikeRepository memberLikeRepository;
private final LikeCountRepository likeCountRepository;
private final MemberRepository memberRepository;
private final CustomLikeRepository customLikeRepository;

public void update(final Long memberId, final Long tripId, final LikeRequest likeRequest) {
final boolean requestStatus = likeRequest.getIsLike();
Map<Long, Boolean> tripLikeStatusMap = new HashMap<>();
final Optional<MemberLike> memberLike = memberLikeRepository.findById(memberId);
if (memberLike.isPresent()) {
tripLikeStatusMap = memberLike.get().getLikeStatusForTrip();
}
tripLikeStatusMap.put(tripId, likeRequest.getIsLike());
memberLikeRepository.save(new MemberLike(memberId, tripLikeStatusMap));
updateLikeCountCache(tripId, likeRequest);
}

if (requestStatus && !likeRepository.existsByMemberIdAndTripId(memberId, tripId)) {
likeRepository.save(new Likes(tripId, memberId));
private void updateLikeCountCache(final Long tripId, final LikeRequest likeRequest) {
final Optional<LikeCount> likeCount = likeCountRepository.findById(tripId);
if (Boolean.TRUE.equals(likeRequest.getIsLike())) {
likeCount.ifPresent(count -> likeCountRepository.save(new LikeCount(tripId, count.getCount() + 1)));
return;
}
if (!requestStatus) {
likeRepository.deleteByMemberIdAndTripId(memberId, tripId);
likeCount.ifPresent(count -> likeCountRepository.save(new LikeCount(tripId, count.getCount() - 1)));
}

@Scheduled(cron = "0 0 * * * *")
public void writeBackMemberLikeCache() {
final List<Likes> likes = memberRepository.findAll().stream()
.flatMap(member -> memberLikeRepository.findById(member.getId())
.map(memberLike -> memberLike.getLikeStatusForTrip()
.entrySet().stream()
.filter(Map.Entry::getValue)
.map(entry -> new Likes(entry.getKey(), member.getId())))
.orElseGet(Stream::empty))
.toList();
customLikeRepository.saveAll(likes);
}

@Scheduled(cron = "0 0 0 * * *")
public void cacheLikeCount() {
final List<TripLikeCount> tripLikeCounts = likeRepository.findCountByAllTrips();
for (final TripLikeCount tripLikeCount : tripLikeCounts) {
likeCountRepository.save(new LikeCount(tripLikeCount.getTripId(), tripLikeCount.getCount()));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,7 @@
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;

@Import({
TripService.class,
CommunityService.class,
LedgerService.class,
RecommendStrategies.class,
CustomDayLogRepositoryImpl.class,
CustomTripCityRepositoryImpl.class,
EventListenerTestConfig.class
})
class CommunityServiceIntegrationTest extends ServiceIntegrationTest {
class CommunityServiceIntegrationTest extends RedisServiceIntegrationTest {

@Autowired
private TripService tripService;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,14 @@
import static org.assertj.core.api.SoftAssertions.assertSoftly;

import hanglog.like.dto.request.LikeRequest;
import hanglog.like.repository.LikeRepository;
import hanglog.like.domain.repository.MemberLikeRepository;
import hanglog.like.service.LikeService;
import hanglog.trip.infrastructure.CustomDayLogRepositoryImpl;
import hanglog.trip.infrastructure.CustomTripCityRepositoryImpl;
import hanglog.trip.service.TripService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;

@Import({
TripService.class,
LikeService.class,
CustomDayLogRepositoryImpl.class,
CustomTripCityRepositoryImpl.class
})
class LikeServiceIntegrationTest extends ServiceIntegrationTest {
class LikeServiceIntegrationTest extends RedisServiceIntegrationTest {

@Autowired
private TripService tripService;
Expand All @@ -29,7 +20,7 @@ class LikeServiceIntegrationTest extends ServiceIntegrationTest {
private LikeService likeService;

@Autowired
private LikeRepository likeRepository;
private MemberLikeRepository memberLikeRepository;

@DisplayName("해당 게시물의 좋아요 여부를 변경할 수 있다.")
@Test
Expand All @@ -42,11 +33,21 @@ void update() {

// when & then
assertSoftly(softly -> {
softly.assertThat(likeRepository.existsByMemberIdAndTripId(member.getId(), tripId)).isFalse();
softly.assertThat(memberLikeRepository.findById(member.getId())).isEmpty();

likeService.update(member.getId(), tripId, likeTrueRequest);
softly.assertThat(likeRepository.existsByMemberIdAndTripId(member.getId(), tripId)).isTrue();
softly.assertThat(memberLikeRepository.findById(member.getId())).isPresent();
softly.assertThat(memberLikeRepository.findById(member.getId())
.get()
.getLikeStatusForTrip()
.get(tripId)).isTrue();

likeService.update(member.getId(), tripId, likeFalseRequest);
softly.assertThat(likeRepository.existsByMemberIdAndTripId(member.getId(), tripId)).isFalse();
softly.assertThat(memberLikeRepository.findById(member.getId())).isPresent();
softly.assertThat(memberLikeRepository.findById(member.getId())
.get()
.getLikeStatusForTrip()
.get(tripId)).isFalse();
});
}
}
Loading

0 comments on commit 7017f5c

Please sign in to comment.