diff --git a/src/main/java/umc/th/juinjang/JuinjangApplication.java b/src/main/java/umc/th/juinjang/JuinjangApplication.java index e1fccace..848e5fcd 100644 --- a/src/main/java/umc/th/juinjang/JuinjangApplication.java +++ b/src/main/java/umc/th/juinjang/JuinjangApplication.java @@ -9,7 +9,7 @@ import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication -@EnableAsync +// @EnableAsync @ImportAutoConfiguration({FeignAutoConfiguration.class}) @EnableScheduling @EnableRetry diff --git a/src/main/java/umc/th/juinjang/api/note/shared/service/SharedNoteQueryService.java b/src/main/java/umc/th/juinjang/api/note/shared/service/SharedNoteQueryService.java index 2fc918ff..9f380a85 100644 --- a/src/main/java/umc/th/juinjang/api/note/shared/service/SharedNoteQueryService.java +++ b/src/main/java/umc/th/juinjang/api/note/shared/service/SharedNoteQueryService.java @@ -39,7 +39,6 @@ import umc.th.juinjang.domain.note.shared.model.SharedNote; import umc.th.juinjang.domain.pencil.used.model.UsedPencil; import umc.th.juinjang.domain.report.model.Report; -import umc.th.juinjang.event.publisher.ApplicationRewardViewCountPublisherAdapter; @Service @Slf4j @@ -51,17 +50,16 @@ public class SharedNoteQueryService { private final LikedNoteFinder likedNoteFinder; private final ChecklistAnswerFinder checklistAnswerFinder; private final ViewCountService viewCountService; - private final ApplicationRewardViewCountPublisherAdapter applicationRewardViewCountPublisherAdapter; private final ReportFinder reportFinder; - @Transactional(readOnly = true) + @Transactional public SharedNoteGetResponse findSharedNote(Member member, Long sharedNoteId) { SharedNote sharedNote = sharedNoteFinder.findByIdWithNoteAndAddress(sharedNoteId); Limjang limjang = sharedNote.getLimjang(); boolean isBuyerOrOwner = getIsBuyerOrOwner(member, sharedNote); - long viewCount = getViewCountAndCheckReward(member, sharedNoteId, sharedNote); + long viewCount = viewCountService.getViewCount(member, sharedNote); Integer countBuyer = makeBuyerCount(usedPencilFinder.countBySharedNoteId(sharedNoteId)); boolean isLiked = likedNoteFinder.existsByMemberAndSharedNote(member, sharedNote); @@ -80,19 +78,6 @@ private boolean getIsBuyerOrOwner(Member requestMember, SharedNote sharedNote) { sharedNote.getMember().getMemberId().equals(requestMember.getMemberId()); } - private long getViewCountAndCheckReward(Member member, Long sharedNoteId, SharedNote sharedNote) { - long viewCount = viewCountService.getRedisViewCount(sharedNote.getSharedNoteId()); - if (!viewCountService.isDuplicate(member.getMemberId(), sharedNoteId)) { - viewCountService.increaseViewCount(sharedNoteId); - viewCount++; - viewCountService.recordViewerHistory(member.getMemberId(), sharedNoteId); - - applicationRewardViewCountPublisherAdapter.checkViewCountRewardPolicy(sharedNote.getMember(), - sharedNote.getSharedNoteId(), viewCount); - } - return viewCount; - } - private Integer makeBuyerCount(int count) { if (count >= 100) { return 100; @@ -128,7 +113,7 @@ public SharedNoteExploreGetResponse findExploreSharedNote(Member member, List mapIdsAndViewcount(List sharedNotes) { return sharedNotes.stream().collect(Collectors.toMap( SharedNote::getSharedNoteId, - it -> viewCountService.getRedisViewCount(it.getSharedNoteId()) + SharedNote::getViewCount )); } diff --git a/src/main/java/umc/th/juinjang/api/note/shared/service/SharedNoteUpdater.java b/src/main/java/umc/th/juinjang/api/note/shared/service/SharedNoteUpdater.java index 53d014f4..461fe797 100644 --- a/src/main/java/umc/th/juinjang/api/note/shared/service/SharedNoteUpdater.java +++ b/src/main/java/umc/th/juinjang/api/note/shared/service/SharedNoteUpdater.java @@ -12,8 +12,8 @@ public class SharedNoteUpdater { private final SharedNoteRepository sharedNoteRepository; - void updateViewCount(long sharedNoteId, long addAmount) { - sharedNoteRepository.incrementViewCount(sharedNoteId, addAmount); + public void updateViewCount(long sharedNoteId) { + sharedNoteRepository.incrementViewCount(sharedNoteId); } public void incrementLikedCountById(Long sharedNoteId) { diff --git a/src/main/java/umc/th/juinjang/api/note/shared/service/ViewCountService.java b/src/main/java/umc/th/juinjang/api/note/shared/service/ViewCountService.java index 80238f67..1bd744a6 100644 --- a/src/main/java/umc/th/juinjang/api/note/shared/service/ViewCountService.java +++ b/src/main/java/umc/th/juinjang/api/note/shared/service/ViewCountService.java @@ -10,6 +10,9 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import umc.th.juinjang.common.redis.RedisKeyFactory; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.note.shared.model.SharedNote; +import umc.th.juinjang.event.publisher.ApplicationRewardViewCountPublisherAdapter; @Component @RequiredArgsConstructor @@ -18,6 +21,8 @@ public class ViewCountService { private final RedisTemplate redisTemplate; private final SharedNoteFinder sharedNoteFinder; + private final SharedNoteUpdater sharedNoteUpdater; + private final ApplicationRewardViewCountPublisherAdapter applicationRewardViewCountPublisherAdapter; public void recordViewerHistory(long memberId, long sharedNoteId) { try { @@ -28,14 +33,6 @@ public void recordViewerHistory(long memberId, long sharedNoteId) { } } - public void increaseViewCount(long sharedNoteId) { - try { - redisTemplate.opsForValue().increment(RedisKeyFactory.viewCountKey(sharedNoteId)); - } catch (RedisConnectionFailureException | RedisSystemException e) { - log.error("Redis 조회수 증가 실패, sharedNoteId={}", sharedNoteId, e); - } - } - public boolean isDuplicate(long memberId, long sharedNoteId) { try { return Boolean.TRUE.equals(redisTemplate.hasKey(RedisKeyFactory.viewHistoryKey(sharedNoteId, memberId))); @@ -45,29 +42,18 @@ public boolean isDuplicate(long memberId, long sharedNoteId) { } } - public Long getRedisViewCount(long sharedNoteId) { + public long getViewCount(Member member, SharedNote sharedNote) { + long sharedNoteId = sharedNote.getSharedNoteId(); + long viewCount = sharedNoteFinder.findViewCountById(sharedNoteId); - String key = RedisKeyFactory.viewCountKey(sharedNoteId); + if (!isDuplicate(member.getMemberId(), sharedNoteId) && sharedNote.getDeletedAt() == null) { + sharedNoteUpdater.updateViewCount(sharedNoteId); + viewCount++; + recordViewerHistory(member.getMemberId(), sharedNoteId); - try { - Object value = redisTemplate.opsForValue().get(key); - - if (value == null) { - Long viewCountFromDb = sharedNoteFinder.findViewCountById(sharedNoteId); - - if (viewCountFromDb == null) { - log.warn("DB의 sharedNote 조회수가 null sharedNoteId={}", sharedNoteId); - viewCountFromDb = 0L; - } - - redisTemplate.opsForValue().set(key, viewCountFromDb.toString()); - return viewCountFromDb; - } - - return Long.parseLong(value.toString()); - } catch (RedisConnectionFailureException | RedisSystemException e) { - log.error("Redis 장애 발생, 기본값 반환 sharedNoteID={}", sharedNoteId, e); - return 0L; + applicationRewardViewCountPublisherAdapter.checkViewCountRewardPolicy(sharedNote.getMember(), + sharedNote.getSharedNoteId(), viewCount); } + return viewCount; } } diff --git a/src/main/java/umc/th/juinjang/api/note/shared/service/ViewCountSyncScheduler.java b/src/main/java/umc/th/juinjang/api/note/shared/service/ViewCountSyncScheduler.java deleted file mode 100644 index 3f126ac2..00000000 --- a/src/main/java/umc/th/juinjang/api/note/shared/service/ViewCountSyncScheduler.java +++ /dev/null @@ -1,76 +0,0 @@ -package umc.th.juinjang.api.note.shared.service; - -import static umc.th.juinjang.common.redis.RedisKeyFactory.*; - -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; - -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -@RequiredArgsConstructor -@Component -public class ViewCountSyncScheduler { - - private final RedisTemplate redisTemplate; - private final SharedNoteUpdater sharedNoteUpdater; - private final SharedNoteFinder sharedNoteFinder; - - @Scheduled(cron = "0 0 */6 * * *") - @Transactional - public void syncRedisViewCountsToRDB() { - Set keys = redisTemplate.keys(VIEW_COUNT + "*"); - if (keys == null || keys.isEmpty()) - return; - - List sharedNoteIds = getSharedNoteIdsInRedis(keys); - Map dbViewCounts = sharedNoteFinder.findAllIdAndViewCountById(sharedNoteIds); - - for (String key : keys) { - try { - long sharedNoteId = Long.parseLong(key.split(":")[2]); - Object value = redisTemplate.opsForValue().get(key); - if (value == null) { - log.warn("조회수 값 없음 - key={}", key); - continue; - } - - long redisViewCount = Long.parseLong(value.toString()); - long dbViewCount = dbViewCounts.getOrDefault(sharedNoteId, 0L); - - if (redisViewCount > dbViewCount) { - sharedNoteUpdater.updateViewCount(sharedNoteId, redisViewCount); - log.info("조회수 동기화: sharedNoteId={}, Redis={}, DB={}", sharedNoteId, redisViewCount, - dbViewCount); - } else { - log.info("동기화 생략: sharedNoteId={}, Redis={}, DB={}", sharedNoteId, redisViewCount, - dbViewCount); - } - } catch (Exception e) { - log.error("동기화 실패: key={}, error={}", key, e.getMessage(), e); - } - } - } - - private List getSharedNoteIdsInRedis(Set keys) { - return keys.stream() - .map(k -> { - try { - return Long.parseLong(k.split(":")[2]); - } catch (Exception e) { - log.warn("잘못된 키 형식: {}", k); - return null; - } - }) - .filter(Objects::nonNull) - .toList(); - } -} diff --git a/src/main/java/umc/th/juinjang/api/reward/service/RewardService.java b/src/main/java/umc/th/juinjang/api/reward/service/RewardService.java index ae20e173..a11c8f3e 100644 --- a/src/main/java/umc/th/juinjang/api/reward/service/RewardService.java +++ b/src/main/java/umc/th/juinjang/api/reward/service/RewardService.java @@ -1,9 +1,11 @@ package umc.th.juinjang.api.reward.service; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import umc.th.juinjang.api.pencil.service.AcquiredPencilUpdater; import umc.th.juinjang.api.pencilAccount.service.PencilAccountFinder; import umc.th.juinjang.domain.member.model.Member; @@ -16,6 +18,7 @@ @Component @RequiredArgsConstructor +@Slf4j public class RewardService { private final AcquiredPencilUpdater acquiredPencilUpdater; @@ -24,7 +27,7 @@ public class RewardService { private final RewardFinder rewardFinder; private final RewardUpdater rewardUpdater; - @Transactional + @Transactional(propagation = Propagation.REQUIRES_NEW) public void giveViewCountReward(Member member, Long sharedNoteId, Long milestone, Long rewardPencil) { if (alreadyViewCountRewardEarned(RewardType.VIEWCOUNT, sharedNoteId, milestone)) { @@ -37,6 +40,9 @@ public void giveViewCountReward(Member member, Long sharedNoteId, Long milestone acquiredPencilUpdater.save( createAcquiredPencil(member, sharedNoteId, milestone, rewardPencil)); rewardUpdater.save(createReward(member, sharedNoteId, milestone, rewardPencil)); + + log.info("유저에게 조회수 리워드 지급 완료: memberId={}, sharedNoteId={}, milestone={}, rewardPencil={}", + member.getMemberId(), sharedNoteId, milestone, rewardPencil); } private AcquiredPencil createAcquiredPencil(Member member, Long sharedNoteId, Long milestone, Long rewardPencil) { diff --git a/src/main/java/umc/th/juinjang/domain/note/shared/model/SharedNote.java b/src/main/java/umc/th/juinjang/domain/note/shared/model/SharedNote.java index 920f07f6..0761580c 100644 --- a/src/main/java/umc/th/juinjang/domain/note/shared/model/SharedNote.java +++ b/src/main/java/umc/th/juinjang/domain/note/shared/model/SharedNote.java @@ -97,7 +97,6 @@ public void updateDeletedAt(Timestamp deletedAt) { this.deletedAt = deletedAt; } - // 23년 12월 초반 임장 public String getPullPeriod() { String shortYear = String.valueOf(this.year).substring(2); return shortYear + "년 " + this.month + "월 " + this.period + " 임장"; diff --git a/src/main/java/umc/th/juinjang/domain/note/shared/repository/SharedNoteRepository.java b/src/main/java/umc/th/juinjang/domain/note/shared/repository/SharedNoteRepository.java index 13d435e7..0ee87ced 100644 --- a/src/main/java/umc/th/juinjang/domain/note/shared/repository/SharedNoteRepository.java +++ b/src/main/java/umc/th/juinjang/domain/note/shared/repository/SharedNoteRepository.java @@ -19,8 +19,8 @@ public interface SharedNoteRepository extends JpaRepository, S Optional findByIdWithNoteAndAddress(@Param("sharedNoteId") Long sharedNoteId); @Modifying - @Query("UPDATE SharedNote s SET s.viewCount = :updateViewCount WHERE s.sharedNoteId = :sharedNoteId") - void incrementViewCount(@Param("sharedNoteId") Long sharedNoteId, @Param("updateViewCount") Long updateViewCount); + @Query("UPDATE SharedNote s SET s.viewCount = s.viewCount + 1 WHERE s.sharedNoteId = :sharedNoteId") + void incrementViewCount(@Param("sharedNoteId") Long sharedNoteId); @Modifying @Query("UPDATE SharedNote sn SET sn.likeCount = sn.likeCount + 1 WHERE sn.sharedNoteId = :sharedNoteId") diff --git a/src/main/java/umc/th/juinjang/event/subscriber/RewardViewCountEventListener.java b/src/main/java/umc/th/juinjang/event/subscriber/RewardViewCountEventListener.java index 40342e99..be91db3f 100644 --- a/src/main/java/umc/th/juinjang/event/subscriber/RewardViewCountEventListener.java +++ b/src/main/java/umc/th/juinjang/event/subscriber/RewardViewCountEventListener.java @@ -19,12 +19,11 @@ public class RewardViewCountEventListener { private final ViewCountPolicy viewCountPolicy; private final RewardService rewardService; - @Async @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + @Async public void handleRewardViewCountEvent(RewardViewCountEvent rewardViewCountEvent) { - Long reward = viewCountPolicy.getRewardForExactMilestone( - rewardViewCountEvent.viewCount()); + Long reward = viewCountPolicy.getRewardForExactMilestone(rewardViewCountEvent.viewCount()); if (reward == null) { return;