Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,21 @@
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Objects;

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Table(name = "book_log_like", uniqueConstraints = {
@UniqueConstraint(columnNames = {"user_id", "book_log_id"})
})
@Entity
public class BookLogLike extends BaseEntity {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@

import dayone.dayone.booklog.entity.BookLog;
import dayone.dayone.bookloglike.entity.BookLogLike;
import jakarta.persistence.LockModeType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;
import java.util.Optional;
import java.util.Set;

public interface BookLogLikeRepository extends JpaRepository<BookLogLike, Long> {

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

List<BookLogLike> findAllByBookLogId(final Long bookLogId);

List<BookLogLike> findAllByBookLogId(final BookLog bookLogId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,10 @@
import dayone.dayone.user.exception.UserErrorCode;
import dayone.dayone.user.exception.UserException;
import lombok.RequiredArgsConstructor;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

@Transactional(readOnly = true)
@RequiredArgsConstructor
@Service
Expand All @@ -33,27 +32,39 @@ public void addLike(final Long bookLogId, final Long userId) {
userRepository.findById(userId)
.orElseThrow(() -> new UserException(UserErrorCode.NOT_EXIST_USER));

final Optional<BookLogLike> alreadyLike = bookLogLikeRepository.findAllByUserIdAndBookLogId(userId, bookLogId);
if (alreadyLike.isPresent()) {
// final Optional<BookLogLike> alreadyLike = bookLogLikeRepository.findBookLogLikeByUserIdAndBookLogIdForUpdate(userId,bookLogId);
// if (alreadyLike.isPresent()) {
// throw new BookLogLikeException(BookLogLikeErrorCode.ALREADY_LIKE_BOOK_LOG);
// }
//
// final BookLogLike bookLogLike = BookLogLike.forSave(userId, bookLogId);
// bookLogLikeRepository.save(bookLogLike);
// bookLogRepository.plusLike(bookLogId);
try {
// 중복 여부 확인 없이 바로 삽입
BookLogLike like = BookLogLike.forSave(userId, bookLogId);
bookLogLikeRepository.save(like);

// 좋아요 수 증가 (Optional: 이벤트로 분리 가능)
bookLogRepository.plusLike(bookLogId);
} catch (DataIntegrityViolationException e) {
// 유니크 키 제약 위반 시 중복 좋아요로 판단
throw new BookLogLikeException(BookLogLikeErrorCode.ALREADY_LIKE_BOOK_LOG);
}

final BookLogLike bookLogLike = BookLogLike.forSave(userId, bookLogId);
bookLogLikeRepository.save(bookLogLike);
bookLogRepository.plusLike(bookLogId);
}

@Transactional
public void deleteLike(final Long bookLogId, final long userId) {
public void deleteLike(final Long bookLogId, final Long userId) {
bookLogRepository.findById(bookLogId)
.orElseThrow(() -> new BookLogException(BookLogErrorCode.NOT_EXIST_BOOK_LOG));

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

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


bookLogLikeRepository.delete(bookLogLike);
bookLogRepository.minusLike(bookLogId);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

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

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

int threadCount = 10;
final int threadCount = 10;
final ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
final CountDownLatch countDownLatch = new CountDownLatch(threadCount);

Expand All @@ -135,6 +136,45 @@ void addLikeOnBookLogWithManyUserSimultaneously() throws InterruptedException {
softAssertions.assertThat(bookLogLikeByBookLogId).hasSize(10);
});
}

@DisplayName("같은 유저가 같은 BookLog에 좋아요를 동시에 2번 추가할 경우 1번만 추가된다.")
@Test
void addLikeOnBookLogWithConcurrentSameUser() throws InterruptedException {
// given
final Book book = testBookFactory.createBook("책", "작가", "출판사");
final User user = testUserFactory.createUser("test@test.com", "password", "이름", 1);
final BookLog bookLog = testBookLogFactory.createBookLog(book, user);

final User anotherUser = testUserFactory.createUser("test2@test.com", "password", "이름", 1);

final int threadCount = 2;
final ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
final CountDownLatch countDownLatch = new CountDownLatch(threadCount);

// when
final AtomicInteger errorCount = new AtomicInteger(0);
for (int i = 0; i < threadCount; i++) {
executorService.submit(() -> {
try {
bookLogLikeService.addLike(bookLog.getId(), anotherUser.getId());
} catch (Exception ignored) {
errorCount.incrementAndGet();
} finally {
countDownLatch.countDown();
}
});
}
countDownLatch.await();

// then
final BookLog likedBookLog = bookLogRepository.findById(bookLog.getId()).get();
final List<BookLogLike> bookLogLikeByBookLogId = bookLogLikeRepository.findAllByBookLogId(bookLog.getId());
SoftAssertions.assertSoftly(softAssertions -> {
softAssertions.assertThat(likedBookLog.getLikeCount()).isEqualTo(1);
softAssertions.assertThat(bookLogLikeByBookLogId).hasSize(1);
softAssertions.assertThat(errorCount.get()).isEqualTo(threadCount - 1);
});
}
}

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

int threadCount = 10;
final int threadCount = 10;
final ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
final CountDownLatch countDownLatch = new CountDownLatch(threadCount);

Expand All @@ -224,5 +264,46 @@ void deleteLikeOnBookLogWithManyUserSimultaneously() throws InterruptedException
softAssertions.assertThat(bookLogLikeByBookLogId).hasSize(0);
});
}

@DisplayName("같은 유저가 BookLog에 좋아요 취소를 동시에 두번 하더라도 1번만 취소 처리된다.")
@Test
void deleteLikeOnBookLogWithConcurrentSameUser() throws InterruptedException {
// given
final Book book = testBookFactory.createBook("책", "작가", "출판사");
final User user = testUserFactory.createUser("test@test.com", "password", "이름", 1);
final BookLog bookLog = testBookLogFactory.createBookLog(book, user);

final User anotherUser = testUserFactory.createUser("test2@test.com", "password", "이름", 1);
testBookLogLikeFactory.createNBookLogLike(bookLog.getId(), List.of(anotherUser));

final int threadCount = 2;
final ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
final CountDownLatch countDownLatch = new CountDownLatch(threadCount);

// when
final AtomicInteger errorCount = new AtomicInteger(0);
for (int i = 0; i < threadCount; i++) {
executorService.submit(() -> {
try {
bookLogLikeService.deleteLike(bookLog.getId(), anotherUser.getId());
} catch (Exception ignored) {
errorCount.incrementAndGet();
} finally {
countDownLatch.countDown();
}

});
}
countDownLatch.await();

// then
final BookLog notLikedBookLog = bookLogRepository.findById(bookLog.getId()).get();
final List<BookLogLike> bookLogLikeByBookLogId = bookLogLikeRepository.findAllByBookLogId(bookLog.getId());
SoftAssertions.assertSoftly(softAssertions -> {
softAssertions.assertThat(notLikedBookLog.getLikeCount()).isEqualTo(0);
softAssertions.assertThat(bookLogLikeByBookLogId).hasSize(0);
softAssertions.assertThat(errorCount.get()).isEqualTo(threadCount - 1);
});
}
}
}