1. 문제 상황

프로젝트 ‘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;
    }
}

팔로잉 요청 흐름

  1. 팔로잉 테이블에서 팔로잉 관계 조회 FollowingCommandPort.findByUserIdAndTargetUserId()
  2. 타겟 유저(팔로잉 당하는 유저) 조회 UserCommandPort.findById()
  3. save 요청 followingCommandPort.save()
    1. 액션 유저(팔로잉 하는 유저) 조회
    2. 팔로잉 관계 테이블에 행 삽입
    3. 타겟 유저 조회 후 업데이트

운영서버의 모니터링 채널로 전송된 500에러 메시지

스크린샷 2025-10-22 오후 5.27.13.png

세부 로그 확인

스크린샷 2025-10-22 오후 5.42.44.png

[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> 💡

시나리오

인기 작가가 작품 홍보차 가입한 상황에서 한번에 사용자들의 팔로잉 요청이 몰리는 경우를 가정