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

428C9
2026년 07월 08일 | DBMS Error 가이드

이 글에서 다루는 내용

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

428C9 generated always 란?

PostgreSQL 에러 코드 428C9GENERATED ALWAYS 속성으로 정의된 컬럼에 사용자가 직접 값을 삽입하거나 갱신하려 할 때 발생하는 에러입니다. GENERATED ALWAYS AS IDENTITY 또는 GENERATED ALWAYS AS (expression) STORED로 선언된 컬럼은 시스템이 자동으로 값을 생성하기 때문에, 외부에서 명시적으로 값을 지정하면 PostgreSQL이 이를 거부하고 해당 에러를 반환합니다. 이 에러는 주로 레거시 시스템 마이그레이션, ORM 프레임워크 사용 시, 또는 기존 SERIAL 컬럼을 IDENTITY 컬럼으로 전환하는 과정에서 자주 접하게 됩니다.


주요 발생 원인

1. GENERATED ALWAYS AS IDENTITY 컬럼에 직접 값 삽입

가장 흔한 원인입니다. GENERATED ALWAYS AS IDENTITY로 선언된 기본키 컬럼에 INSERT 구문으로 직접 값을 지정하면 에러가 발생합니다. 데이터 마이그레이션이나 백업 복원 작업에서 원본 ID 값을 그대로 유지하려 할 때 이 상황이 빈번하게 발생하며, 개발자들이 SERIALIDENTITY의 차이를 간과하는 경우가 많습니다.

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

-- 아래 INSERT 구문은 428C9 에러를 발생시킵니다
INSERT INTO orders (order_id, product_name, amount)
VALUES (100, 'Laptop', 1500.00);
-- ERROR:  cannot insert into column "order_id"
-- DETAIL:  Column "order_id" is an identity column defined as GENERATED ALWAYS.

2. GENERATED ALWAYS AS (expression) STORED 컬럼 수정 시도

GENERATED ALWAYS AS (expression) STORED로 정의된 계산(Generated) 컬럼에 UPDATE 또는 INSERT로 값을 강제 지정하려 할 때 발생합니다. 이 컬럼은 정의된 표현식에 의해 자동으로 값이 계산되어 저장되므로, 사용자가 임의로 값을 변경하는 것이 근본적으로 불가능합니다. 특히 Hibernate, JPA 등의 ORM이 자동 생성 쿼리에 해당 컬럼을 포함시킬 때 자주 문제가 됩니다.

-- 에러 발생 예시
CREATE TABLE products (
    product_id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    price NUMERIC,
    tax_rate NUMERIC DEFAULT 0.1,
    price_with_tax NUMERIC GENERATED ALWAYS AS (price * (1 + tax_rate)) STORED
);

-- 아래 UPDATE 구문은 428C9 에러를 발생시킵니다
UPDATE products
SET price_with_tax = 1650.00
WHERE product_id = 1;
-- ERROR:  column "price_with_tax" can only be updated to DEFAULT

3. 데이터 마이그레이션 또는 덤프/복원 과정에서의 충돌

pg_dump로 생성된 덤프 파일을 복원하거나, 다른 데이터베이스에서 데이터를 이관할 때 기존 ID 값을 유지하려는 경우 에러가 발생합니다. 특히 SERIAL 기반 테이블을 GENERATED ALWAYS AS IDENTITY로 변경한 후 기존 방식으로 데이터를 삽입하면 이 에러가 반드시 발생하며, 운영 환경에서 예상치 못한 장애를 일으키는 주요 원인이 됩니다.

-- 마이그레이션 시나리오 에러 예시
-- 기존 SERIAL 방식에서는 정상 동작
CREATE TABLE old_users (
    user_id SERIAL PRIMARY KEY,
    username TEXT
);
INSERT INTO old_users (user_id, username) VALUES (1, 'alice'); -- 정상

-- IDENTITY로 변경 후 동일 방식 시도 시 에러
CREATE TABLE new_users (
    user_id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    username TEXT
);
INSERT INTO new_users (user_id, username) VALUES (1, 'alice'); -- 428C9 에러

해결 방법

해결책 1: OVERRIDING SYSTEM VALUE 사용

데이터 마이그레이션 등 불가피하게 특정 값을 직접 삽입해야 할 때는 OVERRIDING SYSTEM VALUE 옵션을 사용합니다. 이 방법은 PostgreSQL 10 이상에서 지원되며, 시스템 생성 값을 일시적으로 무시하고 지정한 값을 삽입할 수 있게 해줍니다.

-- OVERRIDING SYSTEM VALUE를 사용한 직접 삽입
INSERT INTO orders (order_id, product_name, amount)
OVERRIDING SYSTEM VALUE
VALUES (100, 'Laptop', 1500.00);

-- 마이그레이션 완료 후 시퀀스 값을 최신 상태로 재설정
SELECT setval(
    pg_get_serial_sequence('orders', 'order_id'),
    (SELECT MAX(order_id) FROM orders)
);

