Skip to content

Conversation

@hd0rable
Copy link
Member

@hd0rable hd0rable commented Nov 12, 2025

#️⃣ 연관된 이슈

closes #333
closes #322

📝 작업 내용

  • 현재 게시글 좋아요에 동시에 요청이 발생할시 데드락이 걸리는 500 에러를 비관락을 적용하여 해결하였습니다.
  • 데드락 에러를 해결하기위해 비관락을 선택한 과정을 노션에 문서화해두었습니다.
  • 저번 회의때 발표한것과는 다르개 낙관락의 재시도 로직을 현실적으로 줄여서 다시 수정하여 문서화 업데이트해두었습니다
    https://separate-snowplow-d8b.notion.site/API-294b701eb0b880088501d46c49b434ad?source=copy_link
  • 추가로 낙관,비관락을 비교하기위해 사용한 k6 부하 스크립트도 추가하였습니닷
  • x-lock을 도입해 성능이 병목될것을 우려하여 성능을 개선하기위해 추후에 부하스크립트 시나리오 구체화 및, 레디스 INCR연산을 사용하여 성능을 개선할 예정입니다! 우선은 데드락 에러를 해결한 부분만 포함하여 pr올립니닷

📸 스크린샷

💬 리뷰 요구사항

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요

📌 PR 진행 시 이러한 점들을 참고해 주세요

* P1 : 꼭 반영해 주세요 (Request Changes) - 이슈가 발생하거나 취약점이 발견되는 케이스 등
* P2 : 반영을 적극적으로 고려해 주시면 좋을 것 같아요 (Comment)
* P3 : 이런 방법도 있을 것 같아요~ 등의 사소한 의견입니다 (Chore)

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 좋아요 시 알림 전송 기능 추가
  • 버그 수정

    • 좋아요/취소 동작의 동시성 문제 완화(갱신 시 잠금으로 데이터 무결성 강화)
  • 테스트

    • 다양한 부하·동시성 시나리오의 자동화된 로드 테스트 및 동시성 단위 테스트 추가로 안정성 검증 강화

@coderabbitai
Copy link

coderabbitai bot commented Nov 12, 2025

Walkthrough

게시글 좋아요의 동시성·정합성 문제 해결을 위해 Feed/Record/Vote 조회에 PESSIMISTIC_WRITE 락 기반의 read-for-update 경로를 추가하고, PostLikeService 흐름을 업데이트했으며 k6 부하 테스트 스크립트 3개와 동시성 단위 테스트를 추가했습니다.

Changes

