PostgreSQL 2203C 오류 원인과 해결 방법 완벽 가이드

2203C
2026년 06월 19일 | DBMS Error 가이드

이 글에서 다루는 내용

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

2203C sql json object not found 는?

PostgreSQL 에러 코드 2203Csql_json_object_not_found 에러로, SQL/JSON 경로 표현식(Path Expression)을 사용할 때 지정한 경로에 해당하는 JSON 객체나 값이 존재하지 않을 때 발생합니다. 이 에러는 주로 jsonb_path_query, jsonb_path_value, JSON_VALUE, JSON_QUERY, JSON_TABLE 등의 SQL/JSON 함수에서 ERROR ON EMPTY 옵션을 명시적으로 지정했거나 기본 동작이 에러를 발생시키도록 설정된 경우에 나타납니다. PostgreSQL 14 이후 도입된 SQL 표준 기반 JSON 함수들이 확산되면서 이 에러를 접하는 DBA와 개발자가 크게 늘어났으며, 데이터 품질 문제나 잘못된 경로 표현식 작성이 주요 원인으로 꼽힙니다.


주요 발생 원인

1. JSON_VALUE / JSON_QUERY에서 존재하지 않는 경로 참조 시 ERROR ON EMPTY 동작

PostgreSQL 14+에서 도입된 JSON_VALUE(), JSON_QUERY() 함수는 기본적으로 경로가 존재하지 않으면 NULL을 반환하지만, ERROR ON EMPTY 옵션을 명시적으로 지정하면 2203C 에러를 발생시킵니다. 실무에서 데이터 검증 목적으로 ERROR ON EMPTY를 사용하는 경우가 많은데, 입력 JSON 데이터의 구조가 일정하지 않을 때 예상치 못한 에러가 발생할 수 있습니다. 특히 외부 API로부터 수신한 JSON 데이터를 처리하거나, 여러 버전의 JSON 스키마가 혼재하는 환경에서 자주 나타납니다.

2. jsonb_path_query_first 또는 jsonb_path_value 함수에서 잘못된 경로 표현식 사용

jsonb_path_value() 함수는 경로 표현식이 정확히 하나의 결과를 반환해야 하며, 결과가 없을 경우 기본적으로 NULL을 반환하지만 엄격 모드(strict mode)에서는 에러를 발생시킵니다. JSON 경로 표현식에서 strict 키워드를 사용하면 존재하지 않는 키나 배열 인덱스 접근 시 에러가 발생하며, lax 모드와 달리 누락된 구조적 요소를 자동으로 처리하지 않습니다. 경로 표현식의 대소문자 오류나 오타도 이 에러를 유발하는 흔한 원인입니다.

3. 중첩된 JSON 구조에서 배열 내 특정 조건을 만족하는 요소가 없는 경우

복잡한 중첩 JSON 구조에서 필터 조건(? 연산자)을 사용하여 배열 내 특정 요소를 검색할 때, 조건을 만족하는 요소가 전혀 없으면 빈 결과셋이 반환됩니다. 이 상황에서 ERROR ON EMPTY 옵션이나 엄격 경로 모드가 활성화되어 있으면 2203C 에러가 발생합니다. 데이터베이스에 저장된 JSON 데이터의 내용이 시간이 지남에 따라 변화하거나, 특정 레코드에만 해당 필드가 존재하는 경우 배치 처리나 대량 쿼리 실행 시 간헐적으로 발생하는 경향이 있습니다.


해결 방법

원인 1 해결: ERROR ON EMPTY 대신 DEFAULT 또는 NULL ON EMPTY 사용

ERROR ON EMPTY 대신 NULL ON EMPTY 또는 DEFAULT 값 ON EMPTY를 사용하여 경로가 없을 때 에러 대신 기본값을 반환하도록 변경합니다.

-- 문제가 되는 쿼리 (ERROR ON EMPTY 명시)
SELECT JSON_VALUE(
    '{"name": "PostgreSQL"}'::json,
    '$.version' ERROR ON EMPTY
);
-- ERROR: SQL/JSON object not found (SQLSTATE 2203C)

