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

좋아요 수 및 좋아요 변경 이력 캐싱 #752

Merged
merged 8 commits into from
Nov 20, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +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.LikeCountRepository;
import hanglog.like.repository.LikeRepository;
import hanglog.like.repository.MemberLikeRepository;
import hanglog.trip.domain.Trip;
import hanglog.trip.domain.repository.TripCityRepository;
import hanglog.trip.domain.repository.TripRepository;
Expand All @@ -26,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 @@ -46,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 @@ -120,8 +127,8 @@ public TripDetailResponse getTripDetail(final Accessor accessor, final Long trip
final LocalDateTime publishedDate = publishedTripRepository.findByTripId(tripId)
.orElseThrow(() -> new BadRequestException(NOT_FOUND_TRIP_ID))
.getCreatedAt();
final LikeElement likeElement = likeRepository.findLikeCountAndIsLikeByTripId(accessor.getMemberId(), tripId)
.orElseGet(() -> new LikeElement(tripId, 0, false));

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

return TripDetailResponse.publishedTrip(
Expand All @@ -133,4 +140,18 @@ public TripDetailResponse getTripDetail(final Accessor accessor, final Long trip
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().getTripLikeStatusMap();
if (tripLikeStatusMap.containsKey(tripId)) {
return new LikeElement(tripId, likeCount.get().getCount(), tripLikeStatusMap.get(tripId));
}
return new LikeElement(tripId, likeCount.get().getCount(), false);
Copy link
Collaborator

Choose a reason for hiding this comment

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

memberLikeRepository에서 멤버의 모든 좋아요 상태(tripLikeStatusMap)을 가져올 필요없이 타겟인 tripId의 상태만 알아오면 되는거죠?!
memberLikeRepository.findByMemberIdAndTripId() 같은거는 만들기 힘든가요?! redis는 오히려 저렇게 전부 조회해오는게 성능이 더 좋은가요? 궁금해서 여쭤봅니다!!!

Copy link
Collaborator

Choose a reason for hiding this comment

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

아하?!!! 혹시 getCommunityTripsByPage()에서는 mysql의 Likes 테이블에서 멤버의 isLike정보를 불러오고, 이 getTripDetail()에서는 redis의 MemberLike에서 멤버의 isLike정보를 불러오는 거 맞나요?!

1시간마다 redis에서 mysql로 동기화를 하는데, 그럼 커뮤니티 전체 페이지에서는 멤버의 좋아요 표시가 안보이고, 디테일 페이지에서는 멤버의 좋아요 표시가 보이는 일이 생길까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

아아 이부분 논의하고 싶었는데 깜빡했네요.
지금 단건 조회인 getTripDetail()의 경우 Redis의 MemberLike에서 게시물 좋아요 이력(isLike) 확인 -> 없으면 DB에서 조회
의 로직인데요.

현재 게시물 리스트 조회의 경우 likeRepository.findLikeCountAndIsLikeByTripIds()를 통해 한 번의 쿼리로 모든 좋아요 이력을 조회해오고 있어요.!

이걸 getTripDetail에 사용한 로직처럼 바꾸면 결국 trip 하나하나당 Redis에 isLike가 있는지 확인하고, 없으면 DB에서 조회
이런 식으로 진행되서 결국 성능이 더 안좋아지는 상황이 생길 수 있을거 같아서 어떻게 해야할 지 의견을 들어보고 싶었습니다.!

Copy link
Member Author

Choose a reason for hiding this comment

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

아 근데 생각해보니까 리스트 조회도 그냥

레디스에서 해당 멤버에 해당하는 Map 가져옴 -> 조회할 trip 리스트가 key에 존재하는지 확인하고 없는 Id는 따로 리스트 만들어 놓음 -> 해당 Id 리스트로 findLikeCountAndIsLikeByTripIds() 호출 -> 캐시에 있던 여행과 없던 여행 하나로 합쳐서 반환

의 로직으로 하면 최대 쿼리 수 1번으로 끝낼 수 있겠네요..!

Copy link
Collaborator

Choose a reason for hiding this comment

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

굉장합니다!!!!!!!!!!!!!

}
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;
}

This file was deleted.

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,10 @@
package hanglog.like.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
Expand Up @@ -2,6 +2,7 @@

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;
Expand Down Expand Up @@ -29,4 +30,11 @@ List<LikeElement> findLikeCountAndIsLikeByTripIds(@Param("memberId") final Long
""")
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();
}
36 changes: 31 additions & 5 deletions backend/src/main/java/hanglog/like/service/LikeService.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
package hanglog.like.service;

import hanglog.like.domain.LikeCount;
import hanglog.like.domain.Likes;
import hanglog.like.domain.MemberLike;
import hanglog.like.dto.MemberLikeCacheEvent;
import hanglog.like.dto.TripLikeCount;
import hanglog.like.dto.request.LikeRequest;
import hanglog.like.repository.CustomLikeRepository;
import hanglog.like.repository.LikeCountRepository;
import hanglog.like.repository.LikeRepository;
import hanglog.like.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.context.ApplicationEventPublisher;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -18,8 +25,11 @@
@Transactional
public class LikeService {

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

public void update(final Long memberId, final Long tripId, final LikeRequest likeRequest) {
Map<Long, Boolean> tripLikeStatusMap = new HashMap<>();
Expand All @@ -39,8 +49,24 @@ public boolean check(final Long memberId, final Long tripId) {
return false;
}

@Scheduled(fixedRate = 3600000)
@Scheduled(cron = "0 0 * * * *")
public void writeBackMemberLikeCache() {
publisher.publishEvent(new MemberLikeCacheEvent());
final List<Likes> likes = memberRepository.findAll().stream()
.flatMap(member -> memberLikeRepository.findById(member.getId())
.map(memberLike -> memberLike.getTripLikeStatusMap()
.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 * * *")
Copy link
Collaborator

Choose a reason for hiding this comment

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

굉장합니다!!!!!!!!신기합니다!!!

Copy link
Member Author

Choose a reason for hiding this comment

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

홍고가 만들어 놓은 일일 환율 저장 로직 참고한건데요!!!!! 본인 칭찬인가요!!!!!!!!

Copy link
Collaborator

Choose a reason for hiding this comment

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

아니ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 요 스케줄링 아래 캐싱로직말한거였는데 제가 이상한 포인트에서 댓글을 달았군요 ㅋ ㅋ ㅋ

public void cacheLikeCount() {
final List<TripLikeCount> tripLikeCounts = likeRepository.findCountByAllTrips();
for (final TripLikeCount tripLikeCount : tripLikeCounts) {
likeCountRepository.save(new LikeCount(tripLikeCount.getTripId(), tripLikeCount.getCount()));
}
}
}

This file was deleted.

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