MySQL - 존재하지 않는 행을 잠근다

윤희종·2025년 7월 29일

OAuth 회원가입 및 로그인 구현 결정

회원 도메인 개발 중에, OAuth를 통한 회원가입 기능을 구현해야했다.

개발중에 가장 신경썻던 부분은 동시성으로 인한 중복 회원가입 문제를 예방하는것이였다.

각 OAuth 계정당 하나의 계정

OAuth에선 인증 토큰을 통해 사용자 정보 조회 시, OAuth서버에서 사용자를 식별하는 고유 ID를 제공한다.

백엔드 서버에선 이 고유 IDProviderUnique Key로 존재하도록 하여 위 문제를 예방하도록 한다.

동시성 문제 해결 고민

위 요구사항을 만족시키기 위해선, 고유 ID Provider 페어가 동시 삽입되는 일이 없도록 해야한다.

이를 위해서 생각할 수 있는 방법은 여러가지가 있지만, 내가 고려했던 방안은 크게 두가지 정도이다.

  • 1) DB 의존
    디비의 복합 유니크 설정을 통해 중복삽입을 방지한다.

  • 2) Pessimistic Lock
    해당 페어를 테이블의 복합 인덱스로 설정하고, Pessimistic Lock으로 중복 삽입을 방지하는 방법

  • 3) Distribted Lock 네임드 락

    • OAuthProvider로부터 받은 IDOAuthProvider 이름으로 항상 네임드락을 시도한다.
    • 잠금 획득 성공
      • 해당 계정의 서버 저장유무를 확인한다.
      • 계정이 서버에 존재한다면 로그인 기능을 수행하고 락을 해제한다.
      • 존재하지 않는다면, 새로운 계정을 생성 후, 락을 해제한다.
    • 잠금 획득 실패
      • 다른 세션에서 해당 계정으로 로그인 or 회원가입중이므로 에러를 응답한다.

결론적으로 2) Pessimistic Lock을 통해 중복 삽입 방지를 구현하기로 결정했다.

  • DB 의존적으로 구현
    할 시, 동시삽입에 대해 SQLIntegrityConstraintViolationException가 발생하게 된다. 어플리케이션 레벨에서 이 예외를 Unique 키 예외라 확신하는것은 디버깅 및 유지보수에 혼란을 줄 수 있기에 위험부담이 있다 판단, 제외했다.
  • Distriuted Lock을 통해 방지하는것이 가장 고급스럽다 생각한다.
    하지만, 위 기능은 OAuthProvider에 의해 제공되는 토큰으로 이뤄짐으로( 사용자의 직접적 로그인 필요 ), 동시성이 많이 발생되지 않을것으로 예상된다.
    뿐만 아니라, JPA에선 namedLock을 위한 api를 제공하지 않으며, 추가적인 인프라를 도입하는것은 오버엔지니어링이라 판단했다.

중복 회원 가입 방지 구현

핵심은, 존재 하지 않는 행에 대한 잠금을 구현하는 것이다.

  • 중복 회원가입을 방지하기 위해, Pessimistic Lock을 적용해야 한다.
  • MySQL은 인덱스를 이용해 락을 적용함으로, oauth_provider, oauth_provider_id 에 복합 유니크 인덱스를 적용한다.
  • MySQL에서 ... FOR UPDATE 쿼리 시, 동등조건에서 존재하지 않는 행에 대한 락은 넥스트 키 락을 유도한다. non-clustered index에만 갭락이 적용된다.

갭락은 행과 행사이에 insert만을 방지한다. 기타 S-Lock, X-Lock 과는 무관하다

따라서, 회원가입 시 다음과 같은 쿼리가 날라간다.

SELECT * FROM MEMBER m WHERE m.oauth_provider = ? AND m.oauth_provider_id = ? FOR UPDATE;

INSERT INTO MEMBER m VALUES( ... );

데드락

여러 트랜잭션이 위 쿼리를 수행하면, 어떤 결과가 나올까?
각 트랜잭션은 동일한 인덱스에 넥스트 키 락을 얻는다. 각 트랜잭션에서 모두 결과를 얻지 못했음으로,
이때, 데드락이 발생하게 된다.

MySQL은 트랜잭션 안에서 존재하지 않는 행에 대한 락을 Next Key Lock으로 건다.
이에 대한 삽입을 모두가 수행하며, 상호간 갭락 해제를 기다리며 무한 대기에 빠짐으로, 각 트랜잭션은 데드락에 빠지게 된다.

데드락이 발생하긴 하나, 항상 하나의 트랜잭션은 성공하는것을 확인할 수 있었다.
해당 기능은 많은 동시성이 발생하지 않을것이라 예측됨으로, 해당 데드락이 발생할 수 있음을 인지하는 정도로 구현을 마무리한다.

느낀점

해당 기능을 구현하며 MySQLX-Lock이 어떻게 실질적으로 락을 거는지 더 깊이 이해할 수 있었다.

  • X-Lock이 인덱스를 탈때, 클러스터 인덱스까지 락을 건다는 점.
  • X-Lock시, 커버링 인덱스로 동작하지 못하면, 현재까지 만족하는 인덱스를 모두(클러스터 인덱스까지) 잠근다는 점
  • X-Lock이 인덱스를 타지 못하면, 모든 행을 잠근다는 점

MySQL에서 락을 걸떄 INDEX의 중요성을 다시 확인할 수 있었다. ㅎㅎ

profile
백 엔드 엔지니어를 꿈꾸는 대학생입니다

0개의 댓글