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

20000
2026년 08월 07일 | DBMS Error 가이드

이 글에서 다루는 내용

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

20000 case not found 는?

PostgreSQL 에러 코드 20000case_not_found라는 이름으로, PL/pgSQL의 CASE 문에서 어떠한 조건에도 일치하는 케이스가 없을 때 발생하는 에러입니다. 이 에러는 CASE 문에 ELSE 절이 없는 상태에서 모든 WHEN 조건이 거짓(false)으로 평가될 때 런타임 오류로 발생하며, SQL의 CASE 표현식이 아닌 PL/pgSQL의 CASE 구문에서만 발생한다는 점이 중요합니다. 실무 환경에서는 예상치 못한 데이터 값이 유입되거나 비즈니스 로직의 변경으로 인해 기존 CASE 구문이 새로운 케이스를 처리하지 못할 때 주로 나타납니다.


주요 발생 원인

  • ELSE 절 없는 CASE 구문에 예상 외 값 유입

가장 흔한 원인입니다. PL/pgSQL 함수나 프로시저 내에서 CASE 구문을 작성할 때 ELSE 절을 생략하면, 정의된 WHEN 조건 중 어느 것도 만족하지 않는 값이 들어올 경우 즉시 case_not_found 에러가 발생합니다. 예를 들어 처음에는 ‘A’, ‘B’, ‘C’만 존재하는 컬럼에 대해 CASE를 작성했지만, 나중에 ‘D’라는 값이 추가되었을 때 기존 코드가 이를 처리하지 못하는 상황입니다.

  • NULL 값에 대한 처리 누락

CASE 구문에서 NULL 값을 별도로 처리하지 않으면 문제가 발생할 수 있습니다. CASE 변수 형식(CASE variable WHEN value THEN ...)을 사용할 경우, NULL = NULL은 항상 FALSE를 반환하므로 NULL 값이 들어오면 어떤 WHEN 절도 매칭되지 않아 에러가 발생합니다. 데이터 품질이 완벽하지 않은 레거시 시스템이나 외부 데이터를 처리할 때 특히 자주 발생합니다.

  • 동적으로 증가하는 Enum 혹은 코드 테이블 값

애플리케이션이 성장하면서 상태 코드, 타입 코드 등의 값이 추가되는 경우, 기존에 작성된 PL/pgSQL 함수나 트리거의 CASE 구문이 새로운 값을 처리하지 못하게 됩니다. 이런 상황은 특히 여러 팀이 협업하는 환경에서 한쪽 팀이 새 코드 값을 추가했지만 DB 함수를 수정하지 않은 경우에 빈번하게 발생하며, 서비스 운영 중 갑작스러운 에러를 유발합니다.


해결 방법

원인 1 해결: ELSE 절 추가

CASE 구문에 반드시 ELSE 절을 추가하여 예외 케이스를 명시적으로 처리합니다.

-- 문제가 있는 코드
CREATE OR REPLACE FUNCTION get_status_label(p_status TEXT)
RETURNS TEXT AS $$
DECLARE
    v_label TEXT;
BEGIN
    CASE p_status
        WHEN 'A' THEN v_label := '활성';
        WHEN 'B' THEN v_label := '비활성';
        WHEN 'C' THEN v_label := '대기';
        -- 'D'가 들어오면 에러 발생!
    END CASE;
    RETURN v_label;
END;
$$ LANGUAGE plpgsql;

-- 수정된 코드: ELSE 절 추가
CREATE OR REPLACE FUNCTION get_status_label(p_status TEXT)
RETURNS TEXT AS $$
DECLARE
    v_label TEXT;
BEGIN
    CASE p_status
        WHEN 'A' THEN v_label := '활성';
        WHEN 'B' THEN v_label := '비활성';
        WHEN 'C' THEN v_label := '대기';
        ELSE
            -- 예외 케이스: 에러를 발생시키거나 기본값 반환
            RAISE EXCEPTION '알 수 없는 상태값: %', p_status
                USING ERRCODE = 'invalid_parameter_value';
    END CASE;
    RETURN v_label;
END;
$$ LANGUAGE plpgsql;

