본문 바로가기
Java/Spring

회원가입 동시성 이슈 테스트 - DB Unique 활용하기

by oneny 2023. 10. 30.

회원가입 동시성 이슈 테스트

절대 흔한 일은 아니지만 만약 동시에 두 명의 사용자가 같은 이메일로 회원가입을 하게 되면 어떻게 될까? 이에 대한 궁금증을 해결하기 위해 직접 테스트를 진행해보고자 한다.

 

회원가입 비즈니스 로직

Service 계층에 위치한 signUp 메서드는 말그대로 회원가입을 진행하는 비즈니스 로직을 담고 있다. 순서는 다음과 같다.

  1. validateMemberDto(_): 사용자가 입력한 정보에 대해 유효성을 검사한다.
  2. if (isDuplicatedMember(_)) { ... }: 만약 이미 가입한 이메일이 있으면 EmailDuplicationException 예외가 발생한다.
  3. signUpMemberDto.toEntity(): DTO -> Entity로 변환한다.
  4. setBcryptPassword(_): 사용자가 입력한 비밀번호를 암호화한다.
  5. memberRepository.insertMember(_): 회원가입을 진행한다.

 

이러한 순서를 통해 회원가입이 진행될 때 만약 두 명의 사용자가 같은 이메일로 회원가입하려고 한다면 어떻게 될까? 한 명의 사용자는 회원가입을, 다른 한 명의 사용자는 비즈니스 로직 중 2번에 해당하는 EmailDulicationException 예외가 발생할까?

테스트를 진행하기 전 예상을 해보자면 두 명의 사용자의 요청을 담당하는 두 스레드가 병렬로 진행하여 두 스레드 모두 비즈니스 로직을 통과하고 데이터 접근 계층으로 넘어갈 것이라고 생각했다. 그리고 email 컬럼이 unique이기 때문에 하나의 스레드는 회원가입이 되고, 다른 하나의 스레드는 @Transactional 어노테이션을 통해 스프링이 제공하는 예외 추상화 기능으로 DataAccessException 예외 중 하나인 DuplicateKeyException 예외가 발생할 것이라고 예상했다. 그러면 내가 예상한 것이 맞는지 테스트를 진행해보자.

 

테스트 진행을 위한 ExceptionThread 생성

위 클래스는 스레드 내에서 발생한 예외를 캐치하고 처리하기 위한 클래스이다. 예를 들어, t1 스레드가 생성한 t2 스레드에서 발생한 예외를 t1 스레드에서 처리하기 위해서 생성자를 보면 try-catch 블록으로 둘러싸여 있는 것을 확인할 수 있다. 만약 t2 스레드에서 예외가 발생하면 t1 스레드 catch 블록에서 예외를 인스턴스 변수인 exception에 저장하도록 작성했다. 그리고 t2 스레드에서 예외가 발생했는지 확인하기 위해서 checkException 메서드를 이용하여 확인할 수 있다.

 

회원가입 동시성 테스트

두 스레드가 거의 동시에 진행되고, join 메서드를 통해 두 스레드 작업이 끝날 때까지 기다린 후 checkException 메서드를 통해 위에서 예상한 것처럼 DuplicateKeyException 예외가 발생하는지를 확인하는 테스트이다. 

@Transactional 어노테이션이 없고 deleteAll 메서드가 있는데 이는 ExceptionThread 클래스를 통해 다른 스레드에서 memberService.signUp() 메서드가 호출되다보니 테스트가 완료 후 rollback이 되지않아 이를 deleteAll 메서드를 통해 clean up할 수 있도록 만들었다.

 

테스트를 실행하면 통과한 것을 확인할 수 있다. 따라서 DuplicateKeyException 예외에 대한 로직도 추가해줘야 하는데 @ControllerAdvice로 처리할지, Service 계층에서 try-catch 블록으로 예외를 변환하는 것이 좋을지는 한 번 고민해봐야 할 것 같다.

 

트랜잭션 관점으로 살펴보기

위 과정을 DB 트랜잭션 관점으로도 살펴보자. 결국 두 명의 사용자가 같은 email로 회원가입 요청을 보냈어도 아직 DB에는 해당 email에 대한 레코드가 존재하지 않기 때문에 두 트랜잭션 모두 read(email) 이후 각각 write(member_1), write(member_2) 쓰기 작업을 수행하게 된다.

하지만 email 컬럼에 unque 제약조건을 설정함으로써 한 트랜잭션에서 쓰기 작업을 진행하고 commit이 된다면 다른 트랜잭션에서는 commit되는 시점에 unique 조건을 만족하지 않아 DataIntegrityViolationException 예외가 발생한다. 따라서 이를 통해 회원가입 동시성을 해결할 수 있다.

 

또한, AUTO_INCREMENT 컬럼이 사용된 테이블에 동시에 여러 레코드가 INSERT하려는 경우에는 내부적으로 AUTO_INCREMENT(자동 증가 락)을 통해 테이블 수준의 잠금을 사용한다. AUTO_INCREMENT 락은 테이블에 단 하나만 존재하기 때문에 두 개의 INSERT 쿼리가 동시에 실행되는 경우 하나의 쿼리가 AUTO_INCREMENT 락을 걸어 다른 쿼리는 AUTO_INCREMENT 락을 기다리고 하나의 쿼리가 끝나면 다른 쿼리가 INSERT 하려고 할 때 DB 단에서 에러가 발생하게 된다.

'Java > Spring' 카테고리의 다른 글

JPA  (0) 2024.03.26
빈 후처리기(BeanPostProcessor)  (0) 2023.10.17
프록시 팩토리를 통한 AOP  (1) 2023.10.16
Auto Configuration  (0) 2023.10.09
ThreadLocal을 사용하여 로깅하기 + MDCFilter  (0) 2023.10.08