-- 해결책 1: NULL ON EMPTY 사용
SELECT JSON_VALUE(
    '{"name": "PostgreSQL"}'::json,
    '$.version' NULL ON EMPTY
);
-- 결과: NULL (에러 없음)

-- 해결책 2: DEFAULT 값 지정
SELECT JSON_VALUE(
    '{"name": "PostgreSQL"}'::json,
    '$.version' DEFAULT 'unknown' ON EMPTY
);
-- 결과: 'unknown'

-- 실무 예제: 테이블에서 안전하게 JSON 값 추출
SELECT
    id,
    JSON_VALUE(
        data,
        '$.user.email'
        DEFAULT 'no-email@example.com' ON EMPTY
        NULL ON ERROR
    ) AS user_email
FROM user_events
WHERE created_at >= NOW() - INTERVAL '7 days';

원인 2 해결: strict 모드를 lax 모드로 변경

경로 표현식에서 strict 키워드를 제거하거나 lax로 변경하면, 존재하지 않는 경로를 보다 유연하게 처리할 수 있습니다.

-- 문제가 되는 쿼리 (strict 모드)
SELECT jsonb_path_query(
    '{"product": {"name": "Laptop"}}'::jsonb,
    'strict $.product.price'
);
-- ERROR: SQL/JSON object not found (SQLSTATE 2203C)

-- 해결책: lax 모드 사용 (기본값)
SELECT jsonb_path_query(
    '{"product": {"name": "Laptop"}}'::jsonb,
    'lax $.product.price'
);
-- 결과: (no rows) -- 에러 없이 빈 결과 반환

-- jsonb_path_query_first를 활용한 안전한 추출
SELECT
    id,
    COALESCE(
        jsonb_path_query_first(
            data,
            'lax $.product.price'
        )::text::numeric,
        0
    ) AS price
FROM products;

-- jsonb -> 연산자와 비교 (NULL 안전 처리)
SELECT
    id,
    (data -> 'product' ->> 'price')::numeric AS price
FROM products
WHERE data -> 'product' ? 'price';  -- 키 존재 여부 먼저 확인

원인 3 해결: 필터 조건 사용 시 존재 여부 사전 확인

배열 필터링 전에 해당 조건을 만족하는 요소가 존재하는지 먼저 확인하는 방어적 쿼리를 작성합니다.

-- 문제가 되는 쿼리
SELECT JSON_QUERY(
    '{"orders": [{"id": 1, "status": "pending"}]}'::json,
    '$.orders[*] ? (@.status == "completed")' ERROR ON EMPTY
);
-- ERROR: SQL/JSON object not found (SQLSTATE 2203C)

-- 해결책 1: NULL ON EMPTY 처리
SELECT JSON_QUERY(
    '{"orders": [{"id": 1, "status": "pending"}]}'::json,
    '$.orders[*] ? (@.status == "completed")' NULL ON EMPTY
);
-- 결과: NULL

-- 해결책 2: jsonb_path_exists로 사전 확인 후 처리
SELECT
    id,
    CASE
        WHEN jsonb_path_exists(data, '$.orders[*] ? (@.status == "completed")')
        THEN jsonb_path_query_array(data, '$.orders[*] ? (@.status == "completed")')
        ELSE '[]'::jsonb
    END AS completed_orders
FROM customer_data;

-- 해결책 3: 실무 배치 처리 패턴
DO $$
DECLARE
    v_record RECORD;
    v_result jsonb;
BEGIN
    FOR v_record IN SELECT id, data FROM customer_data LOOP
        BEGIN
            v_result := jsonb_path_query_first(
                v_record.data,
                'lax $.orders[*] ? (@.status == "completed")'
            );
            -- v_result가 NULL이면 처리 스킵
            IF v_result IS NOT NULL THEN
                -- 비즈니스 로직 처리
                RAISE NOTICE 'Customer % has completed orders', v_record.id;
            END IF;
        EXCEPTION
            WHEN sqlstate '2203C' THEN
                RAISE WARNING 'JSON path not found for record id: %', v_record.id;
        END;
    END LOOP;
