프로젝트 ‘THIP (TextHip)’을 운영하며 발견했던 이슈를 정리한 문서입니다.
현재 팔로우 상태 변경 로직
@Override
@Transactional
public Boolean changeFollowingState(UserFollowCommand followCommand) {
Long userId = followCommand.userId();
Long targetUserId = followCommand.targetUserId();
Boolean type = followCommand.type();
validateParams(userId, targetUserId);
Optional<Following> optionalFollowing = followingCommandPort.findByUserIdAndTargetUserId(userId, targetUserId);
User targetUser = userCommandPort.findById(targetUserId);
boolean isFollowRequest = Following.validateFollowingState(optionalFollowing.isPresent(), type);
if (isFollowRequest) { // 팔로우 요청인 경우
targetUser.increaseFollowerCount();
followingCommandPort.save(Following.withoutId(userId, targetUserId), targetUser);
// 팔로우 푸쉬알림 전송
sendNotifications(userId, targetUserId);
return true;
} else { // 언팔로우 요청인 경우
targetUser.decreaseFollowerCount();
followingCommandPort.deleteFollowing(optionalFollowing.get(), targetUser);
return false;
}
}
팔로잉 요청 흐름
FollowingCommandPort.findByUserIdAndTargetUserId()UserCommandPort.findById()followingCommandPort.save()
운영서버의 모니터링 채널로 전송된 500에러 메시지

세부 로그 확인

[2025-10-22 16:22:19:689881235] [http-nio-8000-exec-8] ERROR k.t.c.e.h.GlobalExceptionHandler - [ServerErrorHandler] could not execute statement [**Deadlock** found when trying to get lock; try restarting transaction] [update users set alias=?,follower_count=?,modified_at=?,nickname=?,nickname_updated_at=?,oauth2_id=?,role=?,status=? where user_id=?]; SQL [update users set alias=?,follower_count=?,modified_at=?,nickname=?,nickname_updated_at=?,oauth2_id=?,role=?,status=? where user_id=?]
org.springframework.dao.**CannotAcquireLockException**: could not execute statement [**Deadlock** found when trying to get lock; try restarting transaction] [update users set alias=?,follower_count=?,modified_at=?,nickname=?,nickname_updated_at=?,oauth2_id=?,role=?,status=? where user_id=?]; SQL [update users set alias=?,follower_count=?,modified_at=?,nickname=?,nickname_updated_at=?,oauth2_id=?,role=?,status=? where user_id=?]
Caused by: org.hibernate.exception.**LockAcquisitionException**: could not execute statement [**Deadlock** found when trying to get lock; try restarting transaction] [update users set alias=?,follower_count=?,modified_at=?,nickname=?,nickname_updated_at=?,oauth2_id=?,role=?,status=? where user_id=?]
at org.hibernate.dialect.MySQLDialect.lambda$buildSQLExceptionConversionDelegate$3(MySQLDialect.java:1260)
Caused by: com.mysql.cj.jdbc.exceptions.**MySQLTransactionRollbackException**: **Deadlock** found when trying to get lock; try restarting transaction
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:115)
at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:114)
확인해보니, **deadlock**이라는 키워드가 반복되는 것을 확인할 수 있었습니다.
원인 파악을 위해 특정 시나리오를 가정한 테스트 코드를 짜서 데이터 정합성 확인해봤습니다.
<aside> 💡
시나리오
인기 작가가 작품 홍보차 가입한 상황에서 한번에 사용자들의 팔로잉 요청이 몰리는 경우를 가정
테스트 내용: POST /users/following/{followingUserId} 엔드포인트로 500명의 유저가 한 유저에게 동시에 요청
테스트 확인
followerCount) 컬럼 역시 팔로우 요청 횟수보다 적을 수 있다.테스트 코드
테스트 결과
=== RESULT ===
OK responses : 500
followings rows : 500
target user's follower_count : 54