PostgreSQL 428C9 오류 원인과 해결 방법 완벽 가이드

428C9
2026년 09월 11일 | DBMS Error 가이드

이 글에서 다루는 내용

428C9 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.

428C9 generated always 는?

PostgreSQL 에러 코드 428C9ERROR: column "컬럼명" can only be updated to DEFAULT 형태로 나타나며, GENERATED ALWAYS AS IDENTITY 또는 GENERATED ALWAYS AS (표현식) STORED로 정의된 컬럼에 직접 값을 삽입하거나 업데이트하려 할 때 발생합니다. 이 컬럼들은 PostgreSQL이 자동으로 값을 생성·관리하도록 설계되어 있기 때문에, 사용자가 임의의 값을 강제로 지정할 수 없습니다. 즉, 시스템이 “항상(ALWAYS)” 값을 생성한다는 제약을 위반했을 때 이 에러가 트리거됩니다.


주요 발생 원인

  • GENERATED ALWAYS AS IDENTITY 컬럼에 명시적 값 삽입

가장 흔한 원인입니다. GENERATED ALWAYS AS IDENTITY로 정의된 기본 키 또는 시퀀스 컬럼에 INSERT 구문에서 직접 숫자 값을 지정하면 이 에러가 발생합니다. 예를 들어 다른 데이터베이스에서 데이터를 마이그레이션하거나, 기존 레거시 코드가 ID 값을 직접 삽입하도록 작성된 경우 자주 마주치게 됩니다. GENERATED BY DEFAULT AS IDENTITY와 달리 ALWAYS 옵션은 사용자 입력을 엄격히 차단합니다.

  • GENERATED ALWAYS AS (expr) STORED 계산 컬럼에 값 직접 업데이트 시도

Generated Column(생성 컬럼)은 다른 컬럼의 값을 기반으로 표현식에 의해 자동으로 계산되어 저장됩니다. 이 컬럼에 UPDATE 문으로 직접 값을 변경하려 하면 에러 428C9가 발생합니다. 예를 들어 price * quantity로 자동 계산되는 total_price 컬럼에 임의의 값을 넣으려는 경우가 대표적입니다.

  • 데이터 마이그레이션 또는 pg_dump / pg_restore 시 충돌

pg_dump로 덤프한 데이터를 다른 데이터베이스에 복원하거나, 외부 ETL 도구가 GENERATED ALWAYS 컬럼에 값을 삽입하려 할 때 발생합니다. 특히 원본 DB가 SERIAL 또는 GENERATED BY DEFAULT 방식이었던 테이블을 대상 DB에서 GENERATED ALWAYS로 재정의한 경우, 복원 스크립트가 기존 ID 값을 그대로 밀어 넣으려 해서 에러가 터집니다.


해결 방법

원인 1 해결: OVERRIDING SYSTEM VALUE 사용 또는 컬럼 제외

GENERATED ALWAYS AS IDENTITY 컬럼에 어쩔 수 없이 특정 값을 삽입해야 한다면 OVERRIDING SYSTEM VALUE 키워드를 사용합니다.

-- 에러 발생 예시
CREATE TABLE orders (
    order_id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    customer_name TEXT
);

-- ❌ 에러 발생: 428C9
INSERT INTO orders (order_id, customer_name) VALUES (100, '홍길동');

-- ✅ 해결 1: OVERRIDING SYSTEM VALUE 사용 (특정 값 강제 삽입)
INSERT INTO orders (order_id, customer_name)
OVERRIDING SYSTEM VALUE
VALUES (100, '홍길동');

-- ✅ 해결 2: order_id 컬럼을 INSERT 목록에서 제외 (권장)
INSERT INTO orders (customer_name) VALUES ('홍길동');

마이그레이션 완료 후 시퀀스 값을 현재 최대값에 맞게 재설정해야 합니다.

-- 시퀀스를 현재 최대 ID 값으로 재동기화
SELECT setval(
    pg_get_serial_sequence('orders', 'order_id'),
    (SELECT MAX(order_id) FROM orders)
);

원인 2 해결: 계산 컬럼의 원본 컬럼 값을 수정

GENERATED ALWAYS AS (expr) STORED 컬럼은 직접 수정이 불가합니다. 해당 컬럼의 값을 바꾸려면 표현식을 구성하는 원본 컬럼을 업데이트해야 합니다.

