2026년 08월 22일 | DBMS Error 가이드
이 글에서 다루는 내용
22039 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.
22039 sql json array not found 는?
PostgreSQL 에러 코드 22039 (sql_json_array_not_found) 는 SQL/JSON 경로 표현식(JSON Path Expression)을 사용할 때 배열(Array)이 존재할 것으로 기대했지만, 실제로 해당 위치에 배열이 없거나 접근할 수 없는 경우 발생하는 에러입니다. 이 에러는 PostgreSQL 12 버전 이후 도입된 jsonb_path_query, jsonb_path_exists, JSON_TABLE 등 JSON Path 관련 함수 및 SQL/JSON 표준 함수들을 사용할 때 주로 나타납니다. 특히 JSON 데이터 구조가 예상과 다르거나, 외부 시스템에서 유입된 데이터의 형식이 일관되지 않을 때 운영 환경에서 빈번하게 발생합니다.
주요 발생 원인
1. JSON Path 표현식에서 배열 인덱스 접근 시 대상이 배열이 아닌 경우
JSON 데이터 내 특정 키의 값이 배열이 아닌 객체(Object)나 스칼라(Scalar) 값임에도 불구하고, JSON Path 표현식에서 배열 인덱스([], [0] 등)를 사용하여 접근하려 할 때 이 에러가 발생합니다. 예를 들어 $.items[]와 같은 경로를 사용했는데 items 필드가 배열이 아니라 단일 객체인 경우, PostgreSQL은 배열을 찾을 수 없다고 판단하여 에러를 반환합니다. 이는 API 응답 구조가 상황에 따라 단일 객체 혹은 배열로 가변적으로 반환되는 경우 특히 자주 발생합니다.
2. 존재하지 않는 경로에 대한 배열 접근 시도
JSON 데이터에서 존재하지 않는 키를 경유하여 배열에 접근하려고 할 때 발생합니다. 예를 들어 $.orders.items[0]와 같이 중간 경로인 orders 자체가 해당 JSON 데이터에 없는 경우, PostgreSQL은 배열에 도달조차 할 수 없어 이 에러를 발생시킵니다. 데이터가 선택적 필드(Optional Field)를 포함하는 구조일 때, 모든 레코드에 해당 필드가 존재한다고 가정하고 쿼리를 작성하는 것이 실수의 근본 원인이 됩니다.
3. strict 모드에서의 JSON Path 평가
PostgreSQL의 JSON Path는 기본적으로 lax 모드로 동작하지만, 명시적으로 strict 모드를 사용하는 경우 배열과 관련된 규칙이 훨씬 엄격하게 적용됩니다. strict 모드에서는 배열 접근 시 해당 값이 반드시 배열 타입이어야 하며, 그렇지 않으면 즉시 에러가 발생합니다. lax 모드에서는 자동으로 래핑(Wrapping)을 시도하거나 에러 대신 빈 결과를 반환하는 방식으로 유연하게 처리되지만, strict 모드를 사용 중이라면 반드시 데이터 타입을 사전에 검증해야 합니다.
해결 방법
원인 1 해결: 배열 여부 사전 확인 및 lax 모드 활용
배열 접근 전에 해당 필드가 실제로 배열인지 확인하거나, lax 모드를 명시적으로 사용하여 유연하게 처리합니다.
-- 문제가 되는 쿼리 예시 (items가 배열이 아닌 경우 22039 발생 가능)
SELECT jsonb_path_query(data, 'strict $.items[*]')
FROM orders;
-- 해결책 1: lax 모드 사용 (기본값이지만 명시적으로 지정)
SELECT jsonb_path_query(data, 'lax $.items[*]')
FROM orders;
-- 해결책 2: 배열 여부를 먼저 확인
SELECT
id,
CASE
WHEN jsonb_typeof(data -> 'items') = 'array' THEN
jsonb_path_query_array(data, '$.items[*]')
ELSE
jsonb_build_array(data -> 'items') -- 단일 객체를 배열로 감싸기
END AS items_array
FROM orders;
-- 해결책 3: jsonb_path_query_first 사용으로 에러 방지
SELECT jsonb_path_query_first(data, '$.items[0]')
FROM orders
WHERE jsonb_typeof(data -> 'items') = 'array';
원인 2 해결: 경로 존재 여부를 먼저 검증
중간 경로가 존재하는지 확인한 후 배열에 접근합니다.
-- 문제 쿼리: orders 키가 없으면 에러 발생
SELECT jsonb_path_query(data, '$.orders.items[*]')
FROM customer_data;
-- 해결책 1: jsonb_path_exists로 경로 존재 여부 확인 후 접근
SELECT
id,
jsonb_path_query_array(data, '$.orders.items[*]') AS order_items
FROM customer_data
WHERE jsonb_path_exists(data, '$.orders.items[*]');
-- 해결책 2: ? 연산자로 키 존재 여부 확인
SELECT
id,
jsonb_path_query_array(data, '$.orders.items[*]') AS order_items
FROM customer_data
WHERE (data -> 'orders') IS NOT NULL
AND jsonb_typeof(data -> 'orders' -> 'items') = 'array';
-- 해결책 3: COALESCE와 함께 기본값 제공
SELECT
id,
COALESCE(
jsonb_path_query_array(data, 'lax $.orders.items[*]'),
'[]'::jsonb
) AS order_items
FROM customer_data;
원인 3 해결: strict 모드 제거 또는 타입 검증 강화
-- strict 모드에서 발생하는 에러
SELECT jsonb_path_query(data, 'strict $.tags[*]')
FROM products;
-- 해결책 1: strict 대신 lax 모드 사용
SELECT jsonb_path_query(data, 'lax $.tags[*]')
FROM products;
-- 해결책 2: strict 모드가 반드시 필요한 경우, 데이터 타입 검증 선행
SELECT jsonb_path_query(data, 'strict $.tags[*]')
FROM products
WHERE jsonb_typeof(data -> 'tags') = 'array'
AND jsonb_array_length(data -> 'tags') > 0;
-- 해결책 3: JSON_TABLE 사용 시 (PostgreSQL 17+)
SELECT p.id, t.tag_value
FROM products p,
JSON_TABLE(
p.data,
'lax $.tags[*]'
COLUMNS (
tag_value TEXT PATH '$'
)
ERROR ON ERROR
) AS t
WHERE jsonb_typeof(p.data -> 'tags') = 'array';
-- 해결책 4: 애플리케이션 레벨 정규화 함수 생성
CREATE OR REPLACE FUNCTION safe_json_array_extract(
p_data JSONB,
p_path TEXT
)
RETURNS JSONB
LANGUAGE plpgsql
AS $$
DECLARE
v_result JSONB;
BEGIN
BEGIN
v_result := jsonb_path_query_array(p_data, p_path::jsonpath);
EXCEPTION
WHEN sqlstate '22039' THEN
v_result := '[]'::jsonb;
WHEN OTHERS THEN
v_result := '[]'::jsonb;
END;
RETURN v_result;
END;
$$;
-- 함수 사용 예시
SELECT id, safe_json_array_extract(data, '$.items[*]') AS items
FROM orders;
예방 방법
1. JSON 데이터 입력 시 스키마 유효성 검사(Validation) 강제화
데이터가 테이블에 저장될 때부터 구조를 일관되게 유지하도록 CHECK 제약 조건이나 트리거를 활용합니다. 이를 통해 배열이어야 하는 필드가 항상 배열로 저장되도록 보장하면, 조회 시점의 에러를 근본적으로 예방할 수 있습니다.
-- CHECK 제약 조건으로 배열 타입 강제
ALTER TABLE orders
ADD CONSTRAINT chk_items_is_array
CHECK (
data -> 'items' IS NULL
OR jsonb_typeof(data -> 'items') = 'array'
);
-- 트리거를 이용한 데이터 정규화 (배열이 아니면 배열로 변환)
CREATE OR REPLACE FUNCTION normalize_order_items()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.data -> 'items' IS NOT NULL
AND jsonb_typeof(NEW.data -> 'items') != 'array' THEN
NEW.data := jsonb_set(
NEW.data,
'{items}',
jsonb_build_array(NEW.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. JSON Path 쿼리에 항상 방어적 코딩 패턴 적용
운영 쿼리에서는 strict 모드를 피하고, lax 모드와 함께 jsonb_path_exists 또는 jsonb_typeof 검증을 조합하는 방어적 패턴을 팀 코딩 표준으로 정립합니다. 또한 주기적으로 실제 데이터를 샘플링하여 JSON 구조의 일관성을 모니터링하는 배치 작업을 운영하면 예상치 못한 구조 변경을 조기에 발견할 수 있습니다.
-- 정기적인 JSON 구조 일관성 모니터링 쿼리
SELECT
COUNT(*) AS total_rows,
COUNT(*) FILTER (WHERE jsonb_typeof(data -> 'items') = 'array') AS array_type,
COUNT(*) FILTER (WHERE jsonb_typeof(data -> 'items') = 'object') AS object_type,
COUNT(*) FILTER (WHERE jsonb_typeof(data -> 'items') = 'null') AS null_type,
COUNT(*) FILTER (WHERE data -> 'items' IS NULL) AS missing_field
FROM orders;
관련 에러
- 22035 (no_sql_json_item): JSON Path 평가 결과가 아무 항목도 반환하지 않을 때 발생하며, 22039와 함께 JSON Path 쿼리 디버깅 시 자주 마주치게 됩니다.
- 22032 (invalid_json_text): 유효하지 않은 JSON 문자열을 파싱할 때 발생하며, 데이터 입력 단계에서 발생하는 선행 에러입니다.
- 22033 (invalid_sql_json_subscript): JSON 배열 인덱스가 유효하지 않은 경우 발생하는 에러로, 배열 접근 관련 에러 그룹에 속합니다.
- 2203A (sql_json_scalar_required): JSON Path 표현식이 스칼라 값을 기대하지만 배열이나 객체가 반환된 경우 발생하며, 22039와 반대 상황에서 나타나는 에러입니다.
- 22034 (sql_json_array_not_found) 계열의 에러들은 모두 PostgreSQL의 SQL/JSON 표준 구현과 관련된 에러 클래스(Class 22: Data Exception)에 속하므로, 관련 PostgreSQL 공식 문서의 JSON 함수 섹션을 함께 참고하는 것을 권장합니다.
주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.