같은 경기 두 번 안 만드는 dedupe 키: 날짜·장소·상대팀
동호회 경기 일정 등록 API에서 날짜·장소·상대팀을 묶은 유니크 키와 ON CONFLICT로 중복 등록을 DB 층에서 막는 방법을 정리했어요.
같은 경기 두 번 안 만드는 dedupe 키: 날짜·장소·상대팀
동호회 일정은 총무도 올리고 주장도 올려요. 그래서 같은 경기가 두 줄로 들어오는 일이 생각보다 자주 생기더라고요. 플랜비의 출전 로테이션은 경기마다 누가 뛰는지 정하는 기능이라, 경기 하나가 둘로 갈라지면 그날 로테이션도 둘로 갈라져요. 직접 만들어 보니 "이미 있나요?" 조회 뒤에 insert하는 앱 코드는 거의 동시에 들어온 요청 2개를 못 걸러냈어요. 막아야 하는 자리는 결국 DB였어요.
키는 컬럼 3개, 단 원문이 아니라 id로
날짜·장소·상대팀을 그대로 유니크 인덱스에 넣으면 금방 깨져요. 테스트해 보면 "한강 썬더스"와 "한강썬더스"는 다른 값이라 둘 다 들어가요. 그래서 상대팀과 장소는 각각 테이블을 두고 opponent_team_id, venue_id로 묶었어요. 운영하며 써 보니 자유 입력이 필요한 팀도 있었는데, 그때는 lower(btrim(...)) 결과를 저장하는 생성 컬럼에 인덱스를 거는 쪽이 맞았어요.
날짜도 함정이 있어요. timestamptz를 그대로 날짜로 쓰면 한국 시간 오전 8시 경기가 UTC로는 전날 밤 11시예요. 9시간 차이가 같은 경기를 다른 날로 보이게 만들어요. 그래서 game_at과 별개로 한국 날짜만 담는 game_date 컬럼을 두고, 키에는 이 컬럼을 넣었어요.
- club_id
- game_date(한국 날짜)
- venue_id
- opponent_team_id
여기서 club_id가 하나 더 들어가요. 동호회 A와 B가 같은 날 같은 체육관에서 같은 팀과 붙을 수 있으니까요. 이 값은 요청 본문이 아니라 세션에서 꺼낸 멤버십으로 채워요. 본문의 club_id를 믿으면 다른 동호회 일정에 끼어드는 길이 열려요.
INSERT 한 번에 끝내기
CREATE UNIQUE INDEX games_dedupe_uq
ON games (club_id, game_date, venue_id, opponent_team_id);
INSERT INTO games (club_id, game_date, venue_id, opponent_team_id, game_at)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (club_id, game_date, venue_id, opponent_team_id) DO NOTHING
RETURNING id;더블탭으로 요청 2개를 거의 동시에 보내는 상황을 돌려 보니 하나만 id가 돌아오고 나머지는 0행이었어요. 0행이면 같은 키로 기존 경기를 한 번 더 조회해서 409 Conflict와 existing_id를 함께 내려줘요. 쿼리 2번이 들지만 클라이언트는 "이미 등록된 경기예요"를 띄우고 그 경기로 바로 이동할 수 있어요. 로그에는 created와 dedupe_hit을 갈라 남겨요. 그래야 나중에 "같은 요청 두 번"과 "경기 두 개 생성"을 구분할 수 있어요.
같은 날 두 경기는요
적용해 봤더니 반례가 하나 바로 나왔어요. 같은 날 같은 상대와 더블헤더를 치르는 날이요. 이때는 키에 시각을 넣기보다 game_no(기본 1) 컬럼을 추가해 컬럼 4개로 넓혔어요. 두 번째 경기만 2를 주면 평소 흐름은 그대로고 예외만 명시적으로 열려요. 라이프케어로그에서 작은 도구를 여러 개 만들며 배운 것도 비슷해요. 중복을 앱 코드로 걸러내는 대신 제약 하나를 DB에 두면 사고 범위가 훨씬 좁아져요.
관련 글
댓글
아직 댓글이 없어요. 첫 댓글을 남겨주세요.