@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)
);
}

2개의 스레드(유저)가 한피드에 대해 동시에 좋아요/취소 요청을 반복하도록 테스트 구성 —> 각 스레드는 좋아요 상태를 번갈아가며 총 10회 반복, 전체 20회의 요청 수행
기대한 결과는 20번의 요청 모두 성공하는 것이었지만, 실제 결과는 11번 성공, 9번 실패(예외 발생)

좋아요 상태변경 요청에 실패했을때 로그에는 데드락 에러가 찍히는것을 확인했다.
위와 같은 시나리오로 k6 부하 테스트를 진행해보았을때 동시성이 별로 높지 않은 상황인데도 무려 실패율이 0.44로 거의 반절꼴로 실패하고, 실제 DB에서도 정합성이 맞지 않는 심각한 상황이었다.

좀 더 자세히 트랜잭션 교착 상태를 확인하기위해 MySQL에서 다음 명령어로 트랜잭션 교착 상태 감지 내역을 확인해봤다.
mysql>SHOW ENGINE INNODB STATUS;
------------------------
LATEST DETECTED DEADLOCK
------------------------
2025-10-22 16:58:21 0x16bad7000
*** (1) TRANSACTION:
TRANSACTION 248488, ACTIVE 0 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 7 lock struct(s), heap size 1128, 3 row lock(s), undo log entries 1
MySQL thread id 646, OS thread handle 6119993344, query id 1987517 localhost 127.0.0.1 huijin updating
update posts set comment_count=0,content='기본 피드 본문입니다.',like_count=1,modified_at='2025-10-22 16:58:21.118337',status='ACTIVE',user_id=2,book_id=2,content_list='[]',is_public=1,report_count=0,tag_list='[]' where post_id=2
*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 1621 page no 4 n bits 72 index PRIMARY of table `thip_local_rds`.`posts` trx id 248488 lock mode S locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 19; compact format; info bits 0
... 중략
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 1621 page no 4 n bits 72 index PRIMARY of table `thip_local_rds`.`posts` trx id 248488 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 19; compact format; info bits 0
... 중략
*** (2) TRANSACTION:
TRANSACTION 248489, ACTIVE 0 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 7 lock struct(s), heap size 1128, 3 row lock(s), undo log entries 1
MySQL thread id 647, OS thread handle 6118879232, query id 1987520 localhost 127.0.0.1 huijin updating
update posts set comment_count=0,content='기본 피드 본문입니다.',like_count=1,modified_at='2025-10-22 16:58:21.122762',status='ACTIVE',user_id=2,book_id=2,content_list='[]',is_public=1,report_count=0,tag_list='[]' where post_id=2
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 1621 page no 4 n bits 72 index PRIMARY of table `thip_local_rds`.`posts` trx id 248489 lock mode S locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 19; compact format; info bits 0
... 중략
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 1621 page no 4 n bits 72 index PRIMARY of table `thip_local_rds`.`posts` trx id 248489 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 19; compact format; info bits 0
... 중략
*** WE ROLL BACK TRANSACTION (2)
S-lock 을 보유 중인 레코드를 **X-lock**모드로의 승격을 기다리고 있음UPDATE posts SET ... WHERE post_id=2S-lock 을 을보유 중인 레코드를 다른 트랜잭션이 사용하고 있음(트랜잭션 1과 교차 락 대기 상황)UPDATE posts SET ... WHERE post_id=2posts**테이블 post_id=2 레코드에 대해 S-lock을 건다.
WE ROLL BACK TRANSACTION (2) 로 트랜잭션 (2)를 롤백