백엔드 개발을 하다 보면 한 번쯤 이런 에러를 보게 된다.
ERROR: duplicate key value violates unique constraint
Key (id)=(4) already exists
auto increment인데 왜 중복이 발생할까?
처음에는 DB가 자동으로 값을 잘 관리해줄 거라고 생각하기 쉽다.
하지만 실제로는 그렇지 않다.
결론부터 말하면
시퀀스는 테이블 상태를 전혀 모른다.
PostgreSQL에서 auto increment는 내부적으로 다음과 같이 동작한다.
nextval('sequence_name')
이 함수는 단순히 숫자를 하나씩 증가시키며 반환한다.
중요한 점은 다음이다.
즉, auto increment는 “똑똑한 증가 기능”이 아니라
독립적인 숫자 생성기에 가깝다.
다음과 같은 상황을 가정해보자.
테이블 데이터:
id = 1, 2, 3, 4, 5
시퀀스 상태:
nextval = 4
이 상태에서 insert를 수행하면
INSERT INTO table (...) VALUES (...);
실제로는 내부적으로 다음과 같이 처리된다.
id = nextval(...)
→ 4
이미 테이블에 존재하는 값이기 때문에
duplicate key 에러가 발생한다.
이런 상황은 의외로 자주 발생한다.
INSERT INTO table(id, ...) VALUES (100, ...);
테이블에는 100이 들어가지만
시퀀스는 이 사실을 모른다.
이후 시퀀스는 기존 값 기준으로 계속 증가하게 되고
결국 충돌이 발생한다.
데이터를 백업했다가 복구할 때
실무에서 가장 흔한 원인이다.
운영 환경의 데이터를 개발 환경으로 복사할 때
이 경우에도 쉽게 문제가 발생한다.
TRUNCATE TABLE table;
이 명령은 데이터를 삭제할 뿐
시퀀스 값은 초기화하지 않는다.
그래서 이후 insert 시 예상과 다른 값이 들어갈 수 있다.
SELECT nextval('seq'); -- 10
ROLLBACK;
이 경우 많은 사람들이
값이 롤백될 것이라고 생각한다.
하지만 시퀀스는 트랜잭션과 무관하다.
그래서 중간 숫자가 비는 것은 정상적인 동작이다.
시퀀스가 테이블과 독립적으로 동작하는 이유는 성능 때문이다.
만약 시퀀스가 테이블 상태를 매번 확인한다면
그래서 DB는 단순한 구조를 선택했다.
문제가 발생했다면 시퀀스를 테이블에 맞춰주면 된다.
SELECT setval(
'sequence_name',
(SELECT MAX(id) FROM table)
);
이렇게 하면 시퀀스가 현재 데이터 기준으로 재정렬된다.
데이터를 복사하거나 복구했다면
반드시 시퀀스를 맞춰야 한다.
특별한 이유가 없다면
auto increment에 맡기는 것이 안전하다.
코드 문제로 보기 전에
시퀀스 상태를 먼저 확인하는 것이 빠르다.
auto increment는 테이블을 기준으로 동작하는 기능이 아니다.
이 구조를 이해하지 못하면
예상하지 못한 duplicate key 문제를 만나게 된다.