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

2203B
2026년 10월 27일 | DBMS Error 가이드

이 글에서 다루는 내용

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

2203B sql json number not found 는?

PostgreSQL 에러 코드 2203B는 sql_json_number_not_found 에러로, SQL/JSON 경로 표현식(path expression)을 사용할 때 숫자(number) 타입의 값이 예상되는 위치에서 해당 값을 찾지 못했을 때 발생합니다. 이 에러는 주로 jsonb_path_query, jsonb_path_value, JSON_VALUE(), JSON_QUERY() 등의 SQL/JSON 함수에서 숫자형 결과를 기대하는 쿼리가 실행될 때 나타납니다. PostgreSQL 12 이후 SQL/JSON 경로 언어(SQL/JSON Path Language) 지원이 강화되면서, 타입 검증이 엄격해진 환경에서 특히 자주 마주치는 에러입니다.


주요 발생 원인

1. JSON 경로 표현식이 숫자가 아닌 값을 반환할 때

가장 흔한 원인으로, JSON 데이터에서 숫자 타입을 기대하는 경로 표현식이 문자열, 불리언, null 또는 배열 등 다른 타입의 값을 반환할 때 발생합니다. 예를 들어, $.price 경로가 "12.50"처럼 따옴표로 감싸인 문자열로 저장되어 있을 경우, 숫자로 인식하지 못하고 이 에러를 발생시킵니다. 실무에서 외부 API로부터 수신한 JSON 데이터를 그대로 저장할 경우 이런 타입 불일치가 매우 빈번하게 발생합니다.

2. strict 모드에서 존재하지 않는 키를 숫자로 접근할 때

SQL/JSON 경로 표현식은 기본적으로 lax 모드로 동작하지만, strict 키워드를 명시하면 경로 탐색 중 누락된 키나 타입 불일치에 대해 즉시 에러를 발생시킵니다. strict 모드에서 존재하지 않는 키에 숫자형 연산(예: 산술 연산, .type() 검증)을 시도하면 2203B 에러가 발생합니다. 이는 특히 스키마가 유동적인 semi-structured 데이터를 다룰 때 예상치 못하게 나타납니다.

3. JSON_VALUE() 함수에서 RETURNING 절의 타입 변환 실패

PostgreSQL 15 이상에서 도입된 JSON_VALUE() 함수는 RETURNING 절을 통해 반환 타입을 명시할 수 있습니다. 이때 지정한 타입이 NUMERIC, INTEGER, FLOAT 등 숫자형인데, 실제 JSON 값이 숫자로 캐스팅될 수 없는 형태라면 2203B 에러가 발생합니다. 단순히 NULL을 반환하는 것이 아니라 에러로 처리되기 때문에, 적절한 에러 핸들링 없이는 쿼리 전체가 실패합니다.


해결 방법

원인 1 해결: 타입 확인 후 안전하게 숫자 변환

JSON 경로 탐색 전에 .type() 메서드로 타입을 확인하거나, lax 모드와 조건식을 활용해 안전하게 처리합니다.

-- 문제가 되는 쿼리 예시
SELECT jsonb_path_query('{"price": "12.50"}', '$.price.double()');
-- ERROR: 2203B: SQL/JSON number not found

-- 해결 방법 1: 타입 확인 후 처리
SELECT
  CASE
    WHEN jsonb_typeof(data->'price') = 'number' THEN (data->>'price')::numeric
    WHEN jsonb_typeof(data->'price') = 'string' THEN (data->>'price')::numeric  -- 문자열이지만 숫자로 변환 가능한 경우
    ELSE NULL
  END AS safe_price
FROM (VALUES ('{"price": "12.50"}'::jsonb)) AS t(data);

-- 해결 방법 2: jsonb_path_query_first 와 NULL 처리
SELECT jsonb_path_query_first(
  '{"price": "12.50"}',
  'lax $.price ? (@.type() == "number")'
);