Cohort / File(s) 변경 요약
K6 로드 테스트 스크립트
loadtest/feed/feed_like_concurrency_test1.js, loadtest/feed/feed_like_concurrency_test2.js, loadtest/feed/feed_like_concurrency_test3.js
세 가지 k6 스크립트 신규 추가: 동시성/램프업/읽기·쓰기 혼합 시나리오, 토큰 사전생성, 응답 검증, htmlReport 기반 요약 생성
Feed 영속성 리포지토리
src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedJpaRepository.java
findByPostIdForUpdate(Long postId) 추가, @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query로 PESSIMISTIC_WRITE 락 적용
Feed 명령 포트·어댑터
src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java, src/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.java
findByIdForUpdate 구현/선언 및 getByIdOrThrowForUpdate 추가 (업데이트용 조회 경로)
Record 영속성 리포지토리 & 포트·어댑터
src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/record/RecordJpaRepository.java, .../RecordCommandPersistenceAdapter.java, .../RecordCommandPort.java
findByPostIdForUpdate 리포지토리 메서드 추가(락 적용) 및 포트/어댑터에 findByIdForUpdate/getByIdOrThrowForUpdate 추가
Vote 영속성 리포지토리 & 포트·어댑터
src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteJpaRepository.java, .../VoteCommandPersistenceAdapter.java, .../VoteCommandPort.java
findByPostIdForUpdate 리포지토리 메서드 추가(락 적용), 포트/어댑터에 findByIdForUpdate/getByIdOrThrowForUpdate 추가; Lock 관련 임포트 추가
Post 비즈니스 로직 및 핸들러
src/main/java/konkuk/thip/post/application/service/PostLikeService.java, src/main/java/konkuk/thip/post/application/service/handler/PostHandler.java
findPostForUpdate 사용으로 읽기→업데이트(사전 like count 변경) 순서 조정, 좋아요 시 알림 트리거 추가, PostHandler에 findPostForUpdate 메서드 추가
테스트 코드
src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeLikeStatusApiTest.java, src/test/java/konkuk/thip/feed/concurrency/FeedChangeLikeStatusConcurrencyTest.java, src/test/java/konkuk/thip/room/adapter/in/web/RoomPostChangeLikeStatusApiTest.java
API 테스트의 전제조건(좋아요 카운트 설정) 보강 및 동시성 테스트 클래스(FeedChangeLikeStatusConcurrencyTest) 신규 추가 (멀티스레드 동시 호출 검증)

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant PostLikeService
    participant PostHandler
    participant CommandPort
    participant JpaRepository
    participant DB

    Client->>PostLikeService: changeLikeStatusPost(command)
    PostLikeService->>PostHandler: findPostForUpdate(type, postId)
    PostHandler->>CommandPort: getByIdOrThrowForUpdate(id)
    CommandPort->>JpaRepository: findByPostIdForUpdate(postId)
    JpaRepository->>DB: SELECT ... FOR UPDATE (PESSIMISTIC_WRITE)
    rect rgb(220,235,255)
      note over DB: 다른 트랜잭션은 대기
    end
    DB-->>JpaRepository: Entity + lock
    JpaRepository-->>CommandPort: Optional<Entity>
    CommandPort-->>PostHandler: Domain entity
    PostHandler-->>PostLikeService: CountUpdatable
    rect rgb(220,255,220)
      PostLikeService->>PostLikeService: 사전 like count 업데이트 및 persist
    end
    PostLikeService->>PostLikeService: 좋아요/취소 로직 실행
    rect rgb(255,235,235)
      PostLikeService->>PostLikeService: (if like) sendNotifications()
    end
    PostLikeService-->>Client: 응답(성공/오류)
    rect rgb(220,235,255)
      note over DB: 트랜잭션 종료 시 락 해제
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20–30 minutes

  • 주의 포인트:
    • Feed/Record/Vote에 적용된 PESSIMISTIC_WRITE 락의 일관성(임포트·어노테이션·쿼리 대상) 확인
    • PostLikeService 변경으로 인한 트랜잭션 경계 및 예외/복구 흐름(사전 카운트 업데이트 후 실패 시 롤백 보장) 확인
    • 동시성 테스트(스레드/락 경합) 및 k6 스크립트(토큰 생성 경로, VU 인덱스 매핑) 검증

Possibly related PRs

Suggested reviewers

  • seongjunnoh
  • buzz0331

Poem

🐰 동시성의 물결 속에서 토끼가 뛰네,
락을 잡고 조심스레 좋아요를 누르네,
k6가 웁니다 — 부하도 함께 춤추고,
데드락은 사라져 피드는 평온하네,
토끼는 다시 안심하며 당근을 먹네 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.04% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 데드락 해결을 위한 비관적 락 도입이라는 주요 변경사항을 명확하게 요약하고 있습니다.
Linked Issues check ✅ Passed PR의 변경사항이 연결된 이슈들의 목표를 충족합니다: #333의 데드락 해결을 위한 비관적 락 구현 [#333]과 #322의 k6 부하 테스트 스크립트 추가 [#322].
Out of Scope Changes check ✅ Passed 모든 변경사항이 연결된 이슈들의 범위 내에 있습니다: 비관적 락 관련 Java 코드, 테스트 개선, k6 부하 테스트 스크립트가 모두 정해진 목표와 관련이 있습니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch test/#322-k6-feed-like-pessimistic-lock

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3b9abb9 and 7677566.

