회원 도메인 개발 중에, OAuth를 통한 회원가입 기능을 구현해야했다.
개발중에 가장 신경썻던 부분은 동시성으로 인한 중복 회원가입 문제를 예방하는것이였다.
각 OAuth 계정당 하나의 계정
OAuth에선 인증 토큰을 통해 사용자 정보 조회 시, OAuth서버에서 사용자를 식별하는 고유 ID를 제공한다.
백엔드 서버에선 이 고유 ID와 Provider가 Unique Key로 존재하도록 하여 위 문제를 예방하도록 한다.
위 요구사항을 만족시키기 위해선, 고유 ID Provider 페어가 동시 삽입되는 일이 없도록 해야한다.
이를 위해서 생각할 수 있는 방법은 여러가지가 있지만, 내가 고려했던 방안은 크게 두가지 정도이다.
1) DB 의존
디비의 복합 유니크 설정을 통해 중복삽입을 방지한다.
2) Pessimistic Lock
해당 페어를 테이블의 복합 인덱스로 설정하고, Pessimistic Lock으로 중복 삽입을 방지하는 방법
3) Distribted Lock 네임드 락
ID와 OAuthProvider 이름으로 항상 네임드락을 시도한다.결론적으로 2) Pessimistic Lock을 통해 중복 삽입 방지를 구현하기로 결정했다.
SQLIntegrityConstraintViolationException가 발생하게 된다. 어플리케이션 레벨에서 이 예외를 Unique 키 예외라 확신하는것은 디버깅 및 유지보수에 혼란을 줄 수 있기에 위험부담이 있다 판단, 제외했다. Distriuted Lock을 통해 방지하는것이 가장 고급스럽다 생각한다.핵심은, 존재 하지 않는 행에 대한 잠금을 구현하는 것이다.
Pessimistic Lock을 적용해야 한다.oauth_provider, oauth_provider_id 에 복합 유니크 인덱스를 적용한다.... 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으로 건다.
이에 대한 삽입을 모두가 수행하며, 상호간 갭락 해제를 기다리며 무한 대기에 빠짐으로, 각 트랜잭션은 데드락에 빠지게 된다.
데드락이 발생하긴 하나, 항상 하나의 트랜잭션은 성공하는것을 확인할 수 있었다.
해당 기능은 많은 동시성이 발생하지 않을것이라 예측됨으로, 해당 데드락이 발생할 수 있음을 인지하는 정도로 구현을 마무리한다.
해당 기능을 구현하며 MySQL의 X-Lock이 어떻게 실질적으로 락을 거는지 더 깊이 이해할 수 있었다.
MySQL에서 락을 걸떄 INDEX의 중요성을 다시 확인할 수 있었다. ㅎㅎ