Skip to content

Commit 2850570

Browse files
Merge pull request #259 from prgrms-web-devcourse-final-project/refactor/share-like-refactoring(WR9-149)
Refactor/share like refactoring(wr9 149)
2 parents 425ddef + 38aff0e commit 2850570

File tree

6 files changed

+348
-55
lines changed

6 files changed

+348
-55
lines changed
Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
package io.crops.warmletter.domain.share.cache;
2-
32
import lombok.RequiredArgsConstructor;
43
import org.springframework.data.redis.core.StringRedisTemplate;
54
import org.springframework.stereotype.Component;
6-
75
import java.util.HashMap;
86
import java.util.Map;
7+
import java.util.Optional;
98
import java.util.Set;
109

1110
@Component
@@ -14,17 +13,37 @@ public class PostLikeRedisManager {
1413

1514
private final StringRedisTemplate redisTemplate;
1615
private static final String POST_LIKE_KEY = "post:%d:like:memberId:%d";
16+
private static final String POST_LIKE_COUNT_KEY = "post:%d:like:count";
17+
18+
public void toggleLike(Long postId, Long memberId,boolean status) {
1719

18-
public void toggleLike(Long postId, Long memberId) {
1920
String key = getKey(postId, memberId);
20-
Boolean isLiked = isLiked(postId, memberId);
21-
redisTemplate.opsForValue().set(key, String.valueOf(!isLiked));
21+
redisTemplate.opsForValue().set(key, String.valueOf(status));
22+
23+
String countKey = getCountKey(postId);
24+
if (status) {
25+
redisTemplate.opsForValue().increment(countKey);
26+
} else {
27+
redisTemplate.opsForValue().decrement(countKey);
28+
}
29+
}
30+
31+
private String getCountKey(Long postId) {
32+
return String.format(POST_LIKE_COUNT_KEY, postId);
2233
}
2334

2435
public boolean isLiked(Long postId, Long memberId) {
36+
return getLikedStatus(postId, memberId).orElse(false);
37+
}
38+
39+
public Optional<Boolean> getLikedStatus(Long postId, Long memberId) {
2540
String key = getKey(postId, memberId);
2641
String value = redisTemplate.opsForValue().get(key);
27-
return value != null && Boolean.parseBoolean(value);
42+
if (value == null) {
43+
return Optional.empty(); // 캐시에 해당 정보가 없음
44+
}
45+
46+
return Optional.of(Boolean.parseBoolean(value));
2847
}
2948

3049
private String getKey(Long postId, Long memberId) {
@@ -34,22 +53,32 @@ private String getKey(Long postId, Long memberId) {
3453
public Map<String, Boolean> getAllLikeStatus() {
3554
Set<String> keys = redisTemplate.keys("post:*:like:memberId:*");
3655
Map<String, Boolean> likeStatusMap = new HashMap<>();
37-
38-
for (String key : keys) {
39-
String value = redisTemplate.opsForValue().get(key);
40-
if (value != null) {
41-
likeStatusMap.put(key, Boolean.parseBoolean(value));
56+
if (keys != null) {
57+
for (String key : keys) {
58+
String value = redisTemplate.opsForValue().get(key);
59+
if (value != null) {
60+
likeStatusMap.put(key, Boolean.parseBoolean(value));
61+
}
4262
}
4363
}
4464
return likeStatusMap;
4565
}
4666

67+
public int getLikeCount(Long postId) {
68+
String countKey = getCountKey(postId);
69+
String countValue = redisTemplate.opsForValue().get(countKey);
70+
return countValue != null ? Integer.parseInt(countValue) : 0;
71+
}
4772

4873
public void clearCache() {
4974
Set<String> keys = redisTemplate.keys("post:*:like:memberId:*");
5075
if (keys != null && !keys.isEmpty()) {
5176
redisTemplate.delete(keys);
5277
}
78+
// 좋아요 카운트도 함께 삭제
79+
Set<String> countKeys = redisTemplate.keys("post:*:like:count");
80+
if (countKeys != null && !countKeys.isEmpty()) {
81+
redisTemplate.delete(countKeys);
82+
}
5383
}
54-
5584
}

src/main/java/io/crops/warmletter/domain/share/scheduler/LikeScheduler.java

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
package io.crops.warmletter.domain.share.scheduler;
2-
32
import io.crops.warmletter.domain.share.entity.SharePostLike;
43
import io.crops.warmletter.domain.share.cache.PostLikeRedisManager;
54
import io.crops.warmletter.domain.share.repository.SharePostLikeRepository;
65
import lombok.RequiredArgsConstructor;
6+
import lombok.extern.slf4j.Slf4j;
77
import org.springframework.scheduling.annotation.Scheduled;
88
import org.springframework.stereotype.Component;
99
import org.springframework.transaction.annotation.Transactional;
10-
1110
import java.util.Map;
1211

1312
@Component
1413
@RequiredArgsConstructor
14+
@Slf4j
1515
public class LikeScheduler {
1616

1717
private static final String REDIS_KEY_DELIMITER = ":";
@@ -21,15 +21,32 @@ public class LikeScheduler {
2121
@Scheduled(fixedRate = 60000)
2222
@Transactional
2323
public void syncLikesToDatabase() {
24-
Map<String, Boolean> likeStatusMap = postLikeRedisManager.getAllLikeStatus();
2524

26-
likeStatusMap.forEach(this::processLikeEntry);
27-
28-
postLikeRedisManager.clearCache();
29-
}
25+
try {
26+
Map<String, Boolean> likeStatusMap = postLikeRedisManager.getAllLikeStatus();
27+
if (likeStatusMap != null) {
28+
// 각 좋아요 처리
29+
for (Map.Entry<String, Boolean> entry : likeStatusMap.entrySet()) {
30+
try {
31+
processLikeEntry(entry.getKey(), entry.getValue());
32+
} catch (Exception e) {
33+
log.error("해당 좋아요 항목 처리 중 오류 발생 : {}", entry.getKey(), e);
34+
}
35+
}
36+
}
37+
postLikeRedisManager.clearCache();
38+
} catch(Exception e){
39+
log.error("DB 동기화 중 오류 발생", e);
40+
}
41+
}
3042

3143
private void processLikeEntry(String key, boolean currentLikeStatus) {
3244
String[] parts = key.split(REDIS_KEY_DELIMITER);
45+
// 키 형식 검증
46+
if (parts.length < 5 || !"post".equals(parts[0]) || !"like".equals(parts[2]) || !"memberId".equals(parts[3])) {
47+
log.warn("Invalid key format: {}", key);
48+
return;
49+
}
3350
Long postId = Long.parseLong(parts[1]);
3451
Long memberId = Long.parseLong(parts[4]);
3552

@@ -41,12 +58,12 @@ private void processLikeEntry(String key, boolean currentLikeStatus) {
4158
}
4259

4360
private void updateLike(SharePostLike likeEntity, boolean redisLikeStatus) {
44-
boolean isSameStatus = likeEntity.isLiked() == redisLikeStatus;
45-
boolean newStatus = isSameStatus ? !redisLikeStatus : redisLikeStatus;
46-
47-
likeEntity.updateLikeStatus(newStatus);
61+
if (likeEntity.isLiked() != redisLikeStatus) {
62+
likeEntity.updateLikeStatus(redisLikeStatus);
63+
}
4864
}
4965

66+
//
5067
private void createLikeIfNeeded(Long postId, Long memberId, boolean currentLikeStatus) {
5168
if (currentLikeStatus) {
5269
sharePostLikeRepository.save(SharePostLike.builder()

src/main/java/io/crops/warmletter/domain/share/service/SharePostLikeService.java

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import io.crops.warmletter.domain.share.repository.SharePostLikeRepository;
77
import lombok.RequiredArgsConstructor;
88
import org.springframework.stereotype.Service;
9+
import java.util.Optional;
910

1011
@Service
1112
@RequiredArgsConstructor
@@ -17,7 +18,18 @@ public class SharePostLikeService {
1718

1819
public void toggleLike(Long postId) {
1920
Long memberId = authFacade.getCurrentUserId();
20-
postLikeRedisManager.toggleLike(postId, memberId);
21+
// Redis 먼저 조회 후, DB 조회
22+
Optional<Boolean> redisLikeStatus = postLikeRedisManager.getLikedStatus(postId, memberId);
23+
24+
boolean currentLikeStatus = redisLikeStatus.orElseGet(() ->
25+
sharePostLikeRepository.findBySharePostIdAndMemberId(postId, memberId)
26+
.map(entity -> entity.isLiked())
27+
.orElse(false)
28+
);
29+
30+
boolean newStatus = !currentLikeStatus;
31+
32+
postLikeRedisManager.toggleLike(postId, memberId, newStatus);
2133
}
2234

2335
public SharePostLikeResponse getLikeCountAndStatus(Long sharePostId) {
@@ -26,7 +38,18 @@ public SharePostLikeResponse getLikeCountAndStatus(Long sharePostId) {
2638

2739
if (sharePostId == null)
2840
throw new ShareInvalidInputValue();
41+
// DB 조회
42+
SharePostLikeResponse dbResponse = sharePostLikeRepository.getLikeCountAndStatus(sharePostId, memberId);
43+
// Redis에서 동기화안된 카운트 가져옴.
44+
int redisLikeCount = postLikeRedisManager.getLikeCount(sharePostId);
45+
// 좋아요 상태 확인 없으면 DB 값으로
46+
Optional<Boolean> redisLikeStatus = postLikeRedisManager.getLikedStatus(sharePostId, memberId);
47+
boolean isLiked = redisLikeStatus.orElse(dbResponse.isLiked());
48+
49+
return new SharePostLikeResponse(
50+
dbResponse.getLikeCount() + redisLikeCount,
51+
isLiked
52+
);
2953

30-
return sharePostLikeRepository.getLikeCountAndStatus(sharePostId,memberId);
3154
}
3255
}

src/test/java/io/crops/warmletter/domain/share/cache/PostLikeRedisManagerTest.java

Lines changed: 84 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
package io.crops.warmletter.domain.share.cache;
2-
32
import org.junit.jupiter.api.BeforeEach;
43
import org.junit.jupiter.api.DisplayName;
54
import org.junit.jupiter.api.Test;
@@ -9,8 +8,8 @@
98
import org.mockito.junit.jupiter.MockitoExtension;
109
import org.springframework.data.redis.core.StringRedisTemplate;
1110
import org.springframework.data.redis.core.ValueOperations;
12-
1311
import java.util.Map;
12+
import java.util.Optional;
1413
import java.util.Set;
1514

1615
import static org.junit.jupiter.api.Assertions.*;
@@ -34,19 +33,21 @@ void setUp() {
3433
}
3534

3635
@Test
37-
@DisplayName("좋아요 토글 - 최초 좋아요")
36+
@DisplayName("좋아요 토글 - 좋아요 추가")
3837
void toggleLike_Success() {
3938
// given
4039
Long postId = 1L;
4140
Long memberId = 1L;
4241
String key = "post:1:like:memberId:1";
43-
when(valueOperations.get(key)).thenReturn(null);
42+
String countKey = "post:1:like:count";
43+
boolean status = true;
4444

4545
// when
46-
postLikeRedisManager.toggleLike(postId, memberId);
46+
postLikeRedisManager.toggleLike(postId, memberId,status);
4747

4848
// then
4949
verify(valueOperations).set(key, "true");
50+
verify(valueOperations).increment(countKey);
5051
}
5152

5253
@Test
@@ -56,13 +57,48 @@ void toggleLike_Cancel() {
5657
Long postId = 1L;
5758
Long memberId = 1L;
5859
String key = "post:1:like:memberId:1";
59-
when(valueOperations.get(key)).thenReturn("true");
60+
String countKey = "post:1:like:count";
61+
boolean status = false;
6062

6163
// when
62-
postLikeRedisManager.toggleLike(postId, memberId);
64+
postLikeRedisManager.toggleLike(postId, memberId,status);
6365

6466
// then
6567
verify(valueOperations).set(key, "false");
68+
verify(valueOperations).decrement(countKey);
69+
}
70+
71+
@Test
72+
@DisplayName("좋아요 상태 조회 - Redis에 데이터 있음")
73+
void getLikedStatus_DataExists() {
74+
// given
75+
Long postId = 1L;
76+
Long memberId = 1L;
77+
String key = "post:1:like:memberId:1";
78+
when(valueOperations.get(key)).thenReturn("true");
79+
80+
// when
81+
Optional<Boolean> result = postLikeRedisManager.getLikedStatus(postId, memberId);
82+
83+
// then
84+
assertTrue(result.isPresent());
85+
assertTrue(result.get());
86+
}
87+
88+
@Test
89+
@DisplayName("좋아요 상태 조회 - Redis에 데이터 없음")
90+
void getLikedStatus_NoData() {
91+
// given
92+
Long postId = 1L;
93+
Long memberId = 1L;
94+
String key = "post:1:like:memberId:1";
95+
when(valueOperations.get(key)).thenReturn(null);
96+
97+
// when
98+
Optional<Boolean> result = postLikeRedisManager.getLikedStatus(postId, memberId);
99+
100+
// then
101+
assertFalse(result.isPresent());
66102
}
67103

68104
@Test
@@ -79,7 +115,7 @@ void getAllLikeStatus() {
79115
assertTrue(likeStatus.get("post:1:like:memberId:1"));
80116
assertFalse(likeStatus.get("post:2:like:memberId:1"));
81117
}
82-
// String value = redisTemplate.opsForValue().get(key);
118+
83119
@Test
84120
@DisplayName("상태 확인 - 좋아요 없음")
85121
void isLiked_False() {
@@ -137,23 +173,54 @@ void isLiked_WithFalseValue() {
137173
assertFalse(result);
138174
verify(valueOperations).get(key);
139175
}
176+
@Test
177+
@DisplayName("좋아요 카운트 조회")
178+
void getLikeCount() {
179+
// given
180+
Long postId = 1L;
181+
String countKey = "post:1:like:count";
182+
when(valueOperations.get(countKey)).thenReturn("5");
183+
184+
// when
185+
int count = postLikeRedisManager.getLikeCount(postId);
186+
187+
// then
188+
assertEquals(5, count);
189+
verify(valueOperations).get(countKey);
190+
}
140191

141192
@Test
142-
@DisplayName("좋아요 토글 - false에서 true로 변경")
143-
void toggleLike_FromFalseToTrue() {
193+
@DisplayName("좋아요 카운트 조회 - 데이터 없음")
194+
void getLikeCount_NoData() {
144195
// given
145196
Long postId = 1L;
146-
Long memberId = 1L;
147-
String key = "post:1:like:memberId:1";
148-
when(valueOperations.get(key)).thenReturn("false");
197+
String countKey = "post:1:like:count";
198+
when(valueOperations.get(countKey)).thenReturn(null);
149199

150200
// when
151-
postLikeRedisManager.toggleLike(postId, memberId);
201+
int count = postLikeRedisManager.getLikeCount(postId);
152202

153203
// then
154-
verify(valueOperations).set(key, "true");
204+
assertEquals(0, count);
205+
verify(valueOperations).get(countKey);
155206
}
156207

208+
// @Test
209+
// @DisplayName("좋아요 토글 - false에서 true로 변경")
210+
// void toggleLike_FromFalseToTrue() {
211+
// // given
212+
// Long postId = 1L;
213+
// Long memberId = 1L;
214+
// String key = "post:1:like:memberId:1";
215+
// when(valueOperations.get(key)).thenReturn("false");
216+
//
217+
// // when
218+
// postLikeRedisManager.toggleLike(postId, memberId,);
219+
//
220+
// // then
221+
// verify(valueOperations).set(key, "true");
222+
// }
223+
157224
@Test
158225
@DisplayName("isLiked - 빈 문자열 값일 경우 false 반환")
159226
void isLiked_WithEmptyString() {
@@ -176,10 +243,9 @@ void isLiked_WithEmptyString() {
176243
void clearCache_WithNullKeys() {
177244
// given
178245
when(redisTemplate.keys("post:*:like:memberId:*")).thenReturn(null);
179-
246+
when(redisTemplate.keys("post:*:like:count")).thenReturn(null);
180247
// when
181248
postLikeRedisManager.clearCache();
182-
183249
// then
184250
verify(redisTemplate, never()).delete(any(Set.class));
185251
}
@@ -207,6 +273,7 @@ void getAllLikeStatus_MixedNullValues() {
207273
void clearCache_WithEmptySet() {
208274
// given
209275
when(redisTemplate.keys("post:*:like:memberId:*")).thenReturn(Set.of());
276+
when(redisTemplate.keys("post:*:like:count")).thenReturn(Set.of());
210277

211278
// when
212279
postLikeRedisManager.clearCache();

0 commit comments

Comments
 (0)