Skip to content

Commit 5193457

Browse files
committed
refactor: Like 중복 체크 로직 제거
- LikeFacade에서 멱등성 체크 담당 - LikeService에서 중복 체크 제거 - 불필요한 productRepository.findById 호출 제거
1 parent b191947 commit 5193457

5 files changed

Lines changed: 55 additions & 66 deletions

File tree

apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
import com.loopers.application.event.like.ProductLikedEvent;
44
import com.loopers.application.event.like.ProductUnlikedEvent;
55
import com.loopers.application.event.tracking.UserActionEvent;
6+
import com.loopers.domain.like.repository.LikeRepository;
67
import com.loopers.domain.like.service.LikeService;
78
import com.loopers.domain.product.Product;
8-
import com.loopers.domain.product.repository.ProductRepository;
99
import lombok.RequiredArgsConstructor;
1010
import lombok.extern.slf4j.Slf4j;
1111
import org.springframework.context.ApplicationEventPublisher;
@@ -22,11 +22,17 @@
2222
public class LikeFacade {
2323

2424
private final LikeService likeService;
25-
private final ProductRepository productRepository;
25+
private final LikeRepository likeRepository;
2626
private final ApplicationEventPublisher eventPublisher;
2727

2828
public void likeProduct(Long memberId, Long productId) {
29-
// 1. DB에 좋아요 저장 (Redis 로직은 LikeService에서 제거됨)
29+
// 멱등성: 이미 좋아요한 경우 early return (좋아요가 존재하면 상품도 존재함)
30+
if (likeRepository.existsByMemberIdAndProductId(memberId, productId)) {
31+
log.debug("[LikeFacade] 이미 좋아요한 상품 - memberId: {}, productId: {}", memberId, productId);
32+
return;
33+
}
34+
35+
// 1. DB에 좋아요 저장
3036
Product product = likeService.like(memberId, productId);
3137

3238
// 2. 이벤트 발행 (Redis 업데이트는 비동기 리스너에서 처리)
@@ -49,7 +55,13 @@ public void likeProduct(Long memberId, Long productId) {
4955
}
5056

5157
public void unlikeProduct(Long memberId, Long productId) {
52-
// 1. DB에서 좋아요 삭제 (Redis 로직은 LikeService에서 제거됨)
58+
// 멱등성: 좋아요하지 않은 경우 early return
59+
if (!likeRepository.existsByMemberIdAndProductId(memberId, productId)) {
60+
log.debug("[LikeFacade] 좋아요하지 않은 상품 - memberId: {}, productId: {}", memberId, productId);
61+
return;
62+
}
63+
64+
// 1. DB에서 좋아요 삭제
5365
Product product = likeService.unlike(memberId, productId);
5466

5567
// 2. 이벤트 발행 (Redis 업데이트는 비동기 리스너에서 처리)

apps/commerce-api/src/main/java/com/loopers/domain/like/service/LikeService.java

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,16 @@ public class LikeService {
2828
* - DB에는 Like 레코드만 저장
2929
* - 좋아요 카운트는 이벤트 리스너에서 Redis에 업데이트
3030
* - 스케줄러가 주기적으로 Redis → DB 동기화
31+
* - 멱등성은 Facade에서 보장 (중복 체크는 Facade 책임)
3132
*
3233
* @return 좋아요한 상품 (이벤트 발행용 brandId 포함)
3334
*/
3435
public Product like(Long memberId, Long productId) {
35-
// 중복 좋아요 방지 (멱등성)
36-
if (likeRepository.existsByMemberIdAndProductId(memberId, productId)) {
37-
log.debug("[LikeService] 이미 좋아요한 상품 - memberId: {}, productId: {}", memberId, productId);
38-
return productRepository.findById(productId)
39-
.orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "상품을 찾을 수 없습니다."));
40-
}
41-
42-
// 1. 상품 존재 확인 (비관적 락 제거)
36+
// 1. 상품 존재 확인
4337
Product product = productRepository.findById(productId)
4438
.orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "상품을 찾을 수 없습니다."));
4539

46-
// 2. DB에 Like 레코드만 저장 (카운트는 Redis에서 관리)
40+
// 2. DB에 Like 레코드 저장 (카운트는 Redis에서 관리)
4741
likeRepository.save(new Like(memberId, productId));
4842

4943
log.info("[LikeService] 좋아요 저장 완료 - memberId: {}, productId: {}", memberId, productId);
@@ -56,22 +50,16 @@ public Product like(Long memberId, Long productId) {
5650
* - DB에서 Like 레코드만 삭제
5751
* - 좋아요 카운트는 이벤트 리스너에서 Redis에 업데이트
5852
* - 스케줄러가 주기적으로 Redis → DB 동기화
53+
* - 멱등성은 Facade에서 보장 (존재 여부 체크는 Facade 책임)
5954
*
6055
* @return 좋아요 취소한 상품 (이벤트 발행용 brandId 포함)
6156
*/
6257
public Product unlike(Long memberId, Long productId) {
63-
// 좋아요 없으면 스킵 (멱등성)
64-
if (!likeRepository.existsByMemberIdAndProductId(memberId, productId)) {
65-
log.debug("[LikeService] 좋아요하지 않은 상품 - memberId: {}, productId: {}", memberId, productId);
66-
return productRepository.findById(productId)
67-
.orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "상품을 찾을 수 없습니다."));
68-
}
69-
70-
// 1. 상품 존재 확인 (비관적 락 제거)
58+
// 1. 상품 존재 확인
7159
Product product = productRepository.findById(productId)
7260
.orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "상품을 찾을 수 없습니다."));
7361