-- 생성 컬럼 예시 테이블
CREATE TABLE sales (
    id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    price NUMERIC(10, 2),
    quantity INT,
    total_price NUMERIC(10, 2) GENERATED ALWAYS AS (price * quantity) STORED
);

-- ❌ 에러 발생: 428C9 (total_price 직접 수정 불가)
UPDATE sales SET total_price = 50000 WHERE id = 1;

-- ✅ 해결: 원본 컬럼(price, quantity)을 수정하면 total_price 자동 재계산
UPDATE sales SET price = 25000, quantity = 2 WHERE id = 1;

-- 결과 확인
SELECT id, price, quantity, total_price FROM sales WHERE id = 1;
-- total_price는 자동으로 50000으로 계산됨

원인 3 해결: 마이그레이션 시 컬럼 정의 변경 또는 덤프 옵션 조정

데이터 마이그레이션 시에는 대상 테이블의 컬럼을 일시적으로 GENERATED BY DEFAULT로 변경하거나, pg_dump 옵션을 활용합니다.

-- 방법 A: 마이그레이션 전 컬럼 정의를 BY DEFAULT로 변경
ALTER TABLE orders
    ALTER COLUMN order_id
    SET GENERATED BY DEFAULT;

-- 데이터 복원 (INSERT 가능)
INSERT INTO orders (order_id, customer_name)
SELECT order_id, customer_name FROM legacy_orders;

-- 마이그레이션 후 다시 ALWAYS로 복원
ALTER TABLE orders
    ALTER COLUMN order_id
    SET GENERATED ALWAYS;

-- 시퀀스 재동기화
SELECT setval(
    pg_get_serial_sequence('orders', 'order_id'),
    (SELECT MAX(order_id) FROM orders),
    true
);
-- 방법 B: pg_dump 사용 시 --column-inserts 및 identity 관련 처리
-- pg_dump 시 identity 컬럼 값 포함 옵션 사용
-- 쉘 명령어 예시 (주석으로 표기)
-- pg_dump --column-inserts --rows-per-insert=1000 -t orders source_db > orders_dump.sql

-- 복원 전 대상 테이블에서 임시 시퀀스 제약 해제
ALTER TABLE orders ALTER COLUMN order_id SET GENERATED BY DEFAULT;
-- psql 로 dump 파일 복원 후
ALTER TABLE orders ALTER COLUMN order_id SET GENERATED ALWAYS;

예방 방법

  • 컬럼 정의 시 GENERATED ALWAYSGENERATED BY DEFAULT의 차이를 명확히 구분하여 설계

실무에서는 외부 시스템과의 연동, 데이터 마이그레이션 가능성, 레거시 코드 호환성을 미리 검토한 후 GENERATED 옵션을 선택해야 합니다. 외부에서 ID 값을 제어해야 하는 상황이 발생할 수 있다면 GENERATED BY DEFAULT AS IDENTITY를 선택하는 것이 더 유연합니다. 반드시 시스템이 값을 독점 관리해야 하는 컬럼(예: 내부 감사 로그 ID)에만 GENERATED ALWAYS를 적용하세요.

  • ORM 및 애플리케이션 레이어에서 Generated 컬럼을 명시적으로 제외 처리

Hibernate, SQLAlchemy, TypeORM 등 ORM을 사용하는 경우, GENERATED ALWAYS 컬럼을 insertable=false, updatable=false 또는 동등한 옵션으로 설정하여 ORM이 해당 컬럼에 값을 삽입하거나 수정하지 않도록 사전에 구성해야 합니다. 코드 리뷰 단계에서 Generated 컬럼에 대한 직접 DML 시도를 정적 분석 도구로 걸러내는 것도 좋은 방법입니다.


관련 에러

  • 42601 (syntax_error): GENERATED ALWAYS AS 표현식의 문법 오류 시 발생합니다.
  • 42P10 (invalid_column_reference): Generated Column의 표현식에서 허용되지 않는 컬럼 참조(예: 다른 테이블 컬럼, 비결정적 함수)를 사용할 때 발생합니다.
  • 55000 (object_not_in_prerequisite_state): 시퀀스 관련 상태 오류로, GENERATED ALWAYS AS IDENTITY 시퀀스가 초기화되지 않았을 때 나타날 수 있습니다.
  • 23505 (unique_violation): OVERRIDING SYSTEM VALUE로 중복 ID를 강제 삽입하거나 시퀀스 재동기화를 잊었을 때 함께 발생할 수 있는 에러입니다.

DBMS 에러 코드 시리즈

주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.

본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.

댓글 남기기