2026년 06월 22일 | DBMS Error 가이드
이 글에서 다루는 내용
23P01 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.
23P01 exclusion violation 는?
PostgreSQL 에러 코드 23P01은 exclusion violation(배제 제약 위반)을 의미하며, 테이블에 정의된 배제 제약(Exclusion Constraint)을 위반하는 데이터를 삽입하거나 수정하려 할 때 발생합니다. 배제 제약은 특정 연산자(예: 겹침, 같음 등)를 기준으로 두 행이 동시에 존재할 수 없도록 강제하는 고급 무결성 제약 조건입니다. 예를 들어, 회의실 예약 시스템에서 동일한 방에 겹치는 시간대의 예약이 두 개 이상 존재하지 못하도록 할 때 흔히 사용됩니다.
주요 발생 원인
1. 시간 범위(Range) 데이터의 중복 삽입
가장 흔한 원인으로, tsrange, daterange, int4range 등의 범위 타입 컬럼에 배제 제약이 설정된 상태에서 이미 존재하는 범위와 겹치는 데이터를 삽입할 때 발생합니다. 예를 들어, 숙박 예약 시스템에서 동일 객실에 대해 날짜가 겹치는 예약이 들어오면 PostgreSQL은 즉시 이 에러를 반환합니다. 배제 제약은 내부적으로 GiST 인덱스를 사용하여 빠르게 충돌 여부를 검사합니다.
2. 공간 데이터(Geometry)의 겹침
PostGIS 확장을 활용하여 공간 데이터에 배제 제약을 설정한 경우, 새로 삽입하려는 공간 객체가 기존 데이터와 물리적으로 겹칠 때 이 에러가 발생합니다. 지도 기반 시스템에서 구역(Zone)이나 영역(Region)이 서로 겹치지 않아야 할 때 배제 제약을 적용하는데, 조금이라도 겹치는 폴리곤을 삽입하면 23P01 에러가 발생합니다. 공간 데이터는 부동소수점 정밀도 문제로 인해 의도치 않은 겹침이 발생할 수 있어 더욱 주의가 필요합니다.
3. 복합 조건 배제 제약에서의 충돌
여러 컬럼을 조합한 복합 배제 제약에서 충돌이 발생하는 경우입니다. 예를 들어, 특정 리소스(resource_id)와 시간 범위(during) 두 조건을 모두 고려하는 배제 제약에서, 같은 리소스에 대해 겹치는 시간대를 예약하려 할 때 에러가 발생합니다. 개발자들이 복합 조건 배제 제약의 동작 방식을 정확히 이해하지 못한 채 데이터를 삽입하는 경우 자주 마주치는 상황입니다.
해결 방법
원인 1 해결: 삽입 전 중복 범위 확인 및 처리
먼저 배제 제약이 어떻게 정의되어 있는지 확인합니다.
-- 테이블의 배제 제약 확인
SELECT conname, pg_get_constraintdef(oid)
FROM pg_constraint
WHERE conrelid = 'room_reservations'::regclass
AND contype = 'x';
삽입하기 전에 겹치는 데이터가 있는지 먼저 조회합니다.
-- 겹치는 예약이 있는지 사전 확인
SELECT *
FROM room_reservations
WHERE room_id = 101
AND during && tsrange('2024-07-01 09:00', '2024-07-01 11:00');
겹치지 않을 때만 삽입하는 방법을 사용합니다.
-- 조건부 삽입 (겹치는 데이터가 없을 때만 삽입)
INSERT INTO room_reservations (room_id, during, user_id)
SELECT 101, tsrange('2024-07-01 09:00', '2024-07-01 11:00'), 42
WHERE NOT EXISTS (
SELECT 1
FROM room_reservations
WHERE room_id = 101
AND during && tsrange('2024-07-01 09:00', '2024-07-01 11:00')
);
애플리케이션 레벨에서 에러를 잡아 사용자에게 친절한 메시지를 전달하는 방법도 병행해야 합니다.
-- 예외 처리를 포함한 PL/pgSQL 예시
DO $$
BEGIN
INSERT INTO room_reservations (room_id, during, user_id)
VALUES (101, tsrange('2024-07-01 09:00', '2024-07-01 11:00'), 42);
EXCEPTION
WHEN exclusion_violation THEN
RAISE NOTICE '해당 시간대에 이미 예약이 존재합니다. 다른 시간을 선택하세요.';
END;
$$;
원인 2 해결: 공간 데이터 겹침 처리
공간 데이터의 경우 삽입 전 겹침 여부를 반드시 확인하고, 필요시 경계를 약간 축소하거나 ST_Buffer를 활용합니다.
-- PostGIS 겹침 사전 확인
SELECT zone_id, zone_name
FROM city_zones
WHERE geom && ST_GeomFromText('POLYGON((...))', 4326)
AND ST_Intersects(geom, ST_GeomFromText('POLYGON((...))', 4326));
-- 겹치지 않는 경우에만 삽입
INSERT INTO city_zones (zone_name, geom)
SELECT '신규구역A', ST_GeomFromText('POLYGON((...))', 4326)
WHERE NOT EXISTS (
SELECT 1 FROM city_zones
WHERE ST_Intersects(geom, ST_GeomFromText('POLYGON((...))', 4326))
);
원인 3 해결: 복합 배제 제약 충돌 해결
복합 배제 제약의 정의를 정확히 파악하고, 삽입 전 모든 조건을 확인합니다.
-- 복합 배제 제약 확인 예시
-- resource_id가 같고 시간이 겹치는 경우를 막는 배제 제약 생성
CREATE TABLE resource_bookings (
id SERIAL PRIMARY KEY,
resource_id INT NOT NULL,
during TSRANGE NOT NULL,
booked_by INT NOT NULL,
EXCLUDE USING gist (
resource_id WITH =,
during WITH &&
)
);
-- 충돌 가능 여부 사전 조회
SELECT *
FROM resource_bookings
WHERE resource_id = 5
AND during && tsrange('2024-08-01 14:00', '2024-08-01 16:00');
-- 문제없이 삽입 가능한 데이터 확인 후 삽입
INSERT INTO resource_bookings (resource_id, during, booked_by)
VALUES (5, tsrange('2024-08-01 17:00', '2024-08-01 19:00'), 99);
예방 방법
1. 애플리케이션 레벨에서의 사전 유효성 검사 및 트랜잭션 잠금 활용
배제 제약을 DB에만 의존하지 말고, 애플리케이션 레벨에서도 중복 여부를 미리 검사하는 로직을 구현하세요. 특히 동시성이 높은 환경에서는 SELECT ... FOR UPDATE 또는 LOCK TABLE을 활용하여 다른 트랜잭션이 같은 자원을 동시에 예약하는 race condition을 방지해야 합니다.
-- 트랜잭션 내에서 잠금을 활용한 안전한 예약 처리
BEGIN;
-- 해당 리소스를 잠금 (다른 트랜잭션이 동시 접근하지 못하도록)
SELECT pg_advisory_xact_lock(resource_id)
FROM resource_bookings
WHERE resource_id = 5
LIMIT 1;
-- 중복 확인 후 삽입
INSERT INTO resource_bookings (resource_id, during, booked_by)
SELECT 5, tsrange('2024-08-01 14:00', '2024-08-01 16:00'), 99
WHERE NOT EXISTS (
SELECT 1 FROM resource_bookings
WHERE resource_id = 5
AND during && tsrange('2024-08-01 14:00', '2024-08-01 16:00')
);
COMMIT;
2. 배제 제약 설계 시 명확한 범위 경계 정책 수립
배제 제약을 설계할 때 범위의 경계(inclusive/exclusive)를 명확히 정의하고, 팀 내에서 이를 문서화하세요. tsrange의 경우 [시작, 종료) 형태의 half-open interval을 사용하면 두 예약이 시간적으로 인접할 때(예: 11:00 종료, 11:00 시작) 충돌 없이 처리할 수 있습니다. 또한, 정기적으로 운영 환경의 배제 제약 위반 시도 횟수를 모니터링하여 이상 패턴을 조기에 탐지하는 것이 중요합니다.
관련 에러
- 23000 integrity_constraint_violation: 배제 제약 위반을 포함한 모든 무결성 제약 위반의 상위 에러 클래스입니다.
- 23505 unique_violation: 유니크 제약 위반으로, 배제 제약과 유사하게 중복 데이터를 방지하지만 단순 등호(=) 기반의 비교에 국한됩니다. 배제 제약은 이보다 더 일반화된 형태입니다.
- 23514 check_violation: CHECK 제약 위반으로, 컬럼 값이 지정된 조건을 만족하지 않을 때 발생합니다. 배제 제약과 함께 데이터 무결성을 다층으로 보호할 때 함께 사용됩니다.
- 55P03 lock_not_available: 배제 제약 충돌을 방지하기 위해 잠금을 사용할 때, 잠금 획득에 실패하면 발생할 수 있는 관련 에러입니다.
주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.