📒 Files selected for processing (2)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeLikeStatusApiTest.java (1 hunks)
  • src/test/java/konkuk/thip/room/adapter/in/web/RoomPostChangeLikeStatusApiTest.java (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/test/java/konkuk/thip/room/adapter/in/web/RoomPostChangeLikeStatusApiTest.java
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeLikeStatusApiTest.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

github-actions bot commented Nov 12, 2025

Test Results

488 tests   488 ✅  44s ⏱️
145 suites    0 💤
145 files      0 ❌

Results for commit 7677566.

♻️ This comment has been updated with latest results.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (1)
src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/record/RecordJpaRepository.java (1)

21-23: 비관적 락 타임아웃 설정 추가 권장

PESSIMISTIC_WRITE 락이 올바르게 적용되었습니다. 그러나 락 대기 시 무한 대기를 방지하기 위해 @QueryHints로 타임아웃을 설정하는 것이 좋습니다.

다음 diff를 적용하여 타임아웃을 추가하세요:

 @Lock(LockModeType.PESSIMISTIC_WRITE)
+@QueryHints(@QueryHint(name = "jakarta.persistence.lock.timeout", value = "3000"))
 @Query("SELECT r FROM RecordJpaEntity r WHERE r.postId = :postId")
 Optional<RecordJpaEntity> findByPostIdForUpdate(@Param("postId") Long postId);

import 추가:

import jakarta.persistence.QueryHint;
import org.springframework.data.jpa.repository.QueryHints;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 37247d1 and 3b9abb9.

📒 Files selected for processing (17)
  • loadtest/feed/feed_like_concurrency_test1.js (1 hunks)
  • loadtest/feed/feed_like_concurrency_test2.js (1 hunks)
  • loadtest/feed/feed_like_concurrency_test3.js (1 hunks)
  • src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java (1 hunks)
  • src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedJpaRepository.java (2 hunks)
  • src/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.java (1 hunks)
  • src/main/java/konkuk/thip/post/application/service/PostLikeService.java (1 hunks)
  • src/main/java/konkuk/thip/post/application/service/handler/PostHandler.java (1 hunks)
  • src/main/java/konkuk/thip/roompost/adapter/out/persistence/RecordCommandPersistenceAdapter.java (1 hunks)
  • src/main/java/konkuk/thip/roompost/adapter/out/persistence/VoteCommandPersistenceAdapter.java (1 hunks)
  • src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/record/RecordJpaRepository.java (2 hunks)
  • src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteJpaRepository.java (2 hunks)
  • src/main/java/konkuk/thip/roompost/application/port/out/RecordCommandPort.java (1 hunks)
  • src/main/java/konkuk/thip/roompost/application/port/out/VoteCommandPort.java (1 hunks)
  • src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeLikeStatusApiTest.java (1 hunks)
  • src/test/java/konkuk/thip/feed/concurrency/FeedChangeLikeStatusConcurrencyTest.java (1 hunks)
  • src/test/java/konkuk/thip/room/adapter/in/web/RoomPostChangeLikeStatusApiTest.java (2 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-07-26T06:09:00.850Z
Learnt from: seongjunnoh
Repo: THIP-TextHip/THIP-Server PR: 101
File: src/main/java/konkuk/thip/comment/application/service/CommentCreateService.java:36-39
Timestamp: 2025-07-26T06:09:00.850Z
Learning: THIP 프로젝트에서 Record와 Vote는 Room에 속하지만 Feed는 Room에 속하지 않는 구조이며, 댓글 작성 시 Record/Vote에 대해서만 사용자가 해당 Room의 참가자인지 검증이 필요하다.

Applied to files:

  • src/test/java/konkuk/thip/room/adapter/in/web/RoomPostChangeLikeStatusApiTest.java
📚 Learning: 2025-07-03T03:05:05.031Z
Learnt from: seongjunnoh
Repo: THIP-TextHip/THIP-Server PR: 43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.

Applied to files:

  • src/main/java/konkuk/thip/roompost/application/port/out/RecordCommandPort.java
  • src/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.java
  • src/main/java/konkuk/thip/roompost/application/port/out/VoteCommandPort.java
📚 Learning: 2025-09-01T13:18:13.652Z
Learnt from: seongjunnoh
Repo: THIP-TextHip/THIP-Server PR: 287
File: src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserJpaRepository.java:8-14
Timestamp: 2025-09-01T13:18:13.652Z
Learning: seongjunnoh는 JpaRepository의 findById 메서드 재정의보다는 도메인별 명시적 메서드(findByUserId, findByRoomId 등)를 정의하여 Hibernate Filter 적용을 보장하는 방식을 선호하며, 이를 통해 더 안전하고 의도가 명확한 코드 구조를 구축한다.

Applied to files:

  • src/main/java/konkuk/thip/roompost/adapter/out/persistence/RecordCommandPersistenceAdapter.java
📚 Learning: 2025-07-14T14:19:38.796Z
Learnt from: buzz0331
Repo: THIP-TextHip/THIP-Server PR: 75
File: src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteQueryRepositoryImpl.java:50-83
Timestamp: 2025-07-14T14:19:38.796Z
Learning: Vote와 VoteItem 엔티티는 자주 함께 사용되므로, N+1 문제를 방지하기 위해 양방향 매핑과 fetch join을 고려하는 것이 좋습니다. 특히 기록장 조회 API 등에서도 함께 사용될 가능성이 높습니다.

Applied to files:

  • src/main/java/konkuk/thip/roompost/adapter/out/persistence/VoteCommandPersistenceAdapter.java
  • src/main/java/konkuk/thip/roompost/application/port/out/VoteCommandPort.java
🧬 Code graph analysis (4)
loadtest/feed/feed_like_concurrency_test2.js (2)
loadtest/feed/feed_like_concurrency_test1.js (8)
  • BASE_URL (5-5)
  • setup (21-34)
  • tokens (22-22)
  • likeStatus (23-23)
  • userId (26-26)
  • res (27-27)
  • res (56-56)
  • token (38-38)
loadtest/feed/feed_like_concurrency_test3.js (10)
  • BASE_URL (5-5)
  • setup (30-43)
  • maxVUs (32-32)
  • tokens (33-33)
  • userId (36-36)
  • res (37-37)
  • res (56-56)
  • res (95-95)
  • token (48-48)
  • token (73-73)
loadtest/feed/feed_like_concurrency_test1.js (2)
loadtest/feed/feed_like_concurrency_test2.js (10)
  • BASE_URL (5-5)
  • options (8-20)
  • options (8-20)
  • setup (23-38)
  • tokens (26-26)
  • likeStatus (27-27)
  • userId (30-30)
  • res (31-31)
  • res (60-60)
  • token (42-42)
loadtest/feed/feed_like_concurrency_test3.js (11)
  • BASE_URL (5-5)
  • options (8-27)
  • options (8-27)
  • setup (30-43)
  • tokens (33-33)
  • userId (36-36)
  • res (37-37)
  • res (56-56)
  • res (95-95)
  • token (48-48)
  • token (73-73)
loadtest/feed/feed_like_concurrency_test3.js (2)
loadtest/feed/feed_like_concurrency_test1.js (10)
  • BASE_URL (5-5)
  • options (9-18)
  • options (9-18)
  • setup (21-34)
  • tokens (22-22)
  • userId (26-26)
  • res (27-27)
  • res (56-56)
  • token (38-38)
  • handleSummary (75-79)
loadtest/feed/feed_like_concurrency_test2.js (11)
  • BASE_URL (5-5)
  • options (8-20)
  • options (8-20)
  • setup (23-38)
  • maxVUs (25-25)
  • tokens (26-26)
  • userId (30-30)
  • res (31-31)
  • res (60-60)
  • token (42-42)
  • handleSummary (79-83)
src/test/java/konkuk/thip/feed/concurrency/FeedChangeLikeStatusConcurrencyTest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
  • TestEntityFactory (35-417)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (10)
src/main/java/konkuk/thip/post/application/service/handler/PostHandler.java (1)

34-40: LGTM! 일관된 패턴으로 update용 조회 메서드 추가

기존 findPost 메서드와 동일한 구조로 update-lock 경로를 제공하여 코드 일관성이 유지됩니다.

src/main/java/konkuk/thip/roompost/adapter/out/persistence/RecordCommandPersistenceAdapter.java (1)

59-63: LGTM! Adapter 레이어의 update-lock 경로 구현

Repository의 findByPostIdForUpdate를 호출하고 도메인 엔티티로 매핑하는 일관된 패턴입니다.

src/main/java/konkuk/thip/roompost/application/port/out/VoteCommandPort.java (1)

23-31: LGTM! Port 인터페이스의 update-lock 경로 추가

기존 findById/getByIdOrThrow 패턴과 일관되게 update용 메서드를 제공합니다. default 메서드를 통해 예외 처리를 캡슐화한 것도 좋습니다.

src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java (1)

47-51: LGTM! Feed Adapter의 update-lock 경로 구현

다른 도메인(Record, Vote)과 일관된 패턴으로 구현되어 있습니다.

src/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.java (1)

15-23: LGTM! Feed Port의 update-lock 경로 추가

다른 CommandPort들(VoteCommandPort, RecordCommandPort)과 동일한 패턴으로 일관성이 유지됩니다.

src/main/java/konkuk/thip/roompost/adapter/out/persistence/VoteCommandPersistenceAdapter.java (1)

84-88: LGTM! Vote Adapter의 update-lock 경로 구현

다른 Adapter들과 일관된 패턴으로 구현되어 있습니다.

src/main/java/konkuk/thip/roompost/application/port/out/RecordCommandPort.java (1)

18-26: LGTM! Record Port의 update-lock 경로 추가

다른 CommandPort들과 동일한 패턴으로 일관성이 잘 유지됩니다.

src/test/java/konkuk/thip/feed/concurrency/FeedChangeLikeStatusConcurrencyTest.java (1)

32-58: 테스트 설정이 적절합니다.

동시성 테스트를 위한 기본 설정이 올바르게 구성되어 있습니다.

src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteJpaRepository.java (1)

21-23: 비관적 락 구현이 올바릅니다.

PESSIMISTIC_WRITE 락을 사용한 findByPostIdForUpdate 메서드가 올바르게 구현되었습니다. 데드락 해결을 위한 적절한 접근 방식입니다.

src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedJpaRepository.java (1)

21-23: 비관적 락 구현이 올바릅니다.

PESSIMISTIC_WRITE 락을 사용한 findByPostIdForUpdate 메서드가 올바르게 구현되었습니다. Feed 엔티티에 대한 동시성 제어가 적절하게 적용되었습니다.

Comment on lines +61 to +109
@Test
public void concurrentLikeToggleTest() throws InterruptedException {

int threadCount = 2;
int repeat = 10; // 스레드별 몇 번 반복할지
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
CountDownLatch latch = new CountDownLatch(threadCount * repeat);

AtomicInteger successCount = new AtomicInteger();
AtomicInteger failCount = new AtomicInteger();

// 각 스레드별로 현재 상태(true/false)를 관리하기 위한 배열
boolean[] likeStatus = new boolean[threadCount];

for (int i = 0; i < threadCount; i++) {
final int userIndex = i;
executor.submit(() -> {
likeStatus[userIndex] = true;
for (int r = 0; r < repeat; r++) {
boolean isLike = likeStatus[userIndex];
try {
// 각 스레드별로 서로 다른 user를 사용하도록 user1, user2 분기 처리
Long userId = (userIndex == 0) ? user1.getUserId() : user2.getUserId();

postLikeService.changeLikeStatusPost(
new PostIsLikeCommand(userId, feed.getPostId(), PostType.FEED, isLike)
);
successCount.getAndIncrement();
// 성공했을 때만 현재 상태를 반전
likeStatus[userIndex] = !likeStatus[userIndex];
} catch (Exception e) {
log.error(e.getMessage(), e);
failCount.getAndIncrement();
} finally {
latch.countDown();
}
}
});
}

latch.await();
executor.shutdown();

// then
assertAll(
() -> assertThat(successCount.get()).isEqualTo(threadCount * repeat),
() -> assertThat(failCount.get()).isEqualTo(0)
);
}
Copy link

@coderabbitai coderabbitai bot Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

최종 데이터베이스 상태 검증이 누락되었습니다.

현재 테스트는 작업 성공 횟수만 확인하지만, 실제 데이터베이스의 최종 상태(좋아요 개수, PostLike 엔티티 존재 여부)를 검증하지 않습니다. 각 사용자가 10번씩 토글하므로 최종적으로 두 사용자 모두 좋아요 상태여야 하며, feed의 likeCount는 2여야 합니다.

테스트 마지막에 다음 검증을 추가하는 것을 권장합니다:

 latch.await();
 executor.shutdown();
+
+// 최종 데이터베이스 상태 검증
+FeedJpaEntity finalFeed = feedJpaRepository.findById(feed.getPostId()).orElseThrow();
+long finalLikeCount = postLikeQueryPort.countByPostId(feed.getPostId());

 // then
 assertAll(
         () -> assertThat(successCount.get()).isEqualTo(threadCount * repeat),
-        () -> assertThat(failCount.get()).isEqualTo(0)
+        () -> assertThat(failCount.get()).isEqualTo(0),
+        () -> assertThat(finalFeed.getLikeCount()).isEqualTo(2),
+        () -> assertThat(finalLikeCount).isEqualTo(2)
 );
🤖 Prompt for AI Agents
In
src/test/java/konkuk/thip/feed/concurrency/FeedChangeLikeStatusConcurrencyTest.java
around lines 61 to 109, the test only asserts success/fail counters but misses
verifying the final DB state; update the end of the test to reload the feed and
post-like data from the repository (or service) after
latch.await()/executor.shutdown(), then assert that each user has a PostLike
record with isLike==true and that feed.getLikeCount() (reloaded) equals 2 (or
the expected value), ensuring you use repository/service methods to fetch fresh
entities (and wrap reload/asserts in the same transactional/test context if
needed).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코리가 말하는 것처럼 최종적으로 마지막에 DB 정합성이 깨지는지 확인하는 것도 좋을 것 같네욥

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@buzz0331, 좋은 지적입니다! 동시성 테스트에서 최종 DB 정합성 검증을 추가하는 것이 중요하네요.

다만 테스트 로직을 분석해보니, 각 사용자가 true로 시작해서 10번 토글하므로:

  • 1번째: 좋아요 추가
  • 2번째: 좋아요 취소
  • 3번째: 좋아요 추가
  • ...
  • 10번째: 좋아요 취소

최종적으로 두 사용자 모두 좋아요 취소 상태가 되어, likeCount는 0이 되어야 합니다.

다음과 같이 최종 DB 상태 검증을 추가하는 것을 권장합니다:

latch.await();
executor.shutdown();

// 최종 데이터베이스 상태 검증
FeedJpaEntity finalFeed = feedJpaRepository.findById(feed.getPostId()).orElseThrow();

// then
assertAll(
        () -> assertThat(successCount.get()).isEqualTo(threadCount * repeat),
        () -> assertThat(failCount.get()).isEqualTo(0),
        () -> assertThat(finalFeed.getLikeCount()).isEqualTo(0),
        () -> assertThat(finalFeed.getLikeCount()).as("DB의 likeCount와 실제 상태가 일치해야 함").isEqualTo(0)
);

더 완벽한 검증을 위해서는 PostLike 엔티티의 실제 개수도 확인하면 좋습니다. 만약 PostLikeCrudRepository 같은 레포지토리가 있다면:

@Autowired private PostLikeCrudRepository postLikeCrudRepository;

// 테스트 마지막에
long actualPostLikeCount = postLikeCrudRepository.countByPostIdAndIsLike(feed.getPostId(), true);
assertThat(actualPostLikeCount).isEqualTo(0);

이렇게 하면 동시성 제어가 제대로 작동해서 데이터 정합성이 유지되는지 확실하게 검증할 수 있습니다! 👍


🧠 Learnings used
Learnt from: buzz0331
Repo: THIP-TextHip/THIP-Server PR: 309
File: src/main/java/konkuk/thip/notification/application/service/RoomNotificationOrchestratorSyncImpl.java:36-44
Timestamp: 2025-09-23T08:31:05.161Z
Learning: buzz0331은 기술적 이슈에 대해 실용적인 해결책을 제시하면서도 과도한 엔지니어링을 피하는 균형감을 선호한다. 복잡도 대비 실제 발생 가능성을 고려하여 "굳이" 불필요한 솔루션보다는 심플함을 유지하는 것을 중요하게 생각한다.

Copy link
Contributor

@buzz0331 buzz0331 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수고하셨습니다~ 문서가 아주 깔끔하네여 👍🏻 👍🏻
희진님도 찾아보셨겠지만 업데이트 충돌이 잦은 환경에서는 낙관락보다는 비즈니스 로직 성공을 반드시 보장해주는 비관락을 권장하는 것으로 알고있어욥
다만, 현재 희진님이 작성해두신 노션을 확인해보니 비관락을 걸었을때 대략 300 RPS까지 오르고 요청 실패가 한번도 없었던 것으로 확인되는데 비관락이 기본적으로 명시적으로 락을 걸면서 리소스 접근 자체를 막는거다보니 더 높은 RPS에서 과연 request time out이 나지 않고 버틸 수 있을지 궁금해지네요.
RPS를 조금 비현실적이더라도 비약적으로 높여보면서 request time out이 나는 지점을 확인하고 그 다음 단계인 Redis 도입, 이벤트 처리를 적용시켜보는 것도 좋을 것 같다는 생각입니다!

Comment on lines +61 to +109
@Test
public void concurrentLikeToggleTest() throws InterruptedException {

int threadCount = 2;
int repeat = 10; // 스레드별 몇 번 반복할지
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
CountDownLatch latch = new CountDownLatch(threadCount * repeat);

AtomicInteger successCount = new AtomicInteger();
AtomicInteger failCount = new AtomicInteger();

// 각 스레드별로 현재 상태(true/false)를 관리하기 위한 배열
boolean[] likeStatus = new boolean[threadCount];

for (int i = 0; i < threadCount; i++) {
final int userIndex = i;
executor.submit(() -> {
likeStatus[userIndex] = true;
for (int r = 0; r < repeat; r++) {
boolean isLike = likeStatus[userIndex];
try {
// 각 스레드별로 서로 다른 user를 사용하도록 user1, user2 분기 처리
Long userId = (userIndex == 0) ? user1.getUserId() : user2.getUserId();

postLikeService.changeLikeStatusPost(
new PostIsLikeCommand(userId, feed.getPostId(), PostType.FEED, isLike)
);
successCount.getAndIncrement();
// 성공했을 때만 현재 상태를 반전
likeStatus[userIndex] = !likeStatus[userIndex];
} catch (Exception e) {
log.error(e.getMessage(), e);
failCount.getAndIncrement();
} finally {
latch.countDown();
}
}
});
}

latch.await();
executor.shutdown();

// then
assertAll(
() -> assertThat(successCount.get()).isEqualTo(threadCount * repeat),
() -> assertThat(failCount.get()).isEqualTo(0)
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코리가 말하는 것처럼 최종적으로 마지막에 DB 정합성이 깨지는지 확인하는 것도 좋을 것 같네욥

Copy link
Collaborator

@seongjunnoh seongjunnoh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확인했습니다! 그런데 비관적 락에 타임아웃 관련해서 명시적으로 설정 하지 않으셨던데, 비즈니스 로직 상 게시글 좋아요에 그렇게 많은 부하가 발생할라나? 라는 의도로 락 타임아웃 설정은 제외하신 걸까요??
mysql은 타임아웃의 디폴트 값이 50초라고 알고 있는데, jpa 환경에서도 이게 유지되는지는 확인해봐야 할 것 같습니다!

개인적으로 락을 걸 때 혹시 모를 장애상황을 대비해서 타임아웃 + 이후 처리(디스코드 알림 or 보상 재시도 등등) 를 고려하는게 좋지 않나 생각합니다!

@hd0rable
Copy link
Member Author

확인했습니다! 그런데 비관적 락에 타임아웃 관련해서 명시적으로 설정 하지 않으셨던데, 비즈니스 로직 상 게시글 좋아요에 그렇게 많은 부하가 발생할라나? 라는 의도로 락 타임아웃 설정은 제외하신 걸까요?? mysql은 타임아웃의 디폴트 값이 50초라고 알고 있는데, jpa 환경에서도 이게 유지되는지는 확인해봐야 할 것 같습니다!

개인적으로 락을 걸 때 혹시 모를 장애상황을 대비해서 타임아웃 + 이후 처리(디스코드 알림 or 보상 재시도 등등) 를 고려하는게 좋지 않나 생각합니다!

제가 진행해본 부하테스트(최대 vu200명이 동시에 좋아요 요청/취소를 반복하는경우)로는 락 타임아웃의 요청이 1건도 발생하지않아서 고려하지않았습니다!! 지금은 성준님이 작성한 부하 테스트 스크립트처럼 어떤 특정상황에 좋아요요청이 동시에 몰릴경우 부하 테스트를 진행하려고하는데 이테스트로 리퀘스트 타임아웃이 나는 지점을 확인해보고 그 후 처리도 고려해보겠습니닷!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[THIP2025-385] [fix] 게시글 좋아요 500 error(deadlock) 해결 [THIP2025-366] [test] k6 부하 테스트 스크립트 추가 및 관리

4 participants