데이터베이스에 중복 생성이 되지 않게 하는 방법은?
문제 상황
기술 면접 중, 동시성 문제와 관련해서 내가 짠 로직으로는 아이디가 중복 생성이 가능해보인다 라는 피드백을 받았다.
코드를 작성할 때는 몰랐지만, 막상 면접에서 보니 그럴만 하다는 생각이 들었다.
(해당 코드는 맨 아래에 첨부하겠습니다)
그래서 이걸 어떻게 하면 중복 생성을 막을 수 있을까 여쭤보셨고 난 다음과 같이 말씀드렸다.
음~ 테이블에서 계정명을 기준으로 SELECT하는 부분부터 생성하는 부분까지 하나의 트랜잭션으로 묶어서 관리하면 될 것 같습니다! ㅎㅎ
그런데 SELECT하는 쿼리는 LOCK이 안 걸리니, PK같은 거로 설정하면 어떨까 말씀해주셨다.
그 당시에는 ㅇㅈ. 하고 넘어갔지만 곰곰히 생각해보니 "보통 VARCHAR로 PK를 하나? UNIQUE가 낫지 않나?" 라는 의문이 들었다.
왜냐면 그동안 했던 프로젝트에선 회원가입이 없었고, 항상 AUTO_INCREMENT INTEGER로 PK를 잡아두었기 때문이다.
(물론 내 수준을 고려해 쉬운 방법을 알려주셨을 수도 있다)
말씀해주신대로 계정명을 PK로 하면 확실히 DB에서 중복생성은 되지 않겠지만 뭔가 어색하다고 느껴졌는데,
아는 게 없는 상황에서의 막연한 어색함이라 조사를 한번 해보았다.
테스트 환경
m2 air에서 jmeter로 스레드 100개가 동시에 같은 아이디로 회원가입 API로 요청을 보내는 시나리오다.
테스트 툴은 jmeter라는 걸 사용해보기로 했다.
쓰레드 100개에서 2번씩 보내게끔 했는데, 딱히 이유는 없고 내 생각에 많은 요청을 넣기 위해 설정한 임의의 숫자다.
그 결과....
중복 생성이 아주 잘 되었다!!!
앞으로 내 해결책이 소용있는지 확인하기 위해, 어떻게 하면 중복생성이 잘 될지 실험해봤다.
내가 건드린 파라미터는 스레드 수와 loop-count인데, loop-count는 예상대로 큰 의미가 없었다.
(최초에 생성에 성공했든 실패했든, 다음 루프에선 무조건 실패라 생각했다)
스레드 수에 따른 중복생성 계정 수는 다음과 같다.
스레드 수/ 실험 회차 | 1차시에서의 중복 계정 수 | 2차시에서의 중복 계정 수 |
100 | 5 | 4 |
1,000 | 30 | 37 |
10,000 | 150 | X |
1만개는 jmeter에서 버그가 발생해서 한번만 했다.
이제 실험을 해보자.
가설 1. 중복체크 ~ 생성까지 트랜잭션으로 묶는다.
문제가 있는 기능은 회원가입인데, 과정을 나눠보면 다음과 같다.
- account_name을 기준으로 select한다. (중복이면 종료)
- select했을 때 결과가 없으면 create한다.
....
라고 생각했다.
그런데 <데이터베이스 첫걸음> 이라는 책을 보면서 데드락에 대한 실습을 해보았는데,
락을 걸 때는 보통 해당 인덱스나 범위의 row를 수정하지 못하게 하는 것이다.
하지만 우리가 문제로 삼고 있는 상황은 동시에 중복 아이디가 생성되는 경우이다.
그러니까 CREATE를 하면서 해당 ROW, 아이디에 대한 lock을 갖고 있어야 하는데,
애초에 SELECT에서 잡을 것이 없으니 lock을 못 잡을 것이다.
만약 lock을 잡는다면 테이블을 잡아야 하는데, 생각해보았을 때 굉장히 비효율적인 방식일 것 같다.
+ 트랜잭션 격리 수준을 트랜잭션마다 설정할 수 있는 줄 몰랐다. 나는 MySQL에서 하나 설정해두면 다른 트랜잭션들도 모두 걸리는 줄...
+ serializable하면 트랜잭션 단위로 큐에 들어가서 처리되는 줄 알았다. 별로 빡빡하지도 않네!
2. account name을 PK(또는 UNIQUE)로 설정하기
내가 한가지 고민한 부분은 PK로 숫자를 쓰지 않는 것인데 이것은 다른 글에서 살펴보자.
MySQL 터미널에서 두가지 connection을 띄워놓고 다음과 같은 순서로 진행해보았다.
(둘 다 격리 수준은 RR)
- A, B 둘 다 트랜잭션 시작
- A에서 특정 id로 insert를 한다.
- B에서 SELECT를 해본다. -> 2에서 INSERT한 애는 커밋해도 안 뜸
- B에서 같은 id로 INSERT 한다. -> Lock wait timeout exceeded
같은 ID더라도 2회 이상 INSERT가 될 수 없는 구조다.
(insert의 atomicity가 보장되는 한...)
+ 한가지 신기했던 건 그동안 RR과 CR의 구분을 못 했었는데 3의 쿼리 결과를 보며 왜 Repeatable Read라고 하는지 알게됐다.
(하나의 스냅샷만을 가지고 있으므로 다시 요청해도 반복 가능한 결과가 나오는 것이다)
이제 실제 nemo 백엔드에서 account name을 pk로 설정하고 테스트해보자!
위와 같이 100, 1000, 10000의 스레드 개수로 실행시켜보았다.
혹시나 싶어 pk는 기존처럼 id로 두고, account_name에 unique를 적용시켜보았다.
결과는 똑같이 중복생성 되지 않았다.
하지만 마냥 똑같은 결과는 아닌 것이, ID가 조금씩 밀려있다.
나의 생각으로는 auto_increment와 관련돼서 커밋할 때 마다 +1이 되는 게 아니고, 커밋 이전에 어딘가에 있을 id변수에 +1을 해놓고 다른 스레드로 넘어가기 때문에 이렇게 된 것 같다.
결론
- 중복 생성이 되면 안 되는 필드로 PK 설정이나 UNIQUE 속성을 주면 DB 레벨에서 중복 생성을 (쉽게) 막을 수 있다.
(그러나 예외처리를 제대로 해주지 않으면 서버가 꺼질 수 있다 -> 난 docker-compose라 무적) - auto_increment는 어떻게 구현되어 있을까?
- ID를 INTEGER로 하지 않아도 괜찮을까 고민, 조사해보자.