Skip to content

Commit 8cd83e5

Browse files
authored
Merge pull request #10 from Kimjipang/round04
feat: 좋아요 등록 API 비관적락으로 동시성 제어
2 parents f2a37b5 + 76edc9e commit 8cd83e5

6 files changed

Lines changed: 68 additions & 7 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public LikeInfo doLike(LikeV1Dto.LikeRequest request) {
3636
() -> new CoreException(ErrorType.NOT_FOUND, "존재하지 않는 상품입니다.")
3737
);
3838

39-
return likeRepository.findByUserIdAndProductId(userId, productId)
39+
return likeRepository.findByUserIdAndProductIdForUpdate(userId, productId)
4040
.map(LikeInfo::from)
4141
.orElseGet(() -> {
4242
Like newLike = request.toEntity();

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,22 @@
44
import jakarta.persistence.Column;
55
import jakarta.persistence.Entity;
66
import jakarta.persistence.Table;
7+
import jakarta.persistence.UniqueConstraint;
78
import lombok.AccessLevel;
89
import lombok.Getter;
910
import lombok.NoArgsConstructor;
1011

1112
@Getter
1213
@Entity
13-
@Table(name = "product_like")
14+
@Table(
15+
name = "product_like",
16+
uniqueConstraints = {
17+
@UniqueConstraint(
18+
name = "uk_like_user_product",
19+
columnNames = {"ref_user_id", "ref_product_id"}
20+
)
21+
}
22+
)
1423
@NoArgsConstructor(access = AccessLevel.PROTECTED)
1524
public class Like extends BaseEntity {
1625
@Column(name = "ref_user_id", nullable = false)

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import java.util.Optional;
44

55
public interface LikeRepository {
6+
Optional<Like> findByUserIdAndProductIdForUpdate(Long userId, Long productId);
7+
68
Optional<Like> findByUserIdAndProductId(Long userId, Long productId);
79

810
Like save(Like like);

apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package com.loopers.infrastructure.like;
22

33
import com.loopers.domain.like.Like;
4+
import jakarta.persistence.LockModeType;
45
import org.springframework.data.jpa.repository.JpaRepository;
6+
import org.springframework.data.jpa.repository.Lock;
7+
import org.springframework.data.jpa.repository.Query;
58

69
import java.util.Optional;
710

@@ -11,4 +14,8 @@ public interface LikeJpaRepository extends JpaRepository<Like, Long> {
1114
int countByProductId(Long productId);
1215

1316
int countByUserIdAndProductId(Long userId, Long productId);
17+
18+
@Lock(LockModeType.PESSIMISTIC_WRITE)
19+
@Query("SELECT l FROM Like l WHERE l.userId = :userId AND l.productId = :productId")
20+
Optional<Like> findByUserIdAndProductIdForUpdate(Long userId, Long productId);
1421
}

apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
public class LikeRepositoryImpl implements LikeRepository {
1313
private final LikeJpaRepository likeJpaRepository;
1414

15+
@Override
16+
public Optional<Like> findByUserIdAndProductIdForUpdate(Long userId, Long productId) {
17+
return likeJpaRepository.findByUserIdAndProductIdForUpdate(userId, productId);
18+
}
19+
1520
@Override
1621
public Optional<Like> findByUserIdAndProductId(Long userId, Long productId) {
1722
return likeJpaRepository.findByUserIdAndProductId(userId, productId);
Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
package com.loopers.domain.like;
22

33
import com.loopers.application.like.LikeFacade;
4+
import com.loopers.domain.product.Product;
5+
import com.loopers.domain.product.ProductRepository;
6+
import com.loopers.domain.user.Gender;
7+
import com.loopers.domain.user.UserEntity;
8+
import com.loopers.domain.user.UserRepository;
49
import com.loopers.interfaces.api.like.LikeV1Dto;
510
import org.junit.jupiter.api.Test;
611
import org.springframework.beans.factory.annotation.Autowired;
712
import org.springframework.boot.test.context.SpringBootTest;
8-
import org.springframework.transaction.annotation.Transactional;
913

14+
import java.math.BigDecimal;
1015
import java.util.concurrent.CountDownLatch;
1116
import java.util.concurrent.ExecutorService;
1217
import java.util.concurrent.Executors;
1318

1419
import static org.assertj.core.api.Assertions.assertThat;
1520

1621
@SpringBootTest
17-
@Transactional
1822
public class LikeConcurrencyIntegrationTest {
1923

2024
@Autowired
@@ -23,29 +27,63 @@ public class LikeConcurrencyIntegrationTest {
2327
@Autowired
2428
private LikeRepository likeRepository;
2529

30+
@Autowired
31+
private UserRepository userRepository;
32+
33+
@Autowired
34+
private ProductRepository productRepository;
35+
2636
@Test
2737
void 동시에_100개의_좋아요요청이_들어오면_좋아요는_1개만_생성된다() throws Exception {
28-
Long userId = 1L;
29-
Long productId = 1L;
3038

39+
// 1. 유저/상품 테스트 데이터 생성
40+
UserEntity user = userRepository.save(
41+
new UserEntity(
42+
"happy97",
43+
"test@test.com",
44+
Gender.MALE,
45+
"1997-09-23",
46+
"test1234!"
47+
)
48+
);
49+
50+
Product product = productRepository.save(
51+
new Product(
52+
1L,
53+
"테스트 상품",
54+
BigDecimal.valueOf(10000),
55+
100
56+
)
57+
);
58+
59+
Long userId = user.getId();
60+
Long productId = product.getId();
61+
62+
// 2. 동시 실행 환경 설정
3163
int threadCount = 100;
3264
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
3365
CountDownLatch latch = new CountDownLatch(threadCount);
3466

67+
// 3. 동시에 doLike() 실행
3568
for (int i = 0; i < threadCount; i++) {
3669
executor.submit(() -> {
3770
try {
3871
likeFacade.doLike(new LikeV1Dto.LikeRequest(userId, productId));
72+
} catch (Exception e) {
73+
// 스레드 예외 출력 (중요)
74+
e.printStackTrace();
3975
} finally {
4076
latch.countDown();
4177
}
4278
});
4379
}
4480

45-
// 모든 스레드 작업이 종료될 때까지 대기
81+
// 4. 모든 요청이 끝날 때까지 대기
4682
latch.await();
4783

84+
// 5. 좋아요 개수 확인
4885
long count = likeRepository.countByUserIdAndProductId(userId, productId);
86+
4987
assertThat(count).isEqualTo(1);
5088
}
5189
}

0 commit comments

Comments
 (0)