-- 또는 기본값을 반환하는 방식
CREATE OR REPLACE FUNCTION get_status_label_safe(p_status TEXT)
RETURNS TEXT AS $$
DECLARE
    v_label TEXT;
BEGIN
    CASE p_status
        WHEN 'A' THEN v_label := '활성';
        WHEN 'B' THEN v_label := '비활성';
        WHEN 'C' THEN v_label := '대기';
        ELSE v_label := '알 수 없음';  -- 기본값 처리
    END CASE;
    RETURN v_label;
END;
$$ LANGUAGE plpgsql;

원인 2 해결: NULL 값 명시적 처리

IS NULL 조건을 별도로 추가하거나, COALESCE를 활용하여 NULL을 사전에 처리합니다.

-- NULL 처리가 포함된 안전한 CASE 구문
CREATE OR REPLACE FUNCTION process_category(p_category TEXT)
RETURNS TEXT AS $$
DECLARE
    v_result TEXT;
BEGIN
    -- 방법 1: WHEN 절에서 직접 NULL 처리
    CASE
        WHEN p_category IS NULL THEN
            v_result := '카테고리 없음';
        WHEN p_category = 'FOOD' THEN
            v_result := '식품';
        WHEN p_category = 'ELEC' THEN
            v_result := '전자기기';
        WHEN p_category = 'CLOTH' THEN
            v_result := '의류';
        ELSE
            v_result := '기타: ' || p_category;
    END CASE;
    RETURN v_result;
END;
$$ LANGUAGE plpgsql;

-- 방법 2: COALESCE로 사전 처리
CREATE OR REPLACE FUNCTION process_category_v2(p_category TEXT)
RETURNS TEXT AS $$
DECLARE
    v_safe_category TEXT;
    v_result TEXT;
BEGIN
    -- NULL을 기본값으로 치환
    v_safe_category := COALESCE(p_category, 'UNKNOWN');

    CASE v_safe_category
        WHEN 'FOOD'    THEN v_result := '식품';
        WHEN 'ELEC'    THEN v_result := '전자기기';
        WHEN 'CLOTH'   THEN v_result := '의류';
        WHEN 'UNKNOWN' THEN v_result := '카테고리 없음';
        ELSE v_result := '기타';
    END CASE;
    RETURN v_result;
END;
$$ LANGUAGE plpgsql;

원인 3 해결: 동적 코드값 처리를 위한 테이블 기반 조회

하드코딩된 CASE 대신 코드 테이블을 조회하는 방식으로 전환합니다.

-- 코드 테이블 생성
CREATE TABLE IF NOT EXISTS code_master (
    code_type   TEXT NOT NULL,
    code_value  TEXT NOT NULL,
    code_label  TEXT NOT NULL,
    is_active   BOOLEAN DEFAULT TRUE,
    PRIMARY KEY (code_type, code_value)
);

INSERT INTO code_master VALUES
    ('STATUS', 'A', '활성',   TRUE),
    ('STATUS', 'B', '비활성', TRUE),
    ('STATUS', 'C', '대기',   TRUE),
    ('STATUS', 'D', '삭제',   TRUE);  -- 신규 추가도 함수 수정 불필요

-- 코드 테이블을 활용한 함수 (CASE 구문 불필요)
CREATE OR REPLACE FUNCTION get_label_from_table(
    p_code_type  TEXT,
    p_code_value TEXT
)
RETURNS TEXT AS $$
DECLARE
    v_label TEXT;
BEGIN
    SELECT code_label
      INTO v_label
      FROM code_master
     WHERE code_type  = p_code_type
       AND code_value = p_code_value
       AND is_active  = TRUE;

    IF NOT FOUND THEN
        RAISE WARNING '코드값을 찾을 수 없습니다: type=%, value=%',
            p_code_type, p_code_value;
        RETURN '알 수 없음';
    END IF;

    RETURN v_label;
END;
$$ LANGUAGE plpgsql;

-- 사용 예시
SELECT get_label_from_table('STATUS', 'D');  -- '삭제' 반환
SELECT get_label_from_table('STATUS', 'Z');  -- '알 수 없음' 반환 (에러 없음)

