2026년 10월 26일 | DBMS Error 가이드
이 글에서 다루는 내용
22038 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.
22038 singleton sql json item required 는?
PostgreSQL 에러 코드 22038 (singleton sql json item required)은 SQL/JSON 경로 표현식(JSON Path Expression)이 단일 값(singleton)을 반환해야 하는 컨텍스트에서 여러 개의 값이나 배열을 반환할 때 발생하는 에러입니다. 주로 JSON_VALUE(), JSON_QUERY() 등의 SQL/JSON 함수가 경로 표현식 평가 결과로 단 하나의 JSON 아이템만을 기대하는 상황에서, 실제로 복수의 결과가 나올 때 이 에러가 트리거됩니다. PostgreSQL 15 이상 버전에서 SQL 표준 JSON 함수들이 도입되면서 실무 환경에서 이 에러를 접하는 빈도가 점차 높아지고 있습니다.
주요 발생 원인
1. JSON_VALUE() 함수에서 배열 경로가 여러 값을 반환하는 경우
JSON_VALUE() 함수는 반드시 하나의 스칼라 값만 반환해야 합니다. JSON 데이터 내에 배열이 포함되어 있고, 경로 표현식이 해당 배열의 모든 요소를 참조하게 되면 함수는 단일 값을 특정할 수 없어 22038 에러를 발생시킵니다. 예를 들어 $.items[*].name처럼 와일드카드를 사용하는 경우가 대표적인 원인입니다.
2. JSON_QUERY() 또는 JSON_VALUE()에서 WITHOUT WRAPPER 옵션과 배열 반환 충돌
JSON_QUERY() 함수에서 WITHOUT ARRAY WRAPPER 옵션을 명시하거나 기본값으로 두었을 때, 경로 표현식이 배열 혹은 복수의 JSON 아이템을 반환하면 이 에러가 발생합니다. 반환 값이 단일 오브젝트나 스칼라가 아닌 경우, PostgreSQL은 래퍼 없이 결과를 단일 아이템으로 처리할 수 없다고 판단하여 에러를 냅니다.
3. @? 또는 @@ 연산자와 함께 사용하는 JSON Path에서 의도치 않은 다중 결과 반환
SQL/JSON Path 연산자인 @?나 @@을 활용할 때, 내부적으로 단일 결과를 기대하는 함수나 컨텍스트에 중첩하여 사용하는 경우에도 이 에러가 발생할 수 있습니다. 특히 서브쿼리나 CTE 내에서 JSON Path 결과를 단일 컬럼으로 반환받으려 할 때, 경로가 복수의 매칭 결과를 갖는 상황이 발생하면 22038 에러로 이어집니다.
해결 방법
원인 1 해결: JSON_VALUE()에서 특정 인덱스 지정
와일드카드 대신 명확한 배열 인덱스를 사용하거나, RETURNING 절과 함께 처리 방식을 명시합니다.
-- 에러 발생 예시
SELECT JSON_VALUE('{"items":[{"name":"Alice"},{"name":"Bob"}]}', '$.items[*].name');
-- ERROR: 22038: singleton SQL/JSON item required
-- 해결 방법 1: 특정 인덱스 지정
SELECT JSON_VALUE('{"items":[{"name":"Alice"},{"name":"Bob"}]}', '$.items[0].name');
-- 결과: Alice
-- 해결 방법 2: DEFAULT 옵션으로 에러 대신 기본값 반환
SELECT JSON_VALUE(
'{"items":[{"name":"Alice"},{"name":"Bob"}]}',
'$.items[*].name'
DEFAULT 'multiple values' ON ERROR
);
-- 결과: multiple values
원인 2 해결: JSON_QUERY()에서 WITH WRAPPER 옵션 사용
복수의 JSON 아이템을 반환해야 하는 경우, WITH ARRAY WRAPPER 옵션을 명시적으로 지정하여 결과를 배열로 감싸서 반환받습니다.
-- 에러 발생 예시
SELECT JSON_QUERY(
'{"tags":["postgresql","database","json"]}',
'$.tags[*]'
);
-- ERROR: 22038: singleton SQL/JSON item required
-- 해결 방법: WITH ARRAY WRAPPER 사용
SELECT JSON_QUERY(
'{"tags":["postgresql","database","json"]}',
'$.tags[*]' WITH ARRAY WRAPPER
);
-- 결과: ["postgresql","database","json"]
-- 조건부 래핑: 단일 값이면 그대로, 복수면 배열로
SELECT JSON_QUERY(
'{"tags":["postgresql","database","json"]}',
'$.tags[*]' WITH CONDITIONAL ARRAY WRAPPER
);
원인 3 해결: jsonb_path_query_first() 또는 jsonb_array_elements() 활용
단일 값이 필요하지만 경로 결과가 복수일 수 있는 경우, PostgreSQL 고유 함수인 jsonb_path_query_first()를 활용하거나 jsonb_array_elements()로 행을 분리하여 처리합니다.
-- 테스트 데이터 준비
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
data JSONB
);
INSERT INTO orders (data) VALUES
('{"customer":"홍길동","items":[{"product":"노트북","price":1500000},{"product":"마우스","price":30000}]}'),
('{"customer":"김철수","items":[{"product":"키보드","price":80000}]}');
-- 에러 발생 가능 패턴: JSON_VALUE로 여러 price를 한번에 추출 시도
-- 해결 방법 1: jsonb_path_query_first() - 첫 번째 결과만 반환
SELECT
data->>'customer' AS customer,
jsonb_path_query_first(data, '$.items[*].price') AS first_price
FROM orders;
-- 해결 방법 2: jsonb_path_query() - 모든 결과를 행으로 반환
SELECT
data->>'customer' AS customer,
jsonb_path_query(data, '$.items[*].product') AS product,
jsonb_path_query(data, '$.items[*].price') AS price
FROM orders;
-- 해결 방법 3: jsonb_array_elements()로 배열 분해 후 처리
SELECT
o.data->>'customer' AS customer,
item->>'product' AS product,
(item->>'price')::NUMERIC AS price
FROM orders o,
jsonb_array_elements(o.data->'items') AS item;
ON ERROR 절을 활용한 방어적 쿼리 작성
-- NULL 반환으로 에러 무시
SELECT JSON_VALUE(
'{"scores":[95, 87, 92]}',
'$.scores[*]'
NULL ON ERROR
) AS score;
-- 에러 로깅을 위한 DEFAULT 값 활용
SELECT
id,
JSON_VALUE(
data::text,
'$.items[*].name'
DEFAULT '(multiple items)' ON ERROR
) AS item_name
FROM orders;
예방 방법
1. JSON Path 작성 시 반환 결과 건수를 사전에 검증하는 습관 들이기
SQL/JSON 함수를 실제 쿼리에 적용하기 전에 jsonb_path_query()나 jsonb_path_query_array()를 사용하여 경로 표현식이 반환하는 결과의 개수와 형태를 먼저 확인하는 것이 중요합니다. 특히 외부에서 유입되는 JSON 데이터나 스키마가 고정되지 않은 반정형 데이터를 다룰 때는, jsonb_path_query_first()와 jsonb_typeof()를 조합하여 데이터 타입과 단일성을 검증한 뒤 처리하는 패턴을 코딩 컨벤션으로 확립하세요.
-- 사전 검증 패턴 예시
SELECT
CASE
WHEN jsonb_array_length(jsonb_path_query_array(data, '$.items[*]')) = 1
THEN JSON_VALUE(data::text, '$.items[0].name')
ELSE '복수 아이템 존재'
END AS safe_item_name
FROM orders;
2. 애플리케이션 레이어와 DB 레이어에서 JSON 스키마 계약(Contract) 명확히 정의하기
JSON 컬럼에 저장되는 데이터의 구조를 PostgreSQL의 CHECK 제약조건과 JSON Schema(jsonb_matches_schema(), PostgreSQL 16+)를 활용하여 입력 시점에 유효성을 검증하면, 런타임에 22038 에러가 발생하는 상황을 원천적으로 줄일 수 있습니다. 또한 ORM이나 API 계층에서도 JSON 필드의 배열 여부와 단일 값 여부를 명확히 타입으로 선언하고, DB와의 인터페이스 계약을 문서화하여 팀 전체가 공유하는 것이 장기적인 예방책이 됩니다.
관련 에러
- 22032 (
invalid json text): JSON 데이터 자체가 파싱 불가능한 형식일 때 발생하며, JSON Path 평가 이전 단계에서 트리거됩니다. - 22033 (
invalid sql json subscript): JSON 배열 인덱스가 유효하지 않은 값(음수, 범위 초과 등)일 때 발생합니다. - 22034 (
json array or string required): JSON Path 함수가 배열이나 문자열을 기대하는 위치에 다른 타입이 입력될 때 발생합니다. - 22035 (
json member not found): 지정한 JSON 키가 오브젝트 내에 존재하지 않을 때 발생하며,ON EMPTY절로 제어할 수 있습니다. - 22037 (
json scalar required):JSON_VALUE()가 스칼라가 아닌 오브젝트나 배열을 반환할 때 발생하며, 22038과 유사한 맥락에서 함께 확인이 필요합니다.
주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.