(A, B)와 (B, A)에 유니크 제약 조건 걸기

June·2023년 5월 1일
0

실무 문제

목록 보기
6/10

가위바위보 이벤트를 만들면서 재밌는 문제가 있었고, 다른 분들의 도움으로 해결할 수 있었다. 그 문제와 해결 과정이 재밌었다.

우선 가위바위보 이벤트는 다른 사람에게 링크를 보내서 링크를 받은 사람이 경기에 참여함으로서 두 사람이 가위바위보를 하게된다. 여기서 제약 조건은 두 사람은 하나의 경기만 할 수 있다.

즉, A가 B에게 A.com 이라는 링크를 보냈을때 둘이 경기를 했다고 치자. 그럼 B가 A에게 B.com 링크를 보내면 새로 경기를 하는 것이 아니라 기존에 했던 경기가 조회되어야 한다.

Game이라는 테이블에는 sender, receiver 라는 초대자와 피초대자의 userId가 들어간다.

이때 잘 발생하지 않을 수 있지만, A와 B가 둘이 동시에 가위바위보 버튼을 누르는 경우를 어떻게 막을 수 있을까? 별다른 방어 로직을 작성하지 않는다고 가정해보자.

Game Table

...senderreceiver...
...AB...
...BA...

이렇게 두 개의 row가 쌓이게 될거다. 그럼 도메인 로직을 어기게 된다. 물론 게임을 생성하기 전에 validation을 하겠지만, 동시성 문제가 발생하면 이 validation 역시 뚫을 수 있다.

손쉬운 방법으로 분산락을 이용하는 방법이 있을 수 있다. 만약 하려면 A id와 B id를 정렬해서 하면 되지만, 실제로 컨트롤러에 들어오는 값은 A id가 아닌 A id를 감싸서 만든 referral 이어서 꽤 복잡했다.

DB에서 유니크 제약 조건을 걸자고 하니, (sender, receiver)에는 복합 유니크 제약 조건을 걸 수 있고, (receiver, sender)에도 걸 수 있지만, (sender, receiver) 와 (receiver, sender)에 한번에 걸 수가 없었다. 어떻게 할지 고민하다가 옆 팀 분이 아이디어를 주셨다.

매핑 테이블과 트랜잭션을 이용하면 이 문제를 해결할 수 있다.

Game Table

idsenderreceiver...
100AB...

Game Map

...firstsecondgameId
...AB100
...BA100

GameTable에 저장하고나서, 그 id를 가지고 (A, B), (B, A) 두 row를 한꺼번에 저장한다. GameMap 테이블에는 (first, second), (second, first) 둘다 유니크 제약 조건이 걸려있다.

예시코드

@Transactional
fun play() {
    val gameId = gameRepository.save(A, B).id
    
    gameMapRepository.save(A, B, gameId)
    gameMapRepository.save(B, A, gameId)
}

우선은 GameTable에 저장을 한다. 여기까지는 어떻게 동시에 의해 A와 B 둘다 각각 게임을 생성할 수 있다.

하지만 그 다음에 game map 테이블에 저장할때, 어느 누군가는 먼저 저장을 해놨을 것이기 때문에 뒤에 실행하는 누군가는 예외를 맞게된다. 예외를 맞으면서 전체가 롤백되게 된다.

MySQL 엔진에서는 락과 인덱스를 이용해야 내부적으로 동시성 문제를 제어한다.

0개의 댓글