-- 해결 방법 3: 문자열로 먼저 꺼낸 후 캐스팅
SELECT (jsonb_path_query_first('{"price": "12.50"}', 'lax $.price') #>> '{}')::numeric;

원인 2 해결: strict 모드 대신 lax 모드 사용 또는 존재 여부 확인

-- 문제가 되는 쿼리 (strict 모드)
SELECT jsonb_path_query(
  '{"item": "book"}',
  'strict $.price.double()'
);
-- ERROR: 2203B

-- 해결 방법 1: lax 모드로 전환
SELECT jsonb_path_query(
  '{"item": "book"}',
  'lax $.price.double()'
);
-- NULL 반환 (에러 없음)

-- 해결 방법 2: 키 존재 여부를 먼저 확인
SELECT
  CASE
    WHEN data ? 'price' AND jsonb_typeof(data->'price') = 'number'
    THEN jsonb_path_query_first(data, '$.price')::text::numeric
    ELSE 0
  END AS price
FROM (VALUES ('{"item": "book"}'::jsonb)) AS t(data);

-- 해결 방법 3: jsonb_path_exists로 안전하게 확인
SELECT
  CASE
    WHEN jsonb_path_exists(data, '$.price ? (@.type() == "number")')
    THEN jsonb_path_query_first(data, '$.price')::text::numeric
    ELSE NULL
  END AS price
FROM your_table;

원인 3 해결: JSON_VALUE() 함수에서 에러 처리 절 추가

-- 문제가 되는 쿼리
SELECT JSON_VALUE(
  '{"score": "excellent"}',
  '$.score'
  RETURNING numeric
);
-- ERROR: 2203B

-- 해결 방법 1: ERROR ON ERROR 대신 NULL ON ERROR 사용
SELECT JSON_VALUE(
  '{"score": "excellent"}',
  '$.score'
  RETURNING numeric
  NULL ON ERROR
);
-- 결과: NULL (에러 없음)

-- 해결 방법 2: DEFAULT 값 지정
SELECT JSON_VALUE(
  '{"score": "excellent"}',
  '$.score'
  RETURNING numeric
  DEFAULT -1 ON ERROR
);
-- 결과: -1

-- 해결 방법 3: NULL ON EMPTY도 함께 처리
SELECT JSON_VALUE(
  '{"data": {}}',
  '$.data.score'
  RETURNING numeric
  NULL ON EMPTY
  NULL ON ERROR
);

-- 실무 적용 예시: 테이블 전체 처리
SELECT
  id,
  JSON_VALUE(payload, '$.amount' RETURNING numeric NULL ON ERROR) AS amount,
  JSON_VALUE(payload, '$.quantity' RETURNING integer DEFAULT 0 ON ERROR) AS quantity
FROM orders_raw
WHERE created_at >= NOW() - INTERVAL '7 days';

예방 방법

1. JSON 데이터 수집 시점에 타입 정규화 및 스키마 검증 적용

외부 시스템에서 JSON 데이터를 수신할 때는 반드시 저장 전에 타입 정규화를 수행하는 것이 중요합니다. PostgreSQL의 CHECK 제약 조건과 jsonb_path_exists()를 결합하면 테이블 수준에서 JSON 스키마를 강제할 수 있습니다.

-- 테이블 생성 시 CHECK 제약으로 숫자 타입 강제
CREATE TABLE product_data (
  id SERIAL PRIMARY KEY,
  payload JSONB NOT NULL,
  CONSTRAINT chk_price_is_number
    CHECK (
      payload->'price' IS NULL OR
      jsonb_typeof(payload->'price') = 'number'
    ),
  CONSTRAINT chk_quantity_is_number
    CHECK (
      payload->'quantity' IS NULL OR
      jsonb_typeof(payload->'quantity') = 'number'
    )
);

-- 입력 시 정규화 함수 활용
CREATE OR REPLACE FUNCTION normalize_product_payload(raw jsonb)
RETURNS jsonb AS $$
BEGIN
  -- 문자열로 저장된 숫자를 실제 숫자로 변환
  IF jsonb_typeof(raw->'price') = 'string' THEN
    raw := jsonb_set(raw, '{price}', to_jsonb((raw->>'price')::numeric));
  END IF;
  RETURN raw;
END;
$$ LANGUAGE plpgsql;

2. SQL/JSON 함수 사용 시 항상 ON ERROR 절과 lax 모드 기본 채택

프로덕션 환경에서는 JSON 데이터의 타입이 보장되지 않는 경우가 많으므로, SQL/JSON 관련 함수를 작성할 때 NULL ON ERROR, DEFAULT ... ON ERROR 등의 에러 처리 절을 표준으로 채택해야 합니다. 또한 경로 표현식에서 strict 모드는 스키마가 완전히 고정된 경우에만 사용하고, 그 외에는 lax 모드를 기본으로 사용하는 팀 내 코딩 컨벤션을 수립하는 것을 강력히 권장합니다.

-- 팀 내 표준 패턴으로 정의된 안전한 숫자 추출 함수
CREATE OR REPLACE FUNCTION safe_json_number(data jsonb, path text)
RETURNS numeric AS $$
DECLARE
  result text;
BEGIN
  SELECT jsonb_path_query_first(data, path::jsonpath)::text INTO result;
  RETURN result::numeric;
EXCEPTION WHEN OTHERS THEN
  RETURN NULL;
END;
$$ LANGUAGE plpgsql STABLE;

-- 사용 예시
SELECT safe_json_number(payload, '$.price') FROM product_data;

관련 에러

  • 22033 (invalid_sql_json_subscript): JSON 배열 인덱스가 유효하지 않을 때 발생하며, 배열 요소 접근 시 숫자 인덱스 문제와 함께 발생할 수 있습니다.
  • 22034 (more_than_one_sql_json_item): SQL/JSON 경로 표현식이 단일 값이 아닌 다중 값을 반환할 때 발생하며, JSON_VALUE()에서 자주 함께 나타납니다.
  • 22035 (no_sql_json_item): 경로 표현식에 해당하는 항목 자체가 없을 때 발생하는 에러로, 2203B와 유사한 상황(존재하지 않는 키)에서 함께 발생할 수 있습니다.
  • 22032 (invalid_json_text): JSON 텍스트 자체가 유효하지 않을 때 발생하며, 파싱 실패로 인해 후속 타입 검증 에러로 이어지는 경우가 있습니다.
  • 2203F (sql_json_scalar_required): 스칼라 값이 필요한 곳에 배열이나 객체가 반환될 때 발생하며, 숫자 타입 요구 에러인 2203B와 함께 SQL/JSON 타입 관련 에러군을 형성합니다.
DBMS 에러 코드 시리즈

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

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

댓글 남기기