이 문제의 실무 기준은 “사용 가능해 보이는가”가 아니라 “최종 커밋 시점에 동일성 범위에서 하나만 남는가”입니다. 사전 조회는 입력 오류를 빠르게 안내하는 보조 경로이고, 최종 UNIQUE 충돌은 정상적인 경쟁 결과로 분류해 처리해야 합니다. 충돌 응답을 만들 때는 이름 문자열이 아니라 검증된 요청 주체와 멱등 기록을 기준으로 기존 결과 재사용 여부를 판단합니다.
사전 조회와 최종 UNIQUE 경쟁
가입 요청 A와 B가 같은 정규화 이름을 조회하고 둘 다 없다고 읽은 뒤 삽입할 수 있습니다. 사전 SELECT는 사용자에게 빠른 안내를 줄 수 있지만 최종 유일성 보장이 아닙니다. 동일성 범위에 맞는 DB UNIQUE·고유 인덱스가 실제 쓰기에서 한 상태만 허용해야 합니다.
| 순서 | A | B |
|---|---|---|
| 1 | 이름 없음 조회 | |
| 2 | 이름 없음 조회 | |
| 3 | 삽입·커밋 성공 | |
| 4 | 삽입 시 대기·고유 충돌 등 엔진 결과 |
동시성 결과는 엔진·격리·제약 시점·삽입 방식에 따라 다르지만 사전 조회를 통과해도 충돌할 수 있다는 계약은 유지됩니다. 드라이버 오류 분류·제약 이름으로 예상 충돌을 처리하고 오류 문자열 전체에 의존하지 않습니다.
고유 충돌 계정과 요청 소유권
이름 충돌이 나면 ‘이미 존재하는 계정’이라는 사실만으로 그 계정을 새 요청자에게 반환하지 않아야 합니다. 같은 논리 요청을 네트워크 재시도로 다시 받은 것인지, 다른 사람이 같은 이름으로 가입한 것인지 요청 ID·검증된 주체·입력 fingerprint(요청 내용을 일관되게 식별하는 값)·멱등 기록을 함께 대조해 구분합니다. 그렇지 않으면 기존 계정이 노출되거나 새 요청자의 계정으로 잘못 연결될 수 있으며, 이름이 같다는 사실 자체는 소유권 증명이 아닙니다.
회원·프로필·환영 outbox가 한 작업이면 제약 실패 때 부분 데이터가 남지 않는 거래 경계를 정합니다. 동일 이름의 계정이 이미 있어도 새 요청의 프로필로 갱신할 권한이 자동 생기지 않습니다.
그림을 글로 읽기
- DB 고유 충돌
- 검증된 동일 요청
- 별도 사용자·새 요청
- 저장된 결과 재사용
- 이름 충돌 거절
- DB 고유 충돌 → 검증된 동일 요청: 멱등 문맥 일치
- DB 고유 충돌 → 별도 사용자·새 요청: 소유·요청 불일치
- 검증된 동일 요청 → 저장된 결과 재사용: 재전달
- 별도 사용자·새 요청 → 이름 충돌 거절: 기존 주체 보호
예상 결과를 확인하는 작은 경쟁 시험은 A와 B가 같은 정규화 이름을 동시에 제출하도록 하고, 최종적으로 한 행만 생성되며 한 요청은 분류된 충돌을 받는지 보는 것입니다. 누적 효과를 검증할 때는 같은 effect key를 재전달해 값이 한 번만 변하는지 별도로 측정해야 합니다. UNIQUE 성공과 업무 효과의 한 번 실행은 같은 보장이 아닙니다.
UPSERT와 업무 멱등성
UPSERT는 ‘없으면 삽입하고, 이미 있으면 정해 둔 방식으로 갱신하는’ 충돌 처리 분기입니다. PostgreSQL의 ON CONFLICT, MySQL의 ON DUPLICATE KEY, SQL Server의 MERGE는 문법 이름만 비슷할 뿐 동시성 처리, 어느 제약을 충돌로 삼는지, 반환값과 잠금의 계약이 같다고 볼 수 없습니다. 특히 UNIQUE가 여러 개면 실제로 어떤 키가 충돌했는지와 그 결과 갱신하려는 행이 원래 의도한 객체인지까지 확인해야 합니다.
절대값 닉네임 저장과 points = points + 10은 재실행 의미가 다릅니다. 후자는 같은 이벤트를 두 번 받으면 두 번 증가하므로 별도 effect key가 필요합니다. expected version·현재 owner·허용 상태를 갱신 조건에 넣고 조건 불일치의 0행을 성공으로 보고하지 않습니다. 생성 전용 필드와 수정 가능 필드, null 입력의 삭제 의미도 구분합니다.
소프트 삭제 이름 재사용과 복구 충돌
활성 행에만 UNIQUE를 적용해 A 탈퇴 후 B가 이름을 사용할 수 있게 했다면 A 복구 때 둘을 동시에 활성화할 수 없습니다. 복구 시 새 이름 요구·수동 검토·재사용 유예 등 정책을 미리 정하고 현재 활성 사용자의 계정을 덮지 않습니다.
partial·filtered index 등 최종 제약이 복구와 동시 가입을 함께 판단해야 합니다. 먼저 이름이 비었는지 조회한 뒤 무조건 active로 바꾸면 같은 경쟁이 남습니다. 불변 account ID로 주문·로그인·제재를 참조해 이름 재사용이 데이터 소유 이전이 되지 않게 합니다.
지역별 UNIQUE와 전역 이름 예약
지역 A와 B의 독립 DB가 동시에 같은 이름을 수락하면 각 지역 UNIQUE는 모두 만족할 수 있습니다. 전역 유일성이 필수라면 정규화 이름별 단일 권위·합의 저장소·이름 namespace 분할 등 최종 예약 경계를 정합니다. 분할 중 권위를 확인할 수 없으면 확정을 보류하거나 요청을 거절할 수 있어야 합니다. 충돌 후 정정 정책을 택하면 즉시 전역 유일성과 다른 계약임을 명시합니다.
예약 성공 뒤 계정 생성 전에 중단될 수 있으므로 reserved·committed·expired 상태와 owner·요청 ID·기한을 연결합니다. 임대가 만료된 옛 요청이 나중 계정을 생성하지 않도록 예약 세대를 최종 생성점에서도 검사합니다. 단순 TTL 잠금만으로 전역 계정과 예약이 원자 거래가 되는 것은 아닙니다.
동일성 정의와 경쟁 재현
대소문자·공백·Unicode·tenant·탈퇴·복구·다중 지역을 포함해 동일 이름 정의를 앱과 DB에서 맞춥니다. 기존 중복 정리도 임의 삭제가 아니라 참조와 소유권을 유지하는 승인 절차가 필요합니다.
테스트는 사전 조회 둘 다 통과, UPSERT 복수 제약 충돌, 누적 이벤트 재전달, 복구와 가입 경쟁, 예약 후 중단을 나눕니다. 현재 작업에서는 다중 지역 DB나 각 제품의 UPSERT를 실행하지 않았습니다. 본문은 유일성·인가·효과의 설계 계약입니다.