Skip to content

Commit de8ccaa

Browse files
committed
Merge branch 'feature/refactor_concurrency_add_delete_booklog' into develop
2 parents 7f6cbbd + 2fa2518 commit de8ccaa

4 files changed

Lines changed: 118 additions & 15 deletions

File tree

src/main/java/dayone/dayone/bookloglike/entity/BookLogLike.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,21 @@
55
import jakarta.persistence.GeneratedValue;
66
import jakarta.persistence.GenerationType;
77
import jakarta.persistence.Id;
8+
import jakarta.persistence.Table;
9+
import jakarta.persistence.UniqueConstraint;
810
import lombok.AccessLevel;
911
import lombok.Getter;
1012
import lombok.NoArgsConstructor;
1113

1214
import java.time.LocalDateTime;
1315
import java.time.temporal.ChronoUnit;
16+
import java.util.Objects;
1417

1518
@NoArgsConstructor(access = AccessLevel.PROTECTED)
1619
@Getter
20+
@Table(name = "book_log_like", uniqueConstraints = {
21+
@UniqueConstraint(columnNames = {"user_id", "book_log_id"})
22+
})
1723
@Entity
1824
public class BookLogLike extends BaseEntity {
1925

src/main/java/dayone/dayone/bookloglike/entity/repository/BookLogLikeRepository.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,21 @@
22

33
import dayone.dayone.booklog.entity.BookLog;
44
import dayone.dayone.bookloglike.entity.BookLogLike;
5+
import jakarta.persistence.LockModeType;
56
import org.springframework.data.jpa.repository.JpaRepository;
7+
import org.springframework.data.jpa.repository.Lock;
8+
import org.springframework.data.jpa.repository.Query;
9+
import org.springframework.data.repository.query.Param;
610

711
import java.util.List;
812
import java.util.Optional;
13+
import java.util.Set;
914

1015
public interface BookLogLikeRepository extends JpaRepository<BookLogLike, Long> {
1116

12-
Optional<BookLogLike> findAllByUserIdAndBookLogId(final Long userId, final Long bookLogId);
17+
@Lock(LockModeType.PESSIMISTIC_WRITE)
18+
@Query("SELECT bl FROM BookLogLike bl WHERE bl.userId = :userId AND bl.bookLogId = :bookLogId")
19+
Optional<BookLogLike> findBookLogLikeByUserIdAndBookLogIdForUpdate(final @Param("userId") Long userId, final @Param("bookLogId") Long bookLogId);
1320

1421
List<BookLogLike> findAllByBookLogId(final Long bookLogId);
15-
16-
List<BookLogLike> findAllByBookLogId(final BookLog bookLogId);
1722
}

src/main/java/dayone/dayone/bookloglike/service/BookLogLikeService.java

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,10 @@
1111
import dayone.dayone.user.exception.UserErrorCode;
1212
import dayone.dayone.user.exception.UserException;
1313
import lombok.RequiredArgsConstructor;
14+
import org.springframework.dao.DataIntegrityViolationException;
1415
import org.springframework.stereotype.Service;
1516
import org.springframework.transaction.annotation.Transactional;
1617

17-
import java.util.Optional;
18-
1918
@Transactional(readOnly = true)
2019
@RequiredArgsConstructor
2120
@Service
@@ -33,27 +32,39 @@ public void addLike(final Long bookLogId, final Long userId) {
3332
userRepository.findById(userId)
3433
.orElseThrow(() -> new UserException(UserErrorCode.NOT_EXIST_USER));
3534

36-
final Optional<BookLogLike> alreadyLike = bookLogLikeRepository.findAllByUserIdAndBookLogId(userId, bookLogId);
37-
if (alreadyLike.isPresent()) {
35+
// final Optional<BookLogLike> alreadyLike = bookLogLikeRepository.findBookLogLikeByUserIdAndBookLogIdForUpdate(userId,bookLogId);
36+
// if (alreadyLike.isPresent()) {
37+
// throw new BookLogLikeException(BookLogLikeErrorCode.ALREADY_LIKE_BOOK_LOG);
38+
// }
39+
//
40+
// final BookLogLike bookLogLike = BookLogLike.forSave(userId, bookLogId);
41+
// bookLogLikeRepository.save(bookLogLike);
42+
// bookLogRepository.plusLike(bookLogId);
43+
try {
44+
// 중복 여부 확인 없이 바로 삽입
45+
BookLogLike like = BookLogLike.forSave(userId, bookLogId);
46+
bookLogLikeRepository.save(like);
47+
48+
// 좋아요 수 증가 (Optional: 이벤트로 분리 가능)
49+
bookLogRepository.plusLike(bookLogId);
50+
} catch (DataIntegrityViolationException e) {
51+
// 유니크 키 제약 위반 시 중복 좋아요로 판단
3852
throw new BookLogLikeException(BookLogLikeErrorCode.ALREADY_LIKE_BOOK_LOG);
3953
}
40-
41-
final BookLogLike bookLogLike = BookLogLike.forSave(userId, bookLogId);
42-
bookLogLikeRepository.save(bookLogLike);
43-
bookLogRepository.plusLike(bookLogId);
4454
}
4555

4656
@Transactional
47-
public void deleteLike(final Long bookLogId, final long userId) {
57+
public void deleteLike(final Long bookLogId, final Long userId) {
4858
bookLogRepository.findById(bookLogId)
4959
.orElseThrow(() -> new BookLogException(BookLogErrorCode.NOT_EXIST_BOOK_LOG));
5060

5161
userRepository.findById(userId)
5262
.orElseThrow(() -> new UserException(UserErrorCode.NOT_EXIST_USER));
5363

54-
final BookLogLike bookLogLike = bookLogLikeRepository.findAllByUserIdAndBookLogId(userId, bookLogId)
64+
final BookLogLike bookLogLike = bookLogLikeRepository.findBookLogLikeByUserIdAndBookLogIdForUpdate(userId, bookLogId)
5565
.orElseThrow(() -> new BookLogLikeException(BookLogLikeErrorCode.NOT_LIKE_BOOK_LOG));
5666

67+
5768
bookLogLikeRepository.delete(bookLogLike);
5869
bookLogRepository.minusLike(bookLogId);
5970
}

src/test/java/dayone/dayone/bookloglike/service/BookLogLikeServiceTest.java

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.concurrent.CountDownLatch;
2626
import java.util.concurrent.ExecutorService;
2727
import java.util.concurrent.Executors;
28+
import java.util.concurrent.atomic.AtomicInteger;
2829

2930
import static org.assertj.core.api.Assertions.assertThatThrownBy;
3031

@@ -112,7 +113,7 @@ void addLikeOnBookLogWithManyUserSimultaneously() throws InterruptedException {
112113
final List<User> users = testUserFactory.createNUser(10, "test@test.com", "password", "이름", 1);
113114
final BookLog bookLog = testBookLogFactory.createBookLog(book, users.get(0));
114115

115-
int threadCount = 10;
116+
final int threadCount = 10;
116117
final ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
117118
final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
118119

@@ -135,6 +136,45 @@ void addLikeOnBookLogWithManyUserSimultaneously() throws InterruptedException {
135136
softAssertions.assertThat(bookLogLikeByBookLogId).hasSize(10);
136137
});
137138
}
139+
140+
@DisplayName("같은 유저가 같은 BookLog에 좋아요를 동시에 2번 추가할 경우 1번만 추가된다.")
141+
@Test
142+
void addLikeOnBookLogWithConcurrentSameUser() throws InterruptedException {
143+
// given
144+
final Book book = testBookFactory.createBook("책", "작가", "출판사");
145+
final User user = testUserFactory.createUser("test@test.com", "password", "이름", 1);
146+
final BookLog bookLog = testBookLogFactory.createBookLog(book, user);
147+
148+
final User anotherUser = testUserFactory.createUser("test2@test.com", "password", "이름", 1);
149+
150+
final int threadCount = 2;
151+
final ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
152+
final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
153+
154+
// when
155+
final AtomicInteger errorCount = new AtomicInteger(0);
156+
for (int i = 0; i < threadCount; i++) {
157+
executorService.submit(() -> {
158+
try {
159+
bookLogLikeService.addLike(bookLog.getId(), anotherUser.getId());
160+
} catch (Exception ignored) {
161+
errorCount.incrementAndGet();
162+
} finally {
163+
countDownLatch.countDown();
164+
}
165+
});
166+
}
167+
countDownLatch.await();
168+
169+
// then
170+
final BookLog likedBookLog = bookLogRepository.findById(bookLog.getId()).get();
171+
final List<BookLogLike> bookLogLikeByBookLogId = bookLogLikeRepository.findAllByBookLogId(bookLog.getId());
172+
SoftAssertions.assertSoftly(softAssertions -> {
173+
softAssertions.assertThat(likedBookLog.getLikeCount()).isEqualTo(1);
174+
softAssertions.assertThat(bookLogLikeByBookLogId).hasSize(1);
175+
softAssertions.assertThat(errorCount.get()).isEqualTo(threadCount - 1);
176+
});
177+
}
138178
}
139179

140180
@DisplayName("BookLog 좋아요 취소")
@@ -203,7 +243,7 @@ void deleteLikeOnBookLogWithManyUserSimultaneously() throws InterruptedException
203243
final BookLog bookLog = testBookLogFactory.createBookLog(book, users.get(0));
204244
testBookLogLikeFactory.createNBookLogLike(bookLog.getId(), users);
205245

206-
int threadCount = 10;
246+
final int threadCount = 10;
207247
final ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
208248
final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
209249

@@ -224,5 +264,46 @@ void deleteLikeOnBookLogWithManyUserSimultaneously() throws InterruptedException
224264
softAssertions.assertThat(bookLogLikeByBookLogId).hasSize(0);
225265
});
226266
}
267+
268+
@DisplayName("같은 유저가 BookLog에 좋아요 취소를 동시에 두번 하더라도 1번만 취소 처리된다.")
269+
@Test
270+
void deleteLikeOnBookLogWithConcurrentSameUser() throws InterruptedException {
271+
// given
272+
final Book book = testBookFactory.createBook("책", "작가", "출판사");
273+
final User user = testUserFactory.createUser("test@test.com", "password", "이름", 1);
274+
final BookLog bookLog = testBookLogFactory.createBookLog(book, user);
275+
276+
final User anotherUser = testUserFactory.createUser("test2@test.com", "password", "이름", 1);
277+
testBookLogLikeFactory.createNBookLogLike(bookLog.getId(), List.of(anotherUser));
278+
279+
final int threadCount = 2;
280+
final ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
281+
final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
282+
283+
// when
284+
final AtomicInteger errorCount = new AtomicInteger(0);
285+
for (int i = 0; i < threadCount; i++) {
286+
executorService.submit(() -> {
287+
try {
288+
bookLogLikeService.deleteLike(bookLog.getId(), anotherUser.getId());
289+
} catch (Exception ignored) {
290+
errorCount.incrementAndGet();
291+
} finally {
292+
countDownLatch.countDown();
293+
}
294+
295+
});
296+
}
297+
countDownLatch.await();
298+
299+
// then
300+
final BookLog notLikedBookLog = bookLogRepository.findById(bookLog.getId()).get();
301+
final List<BookLogLike> bookLogLikeByBookLogId = bookLogLikeRepository.findAllByBookLogId(bookLog.getId());
302+
SoftAssertions.assertSoftly(softAssertions -> {
303+
softAssertions.assertThat(notLikedBookLog.getLikeCount()).isEqualTo(0);
304+
softAssertions.assertThat(bookLogLikeByBookLogId).hasSize(0);
305+
softAssertions.assertThat(errorCount.get()).isEqualTo(threadCount - 1);
306+
});
307+
}
227308
}
228309
}

0 commit comments

Comments
 (0)