END;
$$;

예방 방법

1. JSON 스키마 유효성 검사 함수 도입 및 입력 시점 검증

JSON 데이터를 테이블에 저장하기 전에 필수 필드가 존재하는지 CHECK 제약 조건이나 트리거를 통해 검증하는 것이 가장 효과적인 예방책입니다. 이를 통해 불완전한 JSON 데이터가 데이터베이스에 저장되는 것을 원천 차단하고, 이후 조회 시 발생할 수 있는 2203C 에러를 사전에 방지할 수 있습니다.

-- CHECK 제약 조건으로 필수 필드 보장
ALTER TABLE user_events
ADD CONSTRAINT chk_user_events_required_fields
CHECK (
    jsonb_path_exists(data, '$.user.id') AND
    jsonb_path_exists(data, '$.event_type') AND
    jsonb_path_exists(data, '$.timestamp')
);

-- 또는 트리거를 활용한 검증
CREATE OR REPLACE FUNCTION validate_event_json()
RETURNS TRIGGER AS $$
BEGIN
    IF NOT jsonb_path_exists(NEW.data, '$.user.id') THEN
        RAISE EXCEPTION 'Missing required field: user.id in JSON data';
    END IF;
    IF NOT jsonb_path_exists(NEW.data, '$.event_type') THEN
        RAISE EXCEPTION 'Missing required field: event_type in JSON data';
    END IF;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_validate_event_json
BEFORE INSERT OR UPDATE ON user_events
FOR EACH ROW EXECUTE FUNCTION validate_event_json();

2. 래퍼 함수 생성을 통한 안전한 JSON 경로 접근 패턴 표준화

프로젝트 전체에서 JSON 경로 접근 시 일관된 안전 처리를 보장하기 위해 팀 내 공통 래퍼 함수를 만들어 사용하는 것을 권장합니다. 이렇게 하면 개별 개발자가 매번 NULL ON EMPTY 처리를 신경 쓰지 않아도 되며, 에러 처리 로직이 한 곳에 집중되어 유지보수성이 크게 향상됩니다.

-- 안전한 JSON 값 추출 래퍼 함수
CREATE OR REPLACE FUNCTION safe_json_value(
    p_json jsonb,
    p_path text,
    p_default text DEFAULT NULL
)
RETURNS text AS $$
BEGIN
    RETURN COALESCE(
        jsonb_path_query_first(p_json, p_path::jsonpath)::text,
        p_default
    );
EXCEPTION
    WHEN OTHERS THEN
        RETURN p_default;
END;
$$ LANGUAGE plpgsql IMMUTABLE;

-- 사용 예시
SELECT
    id,
    safe_json_value(data, '$.user.name', 'Anonymous') AS username,
    safe_json_value(data, '$.user.email', 'N/A') AS email,
    safe_json_value(data, '$.metadata.version', '1.0') AS version
FROM user_events;

관련 에러

  • 2203F (sql_json_array_not_found): JSON 경로 표현식이 배열을 기대하지만 해당 위치에 배열이 없을 때 발생하며, 2203C와 유사한 상황에서 함께 나타나는 경우가 많습니다.
  • 2203G (sql_json_scalar_required): JSON_VALUE 함수가 스칼라 값을 기대하지만 배열이나 객체가 반환될 때 발생합니다.
  • 2203W (sql_json_item_cannot_be_cast_to_target_type): JSON 경로로 찾은 값을 지정된 타입으로 캐스팅할 수 없을 때 발생하며, 2203C 이후 타입 변환 단계에서 추가로 마주칠 수 있는 에러입니다.
  • 22032 (invalid_json_text): JSON 데이터 자체가 유효하지 않을 때 발생하며, 데이터 품질 문제의 근본 원인이 2203C로 이어지는 경우도 있습니다.

DBMS 에러 코드 시리즈

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

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

댓글 남기기