74-
// 2. DB에서 Like 레코드만 삭제 (카운트는 Redis에서 관리)
62+
// 2. DB에서 Like 레코드 삭제 (카운트는 Redis에서 관리)
7563
likeRepository.deleteByMemberIdAndProductId(memberId, productId);
7664

7765
log.info("[LikeService] 좋아요 취소 완료 - memberId: {}, productId: {}", memberId, productId);

apps/commerce-api/src/test/java/com/loopers/application/event/integration/LikeEventIntegrationTest.java

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import com.loopers.domain.common.vo.Money;
66
import com.loopers.domain.like.Like;
77
import com.loopers.domain.like.repository.LikeRepository;
8-
import com.loopers.domain.like.service.LikeService;
8+
import com.loopers.application.like.LikeFacade;
99
import com.loopers.domain.members.Member;
1010
import com.loopers.domain.members.enums.Gender;
1111
import com.loopers.domain.members.repository.MemberRepository;
@@ -14,6 +14,7 @@
1414
import com.loopers.domain.product.vo.Stock;
1515
import com.loopers.infrastructure.cache.MemberLikesCache;
1616
import com.loopers.infrastructure.cache.ProductLikeCountCache;
17+
import com.loopers.testcontainers.RedisTestContainersConfig;
1718
import com.loopers.utils.DatabaseCleanUp;
1819
import jakarta.persistence.EntityManager;
1920
import org.junit.jupiter.api.AfterEach;
@@ -25,6 +26,7 @@
2526
import org.springframework.data.redis.core.RedisTemplate;
2627
import org.springframework.test.context.event.ApplicationEvents;
2728
import org.springframework.test.context.event.RecordApplicationEvents;
29+
import org.springframework.context.annotation.Import;
2830

2931
import java.util.concurrent.TimeUnit;
3032