에러 핸들링: EXCEPTION 블록으로 에러 포착

기존 코드를 즉시 수정하기 어려운 경우, EXCEPTION 블록으로 에러를 임시 처리합니다.

CREATE OR REPLACE FUNCTION safe_case_handler(p_value TEXT)
RETURNS TEXT AS $$
DECLARE
    v_result TEXT;
BEGIN
    CASE p_value
        WHEN 'X' THEN v_result := '케이스 X';
        WHEN 'Y' THEN v_result := '케이스 Y';
    END CASE;
    RETURN v_result;
EXCEPTION
    WHEN case_not_found THEN
        -- 에러 로그 기록 후 기본값 반환
        RAISE WARNING '[safe_case_handler] 처리되지 않은 값: %', p_value;
        RETURN '처리 불가';
END;
$$ LANGUAGE plpgsql;

예방 방법

  • 코드 리뷰 시 ELSE 절 필수 확인 정책 수립

팀 내 코딩 컨벤션에 “PL/pgSQL의 모든 CASE 구문에는 반드시 ELSE 절을 포함한다”는 규칙을 명문화하세요. CI/CD 파이프라인에서 PL/pgSQL 코드를 정적 분석하는 스크립트를 추가하거나, 코드 리뷰 체크리스트에 CASE 구문의 ELSE 절 존재 여부를 필수 항목으로 등록하면 근본적인 예방이 가능합니다. 아래와 같이 pg_proc 카탈로그를 조회하여 기존 함수들을 점검하는 것도 좋은 습관입니다.

“`sql

— ELSE 없는 CASE 구문이 포함된 함수 탐색 (간단 패턴 매칭)

SELECT proname, prosrc

FROM pg_proc

WHERE prolang = (SELECT oid FROM pg_language WHERE lanname = ‘plpgsql’)

AND prosrc LIKE ‘%CASE%’

AND prosrc NOT LIKE ‘%ELSE%’

AND pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = ‘public’);

“`

  • 통합 테스트에 경계값 및 신규 코드값 테스트 케이스 포함

함수나 트리거를 배포하기 전에, 정의된 값 외에 NULL, 빈 문자열(''), 예상 외 문자열 등을 입력으로 하는 테스트 케이스를 반드시 포함시켜야 합니다. pgTAP 같은 PostgreSQL 테스트 프레임워크를 활용하면 코드 변경 시 자동으로 경계값 테스트를 실행할 수 있어, 신규 코드값 추가 시 영향을 받는 함수를 사전에 발견할 수 있습니다.

“`sql

— pgTAP을 활용한 예시 테스트

SELECT plan(4);

SELECT is(get_status_label(‘A’), ‘활성’, ‘A 상태 정상 처리’);

SELECT is(get_status_label(‘B’), ‘비활성’, ‘B 상태 정상 처리’);

SELECT is(get_status_label(NULL), ‘알 수 없음’, ‘NULL 값 안전 처리’);

SELECT is(get_status_label(‘ZZZZ’), ‘알 수 없음’, ‘미정의 값 안전 처리’);

SELECT finish();

“`


관련 에러

  • P0001 (raise_exception): CASE 구문의 ELSE 절에서 RAISE EXCEPTION을 사용할 경우 발생하며, case_not_found의 대안적 처리 방식으로 활용됩니다.
  • P0002 (no_data_found): PL/pgSQL에서 SELECT INTO가 결과를 반환하지 않을 때 발생하는 에러로, CASE 기반 로직을 테이블 조회 방식으로 전환할 때 함께 고려해야 합니다.
  • P0003 (too_many_rows): SELECT INTO가 둘 이상의 행을 반환할 때 발생하며, 코드 테이블 조회 방식으로 CASE를 대체할 때 주의해야 할 에러입니다.
  • 22023 (invalid_parameter_value): 잘못된 파라미터 값이 전달될 때 발생하며, case_not_found 상황에서 명시적으로 이 에러를 발생시켜 더 명확한 오류 메시지를 제공하는 패턴에서 함께 사용됩니다.
DBMS 에러 코드 시리즈

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

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

댓글 남기기