[JPA] 중복 유저 등록 이슈

신명철·2022년 5월 19일
0

errors

목록 보기
2/3

들어가며

프로젝트를 진행 중, 중복 등록 방지 로직을 넣었음에도 불구하고 중복된 유저가 등록되는 문제가 발생했다.

해당 문제는 안드로이드에서 유저 등록 API 를 호출할 때 아주 짧은 간격으로 연속적으로 호출할 때 발생했다.

UserService

  • DB에 해당 이메일을 사용중인 회원이 이미 존재하면 IllegalArgumentException를 던진다.

UserControllerTest

  • 2개의 스레드가 동시에 요청을 보낸다.

테스트 결과

  • 같은 시간에 UserService.register()이 호출됐다.

  • userRepository.findByEmail() 은 내부적으로 EntityManager.find()를 호출한다. 1차 캐시에 엔티티가 존재하면 그걸 가져오고, 없으면 SELECT 문을 DB에 날린다.
  • 1차 캐시에 엔티티가 아직 persist 되지 않았기 때문에 둘 다 SELECT 문을 날린 것으로 파악된다.
  • 같은 시간에 SELECT 문이 호출된걸 보면, 아마 2개의 스레드가 동시에 if(userRepository.findByEmail(email).isPresent()) 를 지나친 것으로 보인다.

  • 두 번의 INSERT 쿼리가 0.003 초 차이로 수행됐다.

문제 해석

email 을 고유 값으로 저장해야 하는데 위와 같이 비정상적인 요청이 있는 상황에서는 email 검증 로직이 정상적으로 처리될 수 없다.

엔티티가 EntityManager에 의해서 영속 상태가 되기도 전에 있는지 확인을 한다면 한 개 이상의 요청이 문제없이 통과되기 때문이다.

문제 해결

  • Service 단에서 어떻게든 해보고 싶었지만 타협을 했다.
  • email을 그냥 unique로 관리되도록 설정하고, 정합성 문제로 발생하는 Exception을 @ExceptionHandler를 통해서 잡도록 했다.

해결 시도 방법

두 개의 요청이 동시에 접근하기 때문에 isolation 레벨을 높이면 되지 않을까 생각했었다.

Deadlock found when trying to get lock; try restarting transaction

하지만 Deadlock이 발생했다.

두 개의 요청이 동시에 write 작업을 할 때 두 개의 요청은 서로 Shared Lock을 놓아줄 때 까지 기다리게 되면서 Deadlock Exception이 발생한 것이다.

InnoDB의 Locking에 대한 설명

The system of protecting a transaction from seeing or changing data that is being queried or changed by other transactions. The locking strategy must balance reliability and consistency of database operations (the principles of the ACID philosophy) against the performance needed for good concurrency. Fine-tuning the locking strategy often involves choosing an isolation level and ensuring all your database operations are safe and reliable for that isolation level.

Lock 전략은 동시성과 ACID 간의 균형이 잘 맞아야 한다. Lock 전략을 올바르게 튜닝하는 것은 모든 데이터베이스 동작이 안전하고 신뢰가 있어야 한다. 즉, 동시성을 어느정도는 포기해야 한다.


<참고>

profile
내 머릿속 지우개

0개의 댓글