@@ -41,12 +43,13 @@
4143
* 4. 여러 좋아요/취소 작업 후 데이터 일치 확인
4244
*/
4345
@SpringBootTest
46+
@Import(RedisTestContainersConfig.class)
4447
@RecordApplicationEvents
4548
@DisplayName("좋아요 이벤트 통합 테스트")
4649
class LikeEventIntegrationTest {
4750

4851
@Autowired
49-
private LikeService likeService;
52+
private LikeFacade likeFacade;
5053

5154
@Autowired
5255
private LikeRepository likeRepository;
@@ -84,14 +87,13 @@ void setUp() {
8487
redisTemplate.getConnectionFactory().getConnection().flushDb();
8588

8689
// 테스트 회원 생성
87-
testMember = new Member("like-user", "like@example.com", "password123", "1995-05-05", Gender.FEMALE);
90+
testMember = new Member("likeuser", "like@example.com", "password123", "1995-05-05", Gender.FEMALE);
8891
testMember = memberRepository.save(testMember);
8992

9093
// 테스트 상품 생성
9194
testProduct = new Product(10L, "인기 상품", "좋아요 테스트용", Money.of(50000), Stock.of(50));
9295
testProduct = productRepository.save(testProduct);
9396

94-
entityManager.flush();
9597
entityManager.clear();
9698
}
9799

@@ -109,7 +111,7 @@ void tearDown() {
109111
Long productId = testProduct.getId();
110112

111113
// when
112-
likeService.like(memberId, productId);
114+
likeFacade.likeProduct(memberId, productId);
113115

114116
// then - DB에 Like 레코드 저장 확인
115117
entityManager.clear();
@@ -142,7 +144,7 @@ void tearDown() {
142144
// given - 먼저 좋아요 생성
143145
Long memberId = testMember.getId();
144146
Long productId = testProduct.getId();
145-
likeService.like(memberId, productId);
147+
likeFacade.likeProduct(memberId, productId);
146148

147149
// 비동기 처리 대기
148150
await().atMost(3, TimeUnit.SECONDS).untilAsserted(() -> {
@@ -152,7 +154,7 @@ void tearDown() {
152154
entityManager.clear();
153155

154156
// when - 좋아요 취소
155-
likeService.unlike(memberId, productId);
157+
likeFacade.unlikeProduct(memberId, productId);
156158

157159
// then - DB에서 Like 레코드 삭제 확인
158160
entityManager.clear();
@@ -190,9 +192,9 @@ void tearDown() {
190192
Long productId = testProduct.getId();
191193

192194
// when - 3명이 좋아요
193-
likeService.like(testMember.getId(), productId);
194-
likeService.like(member2.getId(), productId);
195-
likeService.like(member3.getId(), productId);
195+
likeFacade.likeProduct(testMember.getId(), productId);
196+
likeFacade.likeProduct(member2.getId(), productId);
197+
likeFacade.likeProduct(member3.getId(), productId);
196198

197199
// then - DB에 3개 레코드 저장 확인
198200
entityManager.clear();
@@ -217,12 +219,12 @@ void tearDown() {
217219
Long productId = testProduct.getId();
218220

219221
// when - 복잡한 좋아요/취소 시나리오
220-
likeService.like(testMember.getId(), productId); // +1 = 1
221-
likeService.like(member2.getId(), productId); // +1 = 2
222-
likeService.like(member3.getId(), productId); // +1 = 3
223-
likeService.unlike(member2.getId(), productId); // -1 = 2
224-
likeService.like(member4.getId(), productId); // +1 = 3
225-
likeService.unlike(testMember.getId(), productId); // -1 = 2
222+
likeFacade.likeProduct(testMember.getId(), productId); // +1 = 1
223+
likeFacade.likeProduct(member2.getId(), productId); // +1 = 2
224+
likeFacade.likeProduct(member3.getId(), productId); // +1 = 3
225+
likeFacade.unlikeProduct(member2.getId(), productId); // -1 = 2
226+
likeFacade.likeProduct(member4.getId(), productId); // +1 = 3
227+
likeFacade.unlikeProduct(testMember.getId(), productId); // -1 = 2
226228

227229
// 비동기 처리 완료 대기
228230
await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> {
@@ -252,9 +254,9 @@ void tearDown() {
252254
Long productId = testProduct.getId();
253255

254256
// when - 동일 회원이 같은 상품에 3번 좋아요
255-
likeService.like(memberId, productId);
256-
likeService.like(memberId, productId);
257-
likeService.like(memberId, productId);
257+
likeFacade.likeProduct(memberId, productId);
258+
likeFacade.likeProduct(memberId, productId);
259+
likeFacade.likeProduct(memberId, productId);
258260

259261
// then - DB에는 1개만 저장
260262
entityManager.clear();

apps/commerce-api/src/test/java/com/loopers/domain/like/LikeConcurrencyTest.java

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import com.loopers.domain.brand.Brand;
77
import com.loopers.domain.brand.repository.BrandRepository;
88
import com.loopers.domain.common.vo.Money;
9+
import com.loopers.domain.like.repository.LikeRepository;
910
import com.loopers.domain.members.enums.Gender;
1011
import com.loopers.domain.product.Product;
1112
import com.loopers.domain.product.repository.ProductRepository;
@@ -39,6 +40,9 @@ class LikeConcurrencyTest {
3940
@Autowired
4041
private ProductRepository productRepository;
4142

43+
@Autowired
44+
private LikeRepository likeRepository;
45+
4246
@Autowired
4347
private BrandRepository brandRepository;
4448

@@ -93,8 +97,8 @@ void shouldHandleConcurrentLikes_whenMultipleUsersLikeSameProduct() throws Inter
9397
}
9498

9599
// then
96-
Product result = productRepository.findById(productId).orElseThrow();
97-
assertThat(result.getLikeCount()).isEqualTo(threadCount);
100+
long likeCount = likeRepository.countByProductId(productId);
101+
assertThat(likeCount).isEqualTo(threadCount);
98102
}
99103

100104
@Test
@@ -124,8 +128,8 @@ void shouldHandleConcurrentUnlikes_whenMultipleUsersUnlikeSameProduct() throws I
124128
}
125129

126130
// 좋아요 개수 확인
127-
Product beforeUnlike = productRepository.findById(productId).orElseThrow();
128-
assertThat(beforeUnlike.getLikeCount()).isEqualTo(threadCount);
131+
long beforeUnlikeCount = likeRepository.countByProductId(productId);
132+
assertThat(beforeUnlikeCount).isEqualTo(threadCount);
129133

130134
CountDownLatch latch = new CountDownLatch(threadCount);
131135

@@ -146,8 +150,8 @@ void shouldHandleConcurrentUnlikes_whenMultipleUsersUnlikeSameProduct() throws I
146150
}
147151

148152
// then
149-
Product result = productRepository.findById(productId).orElseThrow();
150-
assertThat(result.getLikeCount()).isEqualTo(0);
153+
long likeCount = likeRepository.countByProductId(productId);
154+
assertThat(likeCount).isEqualTo(0);
151155
}
152156

153157
@Test
@@ -212,7 +216,7 @@ void shouldHandleConcurrentMixedLikes_whenMultipleUsersLikeAndUnlikeSameProduct(
212216
}
213217

214218
// then: likeCount명만 좋아요 상태여야 함
215-
Product result = productRepository.findById(productId).orElseThrow();
216-
assertThat(result.getLikeCount()).isEqualTo(likeCount);
219+
long finalLikeCount = likeRepository.countByProductId(productId);
220+
assertThat(finalLikeCount).isEqualTo(likeCount);
217221
}
218222
}

apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import com.loopers.domain.product.vo.Stock;
1212
import com.loopers.utils.DatabaseCleanUp;
1313
import org.junit.jupiter.api.*;
14-
import jakarta.persistence.EntityManager;
1514
import org.springframework.beans.factory.annotation.Autowired;
1615
import org.springframework.boot.test.context.SpringBootTest;
1716
import org.springframework.transaction.annotation.Transactional;
@@ -36,9 +35,6 @@ class LikeServiceIntegrationTest {
3635
@Autowired
3736
private DatabaseCleanUp cleanUp;
3837

39-
@Autowired
40-
private EntityManager entityManager;
41-
4238
@AfterEach
4339
void tearDown() {
4440
cleanUp.truncateAllTables();
@@ -70,11 +66,6 @@ void likeSuccess() {
7066
// then
7167
Like saved = likeRepository.findByMemberIdAndProductId(member.getId(), product.getId()).orElse(null);
7268
assertThat(saved).isNotNull();
73-
74-
entityManager.flush();
75-
entityManager.clear(); // 1차 캐시 클리어
76-
Product updated = productRepository.findById(product.getId()).get();
77-
assertThat(updated.getLikeCount()).isEqualTo(1);
7869
}
7970

8071
@Test
@@ -93,11 +84,6 @@ void duplicateLike() {
9384
// then
9485
long likeCount = likeRepository.countByProductId(product.getId());
9586
assertThat(likeCount).isEqualTo(1L);
96-
97-
entityManager.flush();
98-
entityManager.clear(); // 1차 캐시 클리어
99-
Product updated = productRepository.findById(product.getId()).get();
100-
assertThat(updated.getLikeCount()).isEqualTo(1); // 증가 X
10187
}
10288

10389
@Test
@@ -116,9 +102,6 @@ void unlikeSuccess() {
116102
// then
117103
Like like = likeRepository.findByMemberIdAndProductId(member.getId(), product.getId()).orElse(null);
118104
assertThat(like).isNull();
119-
120-
Product updated = productRepository.findById(product.getId()).get();
121-
assertThat(updated.getLikeCount()).isEqualTo(0);
122105
}
123106

124107
@Test

0 commit comments

Comments
 (0)