2026년 07월 14일 | DBMS Error 가이드
이 글에서 다루는 내용
42P16 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.
42P16 invalid table definition 는?
PostgreSQL 에러 코드 42P16은 invalid table definition 으로, 테이블 생성(CREATE TABLE) 또는 테이블 변경(ALTER TABLE) 시 정의 자체가 잘못되었을 때 발생합니다. 즉, PostgreSQL이 내부적으로 테이블 구조를 해석하는 과정에서 논리적으로 불가능하거나 충돌하는 제약 조건, 컬럼 정의, 상속 구조 등을 감지했을 때 이 에러를 반환합니다. 단순한 문법 오류(예: 42601 syntax error)와는 달리, 문법적으로는 올바르지만 테이블 정의 자체의 의미론적(Semantic) 무결성이 깨진 경우에 발생한다는 점에서 초보 DBA들이 원인을 파악하기 어려운 에러 중 하나입니다.
주요 발생 원인
1. PRIMARY KEY 또는 UNIQUE 제약 조건의 중복 및 충돌 정의
가장 흔하게 발생하는 원인으로, 동일한 컬럼에 대해 PRIMARY KEY를 두 번 선언하거나, 컬럼 레벨과 테이블 레벨에서 동시에 기본 키를 정의하는 경우입니다. PostgreSQL은 테이블 당 기본 키를 하나만 허용하므로, 두 개 이상의 PRIMARY KEY 정의가 감지되면 즉시 42P16 에러를 발생시킵니다. 실무에서는 ORM(Object-Relational Mapping) 도구가 자동 생성한 DDL 스크립트나 레거시 시스템에서 마이그레이션한 스크립트에서 이 문제가 자주 등장합니다.
2. 테이블 상속(Inheritance) 구조에서의 제약 조건 충돌
PostgreSQL의 고유 기능인 테이블 상속(INHERITS) 사용 시, 부모 테이블과 자식 테이블 간의 컬럼 타입 불일치 또는 동일 컬럼에 대한 NOT NULL, DEFAULT 등의 충돌 정의가 발생할 수 있습니다. 특히 부모 테이블에 존재하는 컬럼을 자식 테이블에서 다른 데이터 타입으로 재정의하려 할 때 이 에러가 발생합니다. 파티셔닝(Partitioning)이 내부적으로 상속 메커니즘을 사용하던 구버전(PostgreSQL 10 이전)에서는 이와 관련된 에러가 더욱 빈번하게 나타났습니다.
3. 파티션 테이블 정의 오류 (Partition Bound 충돌 또는 누락)
PostgreSQL 10 이후 도입된 선언적 파티셔닝(Declarative Partitioning)에서 파티션 범위(PARTITION BY RANGE, PARTITION BY LIST, PARTITION BY HASH)를 잘못 정의하거나, 파티션 자식 테이블의 FOR VALUES 절이 부모 테이블의 파티션 키 타입과 맞지 않을 때 발생합니다. 예를 들어, 부모 테이블이 RANGE 파티션으로 정의되었는데 자식 파티션을 LIST 방식으로 연결하려 하거나, 파티션 바운드 값이 겹치는(overlapping) 경우에도 이 에러가 나타납니다. 대용량 데이터 시스템에서 파티션 전략을 설계할 때 사전에 충분한 검토가 필요한 이유가 바로 이것입니다.
해결 방법
원인 1 해결: PRIMARY KEY 중복 제거
컬럼 레벨과 테이블 레벨에서 동시에 기본 키를 정의한 경우, 둘 중 하나만 남겨야 합니다.
잘못된 예시:
-- 에러 발생: PRIMARY KEY가 두 곳에 정의됨
CREATE TABLE orders (
order_id BIGINT PRIMARY KEY, -- 컬럼 레벨 PK
order_date DATE NOT NULL,
customer_id INT,
CONSTRAINT pk_orders PRIMARY KEY (order_id) -- 테이블 레벨 PK (충돌!)
);
-- ERROR: multiple primary keys for table "orders" are not allowed (42P16)
올바른 예시 (컬럼 레벨):
CREATE TABLE orders (
order_id BIGINT PRIMARY KEY,
order_date DATE NOT NULL,
customer_id INT
);
올바른 예시 (테이블 레벨, 복합 PK의 경우 권장):
CREATE TABLE order_items (
order_id BIGINT NOT NULL,
item_seq INT NOT NULL,
product_id INT,
CONSTRAINT pk_order_items PRIMARY KEY (order_id, item_seq)
);
원인 2 해결: 테이블 상속 구조 충돌 수정
부모 테이블의 컬럼 타입과 자식 테이블의 컬럼 타입이 일치해야 합니다.
잘못된 예시:
-- 부모 테이블 생성
CREATE TABLE vehicles (
vehicle_id INT PRIMARY KEY,
registration_no VARCHAR(20) NOT NULL
);
-- 에러 발생: registration_no 타입 불일치 (VARCHAR vs TEXT)
CREATE TABLE cars (
registration_no TEXT, -- 부모와 타입 충돌!
fuel_type VARCHAR(10)
) INHERITS (vehicles);
-- ERROR: column "registration_no" has a type conflict (42P16)
올바른 예시:
-- 부모 테이블 생성
CREATE TABLE vehicles (
vehicle_id INT PRIMARY KEY,
registration_no VARCHAR(20) NOT NULL
);
-- 자식 테이블: 충돌 컬럼 제거 후 부모로부터 상속
CREATE TABLE cars (
fuel_type VARCHAR(10)
) INHERITS (vehicles);
-- 상속된 구조 확인
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = 'cars';
원인 3 해결: 파티션 정의 오류 수정
파티션 바운드 타입과 범위가 부모 테이블의 파티션 키와 일치해야 합니다.
잘못된 예시:
-- 부모 테이블: RANGE 파티셔닝
CREATE TABLE sales (
sale_id BIGINT,
sale_date DATE NOT NULL,
amount NUMERIC(12,2)
) PARTITION BY RANGE (sale_date);
-- 에러 발생: RANGE 파티션에 LIST 바운드 사용
CREATE TABLE sales_2024
PARTITION OF sales
FOR VALUES IN ('2024-01-01'); -- LIST 방식 사용 (잘못됨!)
-- ERROR: invalid bound specification for a range partition (42P16)
올바른 예시:
-- 부모 테이블: RANGE 파티셔닝
CREATE TABLE sales (
sale_id BIGINT,
sale_date DATE NOT NULL,
amount NUMERIC(12,2)
) PARTITION BY RANGE (sale_date);
-- 올바른 RANGE 파티션 자식 테이블 생성
CREATE TABLE sales_2024_q1
PARTITION OF sales
FOR VALUES FROM ('2024-01-01') TO ('2024-04-01');
CREATE TABLE sales_2024_q2
PARTITION OF sales
FOR VALUES FROM ('2024-04-01') TO ('2024-07-01');
-- 파티션 구조 확인
SELECT
child.relname AS partition_name,
pg_get_expr(child.relpartbound, child.oid) AS partition_bound
FROM pg_class parent
JOIN pg_inherits ON pg_inherits.inhparent = parent.oid
JOIN pg_class child ON pg_inherits.inhrelid = child.oid
WHERE parent.relname = 'sales';
예방 방법
1. DDL 스크립트 사전 검증 및 리뷰 프로세스 도입
운영 환경에 DDL을 적용하기 전, 반드시 개발 또는 스테이징 환경에서 먼저 실행하여 에러 여부를 확인하는 습관을 들여야 합니다. 특히 ORM이 생성한 마이그레이션 스크립트나 타 DBMS(MySQL, Oracle 등)에서 변환된 DDL의 경우, PostgreSQL의 제약 조건 정의 방식과 미묘한 차이가 있을 수 있으므로 psql의 \d+ 테이블명 명령으로 생성 후 구조를 반드시 확인하십시오. 또한 팀 내에서 DDL 변경 시 코드 리뷰(Pull Request)를 필수화하고, pg_dump로 현재 스키마를 백업한 뒤 적용하는 프로세스를 정착시키는 것이 좋습니다.
-- 테이블 구조 상세 확인 (psql 명령어)
-- \d+ table_name
-- 제약 조건 목록 확인
SELECT
conname AS constraint_name,
contype AS constraint_type,
pg_get_constraintdef(oid) AS definition
FROM pg_constraint
WHERE conrelid = 'your_table'::regclass
ORDER BY contype;
2. 파티셔닝 설계 시 사전 계획 문서화 및 테스트
파티션 전략(RANGE, LIST, HASH)은 한번 결정하면 변경 비용이 매우 크므로, 설계 단계에서 파티션 키 선정, 바운드 범위, 자식 테이블 수 등을 문서화하고 소규모 데이터로 테스트를 먼저 수행해야 합니다. PostgreSQL 공식 문서의 파티셔닝 가이드를 참고하고, 아래와 같이 파티션 정보를 정기적으로 모니터링하는 쿼리를 활용하십시오.
-- 파티션 테이블 전체 현황 모니터링
SELECT
nmsp_parent.nspname AS parent_schema,
parent.relname AS parent_table,
nmsp_child.nspname AS child_schema,
child.relname AS child_table,
pg_get_expr(child.relpartbound, child.oid, true) AS partition_expression
FROM pg_inherits
JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
JOIN pg_class child ON pg_inherits.inhrelid = child.oid
JOIN pg_namespace nmsp_parent ON nmsp_parent.oid = parent.relnamespace
JOIN pg_namespace nmsp_child ON nmsp_child.oid = child.relnamespace
WHERE parent.relkind = 'p' -- 파티션 테이블만 필터
ORDER BY parent.relname, child.relname;
관련 에러
42601(syntax_error): DDL 문법 자체가 잘못된 경우로,42P16보다 먼저 발생합니다. 문법 에러 해결 후에도42P16이 남아있다면 의미론적 정의 문제를 확인해야 합니다.42P07(duplicate_table): 이미 존재하는 테이블 이름으로CREATE TABLE시 발생합니다.CREATE TABLE IF NOT EXISTS구문으로 예방할 수 있습니다.23514(check_violation): 테이블 정의 단계가 아닌 데이터 삽입/수정 시CHECK제약 조건 위반으로 발생하며,42P16과 혼동될 수 있습니다.0A000(feature_not_supported): 특정 파티션 구성이나 상속 구조에서 PostgreSQL 버전이 해당 기능을 지원하지 않을 때 발생하며,42P16과 함께 나타나는 경우가 있습니다.
주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.