2026년 10월 26일 | DBMS Error 가이드
이 글에서 다루는 내용
22039 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.
22039 sql json array not found 는?
PostgreSQL 에러 코드 22039는 SQL/JSON 경로 표현식(Path Expression)을 사용하여 JSON 데이터를 조회할 때, 해당 경로가 배열(Array)을 가리켜야 하는데 실제 데이터에서 배열을 찾지 못했을 경우 발생하는 에러입니다. 주로 jsonb_path_query, jsonb_path_exists, JSON_TABLE, JSON_QUERY 등의 SQL/JSON 함수를 사용할 때 나타나며, 특히 strict 모드에서 경로 결과가 배열이 아닌 경우 또는 배열 자체가 존재하지 않는 경우 이 에러가 발생합니다. PostgreSQL 12 이후 도입된 SQL/JSON 표준 기능을 사용하는 환경에서 자주 접할 수 있으며, JSON 데이터 구조가 일관되지 않은 실무 환경에서 특히 주의가 필요합니다.
주요 발생 원인
1. strict 모드에서 배열이 아닌 값에 배열 접근 시도
SQL/JSON 경로 표현식에서 strict 키워드를 사용하면, 경로 탐색 도중 예상한 구조와 실제 데이터 구조가 다를 경우 즉시 에러를 발생시킵니다. 예를 들어 배열 인덱스([*], [0])를 경로에 포함했는데 실제 해당 키의 값이 배열이 아닌 단일 객체나 스칼라 값일 때 22039 에러가 발생합니다. lax 모드와 달리 strict 모드는 자동 래핑(Auto-wrapping) 기능이 없으므로, 데이터 구조에 대한 엄격한 검증이 필요한 상황에서 이 에러를 자주 마주치게 됩니다.
2. JSON_TABLE 또는 JSON_QUERY에서 경로 표현식이 배열을 반환하지 않는 경우
JSON_TABLE 함수는 내부적으로 지정한 경로가 배열이나 반복 가능한 값을 반환한다고 가정합니다. 만약 해당 경로가 단일 객체를 반환하거나, 경로 자체가 존재하지 않는다면 PostgreSQL은 22039 에러를 발생시킵니다. 실무에서 API 응답 데이터를 JSON 형태로 저장하고 이를 JSON_TABLE로 파싱할 때, 일부 레코드에서 특정 키가 배열 대신 null이나 단일 값으로 저장되어 있는 경우가 빈번하므로 주의해야 합니다.
3. 동적으로 생성된 JSON 데이터의 구조 불일치
외부 시스템(REST API, 모바일 앱, 레거시 시스템 등)에서 유입되는 JSON 데이터는 항상 동일한 구조를 보장하지 않습니다. 어떤 레코드에서는 특정 필드가 배열([])로 존재하지만, 다른 레코드에서는 해당 필드 자체가 없거나 단일 오브젝트로 저장되어 있을 수 있습니다. 이런 비정형 데이터를 엄격한 경로 표현식으로 일괄 조회하면 일부 행에서 22039 에러가 발생하여 전체 쿼리가 실패하는 상황이 발생합니다.
해결 방법
원인 1 해결: strict 모드를 lax 모드로 전환
strict 모드 대신 lax 모드를 사용하면, 배열이 아닌 값에 배열 접근자를 적용해도 PostgreSQL이 자동으로 값을 배열로 래핑하거나 빈 결과를 반환합니다.
-- 문제가 발생하는 strict 모드 예시
SELECT jsonb_path_query(
'{"user": {"name": "Alice"}}'::jsonb,
'strict $.user[*]'
);
-- ERROR: SQL/JSON array not found (SQLSTATE 22039)
-- lax 모드로 변경하여 해결
SELECT jsonb_path_query(
'{"user": {"name": "Alice"}}'::jsonb,
'lax $.user[*]'
);
-- 결과: {"name": "Alice"} (자동 래핑되어 정상 반환)
-- jsonb_path_query_array로 안전하게 배열 반환
SELECT jsonb_path_query_array(
'{"items": {"id": 1, "name": "test"}}'::jsonb,
'lax $.items[*]'
);
원인 2 해결: JSON_TABLE 사용 시 경로 검증 및 기본값 처리
JSON_TABLE에서 경로가 배열을 반환하지 않을 때를 대비하여, 데이터를 전처리하거나 조건부 처리를 적용합니다.
-- 문제 상황: items가 배열이 아닌 경우
SELECT *
FROM orders,
JSON_TABLE(
order_data,
'$.items[*]'
COLUMNS (
item_id INT PATH '$.id',
item_name TEXT PATH '$.name'
)
) AS jt;
-- 일부 행에서 22039 에러 발생 가능
-- 해결 방법 1: CASE로 배열 여부를 사전 검증 후 처리
SELECT o.order_id,
jt.item_id,
jt.item_name
FROM orders o
CROSS JOIN LATERAL (
SELECT *
FROM JSON_TABLE(
CASE
WHEN jsonb_typeof(o.order_data -> 'items') = 'array'
THEN o.order_data
ELSE jsonb_set(o.order_data, '{items}',
to_jsonb(ARRAY[o.order_data -> 'items']))
END,
'$.items[*]'
COLUMNS (
item_id INT PATH '$.id',
item_name TEXT PATH '$.name'
)
) AS jt
) AS jt;
-- 해결 방법 2: jsonb_typeof로 배열 여부 필터링
SELECT
order_id,
elem -> 'id' AS item_id,
elem ->> 'name' AS item_name
FROM orders,
jsonb_array_elements(
CASE
WHEN jsonb_typeof(order_data -> 'items') = 'array'
THEN order_data -> 'items'
ELSE '[]'::jsonb
END
) AS elem;
원인 3 해결: 데이터 정규화 및 방어적 쿼리 작성
비정형 JSON 데이터를 다룰 때는 jsonb_typeof, jsonb_path_exists, COALESCE 등을 조합하여 방어적으로 쿼리를 작성합니다.
-- 배열 여부를 미리 확인하는 방어적 쿼리
SELECT
id,
CASE
WHEN jsonb_typeof(payload -> 'tags') = 'array'
THEN jsonb_path_query_array(payload, '$.tags[*]')
WHEN (payload -> 'tags') IS NOT NULL
THEN jsonb_build_array(payload -> 'tags')
ELSE '[]'::jsonb
END AS tags
FROM events;
-- jsonb_path_query_first로 안전하게 첫 번째 요소만 조회
SELECT
id,
jsonb_path_query_first(
payload,
'lax $.items[0]'
) AS first_item
FROM orders
WHERE payload IS NOT NULL;
-- 배열 존재 여부를 사전에 확인하는 CTE 활용
WITH validated AS (
SELECT
id,
payload,
jsonb_path_exists(payload, '$.items[*]') AS has_items
FROM raw_data
)
SELECT
id,
jsonb_array_elements(payload -> 'items') AS item
FROM validated
WHERE has_items = true;
예방 방법
1. JSON 스키마 유효성 검사 및 데이터 적재 시점 정규화
JSON 데이터를 테이블에 삽입하기 전, 트리거(Trigger) 또는 CHECK 제약 조건을 활용하여 필수 필드가 항상 배열 타입으로 저장되도록 강제합니다. 이렇게 하면 이후 조회 시 구조 불일치로 인한 에러를 원천 차단할 수 있습니다.
-- CHECK 제약으로 items 필드가 항상 배열임을 보장
ALTER TABLE orders
ADD CONSTRAINT chk_items_is_array
CHECK (
order_data -> 'items' IS NULL
OR jsonb_typeof(order_data -> 'items') = 'array'
);
-- 삽입 트리거로 단일 객체를 자동으로 배열로 변환
CREATE OR REPLACE FUNCTION normalize_order_items()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.order_data ? 'items'
AND jsonb_typeof(NEW.order_data -> 'items') <> 'array' THEN
NEW.order_data := jsonb_set(
NEW.order_data,
'{items}',
jsonb_build_array(NEW.order_data -> 'items')
);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_normalize_order_items
BEFORE INSERT OR UPDATE ON orders
FOR EACH ROW EXECUTE FUNCTION normalize_order_items();
2. SQL/JSON 함수 사용 시 lax 모드와 오류 처리 함수를 기본으로 사용
프로덕션 쿼리에서는 가능하면 lax 모드를 기본으로 사용하고, jsonb_path_query 대신 결과가 없을 때 NULL을 반환하는 jsonb_path_query_first나, 오류 발생 시 기본값을 반환하는 패턴을 표준으로 채택합니다. 팀 내 SQL 코딩 컨벤션에 이 규칙을 명문화하면 유사한 에러의 재발을 효과적으로 방지할 수 있습니다.
-- 권장 패턴: lax 모드 + NULL 안전 처리
SELECT
id,
COALESCE(
jsonb_path_query_array(payload, 'lax $.tags[*]'),
'[]'::jsonb
) AS tags,
jsonb_path_query_first(payload, 'lax $.meta.version') AS version
FROM documents;
관련 에러
22032(invalid_json_text): JSON 문자열 자체가 유효하지 않은 형식일 때 발생합니다. JSON 파싱 단계에서 실패하므로22039보다 앞서 발생합니다.22033(impossible_power) /22034(invalid_argument_for_sql_json_datetime_function): SQL/JSON 함수의 인자 처리 중 발생하는 관련 에러 계열입니다.22035(no_sql_json_item): 경로 표현식이 어떤 값도 반환하지 못할 때 발생하며,22039와 함께 strict 모드에서 자주 쌍으로 나타납니다.22038(singleton_sql_json_item_required): SQL/JSON 경로 결과가 단일 값이어야 하는데 여러 값이 반환될 때 발생합니다.22039와 반대 방향의 구조 불일치 에러입니다.
주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.