해결책 2: GENERATED BY DEFAULT 로 컬럼 재정의

마이그레이션이 잦거나 외부 시스템과의 연동이 많은 경우, GENERATED ALWAYS 대신 GENERATED BY DEFAULT AS IDENTITY를 사용하면 사용자가 값을 직접 지정할 수도 있고 지정하지 않으면 자동 생성됩니다.

-- 기존 GENERATED ALWAYS 컬럼 변경
ALTER TABLE orders
ALTER COLUMN order_id
SET GENERATED BY DEFAULT;

-- 이제 직접 값 삽입 가능
INSERT INTO orders (order_id, product_name, amount)
VALUES (200, 'Monitor', 800.00);

-- 또는 자동 생성도 가능
INSERT INTO orders (product_name, amount)
VALUES ('Keyboard', 150.00);

해결책 3: Generated 컬럼 우회 (INSERT/UPDATE 시 제외)

GENERATED ALWAYS AS (expression) STORED 컬럼의 경우, INSERT/UPDATE 구문에서 해당 컬럼을 완전히 제외하거나 DEFAULT 키워드를 사용해야 합니다.

-- 잘못된 방법 (428C9 에러 발생)
INSERT INTO products (product_id, price, tax_rate, price_with_tax)
VALUES (DEFAULT, 1500.00, 0.1, 1650.00);

-- 올바른 방법 1: 컬럼 목록에서 제외
INSERT INTO products (price, tax_rate)
VALUES (1500.00, 0.1);

-- 올바른 방법 2: DEFAULT 키워드 사용
INSERT INTO products (price, tax_rate, price_with_tax)
VALUES (1500.00, 0.1, DEFAULT);

-- UPDATE 시에도 동일하게 적용
UPDATE products
SET price = 2000.00  -- price_with_tax는 자동으로 재계산됨
WHERE product_id = 1;

해결책 4: ORM 설정 수정 (JPA/Hibernate 예시)

ORM을 사용하는 경우, 엔티티 클래스에서 해당 컬럼을 insertable=false, updatable=false로 설정하여 ORM이 자동 생성 쿼리에 해당 컬럼을 포함시키지 않도록 합니다.

-- ORM 문제 진단 쿼리: GENERATED 컬럼 조회
SELECT
    column_name,
    identity_generation,
    is_generated,
    generation_expression
FROM information_schema.columns
WHERE table_name = 'orders'
  AND table_schema = 'public'
  AND (identity_generation IS NOT NULL OR is_generated = 'ALWAYS');

예방 방법

1. 테이블 설계 시 GENERATED 컬럼 전략 명확히 수립

신규 테이블 설계 단계에서 외부 시스템과의 데이터 교환이 예상되거나, 마이그레이션 가능성이 있는 테이블에는 처음부터 GENERATED BY DEFAULT AS IDENTITY를 사용하는 것을 표준으로 정립하세요. 내부 자동 채번만으로 충분한 경우에만 GENERATED ALWAYS AS IDENTITY를 선택하고, 팀 내 코딩 컨벤션 문서에 해당 내용을 명시적으로 기록해 두는 것이 중요합니다. 또한 신규 개발자가 합류할 때 이 차이를 온보딩 교육에 반드시 포함시켜야 합니다.

2. 개발/스테이징 환경에서 마이그레이션 스크립트 사전 검증

운영 환경 배포 전에 반드시 동일한 스키마 구조의 스테이징 환경에서 전체 마이그레이션 스크립트를 테스트하세요. 특히 pg_dump/pg_restore 작업 시 --disable-triggers 옵션이나 시퀀스 재설정 로직이 포함되어 있는지 체크리스트를 만들어 관리하면 운영 장애를 크게 줄일 수 있습니다. CI/CD 파이프라인에 마이그레이션 검증 단계를 자동화하는 것도 좋은 방법입니다.


관련 에러

  • 42601 (syntax_error): GENERATED ALWAYS AS 표현식 작성 시 문법 오류가 있을 때 발생합니다.
  • 42P10 (invalid_column_reference): Generated 컬럼 표현식 내에서 다른 Generated 컬럼을 참조하려 할 때 발생합니다.
  • 0A000 (feature_not_supported): Generated 컬럼에서 지원되지 않는 함수나 비결정적(non-deterministic) 함수를 사용할 때 발생합니다. now(), random() 등 가변 함수는 Generated 컬럼 표현식에 사용할 수 없습니다.
  • 55000 (object_not_in_prerequisite_state): 시퀀스 관련 작업에서 상태 불일치가 발생할 때 나타나며, OVERRIDING SYSTEM VALUE 사용 후 시퀀스 재설정을 누락했을 때 간접적으로 연관될 수 있습니다.

DBMS 에러 코드 시리즈

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

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

댓글 남기기